@integrity-labs/agt-cli 0.27.8 → 0.27.9-test.12
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 +946 -129
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-YSBGIXJG.js → chunk-I2OTQJH3.js} +361 -26
- package/dist/chunk-I2OTQJH3.js.map +1 -0
- package/dist/{chunk-GN4XPQWJ.js → chunk-MQJ5DMPT.js} +535 -94
- package/dist/chunk-MQJ5DMPT.js.map +1 -0
- package/dist/{chunk-IB655E5U.js → chunk-SXBZDUKN.js} +1086 -285
- package/dist/chunk-SXBZDUKN.js.map +1 -0
- package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-F6USL3EW.js} +2 -2
- package/dist/lib/manager-worker.js +2071 -517
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +49 -455
- package/dist/mcp/index.js +232 -137
- package/dist/mcp/slack-channel.js +1379 -856
- package/dist/mcp/teams-channel.js +16018 -0
- package/dist/mcp/telegram-channel.js +1264 -618
- package/dist/{persistent-session-ICYFLUAM.js → persistent-session-OL5EDYB4.js} +5 -3
- package/dist/responsiveness-probe-B6LJJRUD.js +74 -0
- package/dist/responsiveness-probe-B6LJJRUD.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-GN4XPQWJ.js.map +0 -1
- package/dist/chunk-IB655E5U.js.map +0 -1
- package/dist/chunk-YSBGIXJG.js.map +0 -1
- package/dist/responsiveness-probe-WZNQ2762.js +0 -33
- package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
- /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-F6USL3EW.js.map} +0 -0
- /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-OL5EDYB4.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,17 +14218,468 @@ 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
|
+
}
|
|
14303
|
+
|
|
14304
|
+
// src/ack-reaction.ts
|
|
14305
|
+
import { readdirSync, readFileSync } from "fs";
|
|
14306
|
+
import { join } from "path";
|
|
14307
|
+
var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
14308
|
+
var ACK_STARTUP_GRACE_MS = 6e4;
|
|
14309
|
+
var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
|
|
14310
|
+
function decideAckReaction(i) {
|
|
14311
|
+
if (!i.hasTarget) return "none";
|
|
14312
|
+
if (!i.integrationReady) return "undeliverable";
|
|
14313
|
+
if (i.tmux === "dead") return "undeliverable";
|
|
14314
|
+
if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
|
|
14315
|
+
const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
|
|
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
|
+
}
|
|
14322
|
+
return "undeliverable";
|
|
14323
|
+
}
|
|
14324
|
+
return "ack";
|
|
14325
|
+
}
|
|
14326
|
+
function decideRecoveryHeal(i) {
|
|
14327
|
+
return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
|
|
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
|
+
}
|
|
14362
|
+
function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
|
|
14363
|
+
if (!dir) return null;
|
|
14364
|
+
let names;
|
|
14365
|
+
try {
|
|
14366
|
+
names = readdirSync(dir);
|
|
14367
|
+
} catch {
|
|
14368
|
+
return null;
|
|
14369
|
+
}
|
|
14370
|
+
let oldest = null;
|
|
14371
|
+
for (const name of names) {
|
|
14372
|
+
if (!name.endsWith(".json")) continue;
|
|
14373
|
+
let receivedAt;
|
|
14374
|
+
try {
|
|
14375
|
+
const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
14376
|
+
receivedAt = raw.received_at;
|
|
14377
|
+
} catch {
|
|
14378
|
+
continue;
|
|
14379
|
+
}
|
|
14380
|
+
if (!receivedAt) continue;
|
|
14381
|
+
const t = Date.parse(receivedAt);
|
|
14382
|
+
if (Number.isNaN(t)) continue;
|
|
14383
|
+
const age = now - t;
|
|
14384
|
+
if (age < 0) continue;
|
|
14385
|
+
if (oldest == null || age > oldest) oldest = age;
|
|
14386
|
+
}
|
|
14387
|
+
return oldest;
|
|
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
|
+
}
|
|
14404
|
+
|
|
14405
|
+
// src/session-probe-runtime.ts
|
|
14406
|
+
import { execFileSync } from "child_process";
|
|
14407
|
+
function agentTmuxSessionName(codeName) {
|
|
14408
|
+
return `agt-${codeName}`;
|
|
14409
|
+
}
|
|
14410
|
+
function escapePgrepRegex(value) {
|
|
14411
|
+
return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
|
|
14412
|
+
}
|
|
14413
|
+
function probeClaudeProcessInTmux(tmuxSession) {
|
|
14414
|
+
const escapedSession = escapePgrepRegex(tmuxSession);
|
|
14415
|
+
const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
|
|
14416
|
+
try {
|
|
14417
|
+
const out = execFileSync("pgrep", ["-f", "--", pattern], {
|
|
14418
|
+
encoding: "utf-8",
|
|
14419
|
+
timeout: 3e3
|
|
14420
|
+
}).trim();
|
|
14421
|
+
return out.length > 0 ? "alive" : "dead";
|
|
14422
|
+
} catch (err) {
|
|
14423
|
+
const e = err;
|
|
14424
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14425
|
+
return e?.status === 1 ? "dead" : "unknown";
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
14428
|
+
function probeTmuxSession(tmuxSession) {
|
|
14429
|
+
try {
|
|
14430
|
+
execFileSync("tmux", ["has-session", "-t", tmuxSession], {
|
|
14431
|
+
stdio: "ignore",
|
|
14432
|
+
timeout: 3e3
|
|
14433
|
+
});
|
|
14434
|
+
return "alive";
|
|
14435
|
+
} catch (err) {
|
|
14436
|
+
const e = err;
|
|
14437
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14438
|
+
return "dead";
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
function probeAgentSession(codeName) {
|
|
14442
|
+
const session = agentTmuxSessionName(codeName);
|
|
14443
|
+
const tmux = probeTmuxSession(session);
|
|
14444
|
+
const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
|
|
14445
|
+
return { tmux, claude };
|
|
14446
|
+
}
|
|
14447
|
+
var probeCache = /* @__PURE__ */ new Map();
|
|
14448
|
+
var SESSION_PROBE_TTL_MS = 15e3;
|
|
14449
|
+
function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
|
|
14450
|
+
const cached2 = probeCache.get(codeName);
|
|
14451
|
+
if (cached2 && now - cached2.at < ttlMs) return cached2.value;
|
|
14452
|
+
const value = probeAgentSession(codeName);
|
|
14453
|
+
probeCache.set(codeName, { at: now, value });
|
|
14454
|
+
return value;
|
|
14455
|
+
}
|
|
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
|
+
}
|
|
14251
14683
|
|
|
14252
14684
|
// src/slack-loop-throttle.ts
|
|
14253
14685
|
var DEFAULT_THROTTLE = {
|
|
@@ -14351,479 +14783,185 @@ async function isThreadKilled(opts) {
|
|
|
14351
14783
|
}
|
|
14352
14784
|
}
|
|
14353
14785
|
|
|
14354
|
-
// src/slack-
|
|
14355
|
-
var
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
}
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
const
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
const
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
}
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
}
|
|
14392
|
-
|
|
14786
|
+
// src/slack-thread-context.ts
|
|
14787
|
+
var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
|
|
14788
|
+
var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
|
|
14789
|
+
function formatThreadMessages(messages, nameById) {
|
|
14790
|
+
return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
|
|
14791
|
+
}
|
|
14792
|
+
function capThreadContext(formatted, maxChars) {
|
|
14793
|
+
if (formatted.length <= maxChars) return formatted;
|
|
14794
|
+
const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
|
|
14795
|
+
if (maxChars <= marker.length) return marker.slice(0, maxChars);
|
|
14796
|
+
const tailBudget = maxChars - marker.length;
|
|
14797
|
+
const tail = formatted.slice(formatted.length - tailBudget);
|
|
14798
|
+
const firstNewline = tail.indexOf("\n");
|
|
14799
|
+
const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
|
|
14800
|
+
return `${marker}${clean}`;
|
|
14801
|
+
}
|
|
14802
|
+
async function fetchThreadTranscript(channel, threadTs, limit, deps) {
|
|
14803
|
+
const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
|
|
14804
|
+
const cappedLimit = Math.min(Math.max(limit, 1), 200);
|
|
14805
|
+
const allMessages = [];
|
|
14806
|
+
let cursor;
|
|
14807
|
+
do {
|
|
14808
|
+
const remaining = cappedLimit - allMessages.length;
|
|
14809
|
+
if (remaining <= 0) break;
|
|
14810
|
+
const params = new URLSearchParams({
|
|
14811
|
+
channel,
|
|
14812
|
+
ts: threadTs,
|
|
14813
|
+
limit: String(Math.min(remaining, 100)),
|
|
14814
|
+
...cursor ? { cursor } : {}
|
|
14815
|
+
});
|
|
14816
|
+
let data;
|
|
14817
|
+
try {
|
|
14818
|
+
const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
|
|
14819
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
14820
|
+
});
|
|
14821
|
+
if (!res.ok) return { ok: false, error: `http_${res.status}` };
|
|
14822
|
+
data = await res.json();
|
|
14823
|
+
} catch (err) {
|
|
14824
|
+
return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
|
|
14825
|
+
}
|
|
14826
|
+
if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
|
|
14827
|
+
for (const msg of data.messages ?? []) {
|
|
14828
|
+
allMessages.push({
|
|
14829
|
+
user: msg.user ?? msg.bot_id ?? "unknown",
|
|
14830
|
+
text: msg.text ?? "",
|
|
14831
|
+
ts: msg.ts ?? ""
|
|
14832
|
+
});
|
|
14393
14833
|
}
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14834
|
+
cursor = data.response_metadata?.next_cursor || void 0;
|
|
14835
|
+
} while (cursor && allMessages.length < cappedLimit);
|
|
14836
|
+
const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
|
|
14837
|
+
const resolved = await Promise.all(
|
|
14838
|
+
uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
|
|
14839
|
+
);
|
|
14840
|
+
const nameById = new Map(resolved);
|
|
14841
|
+
return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
|
|
14397
14842
|
}
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
const
|
|
14843
|
+
|
|
14844
|
+
// src/impersonation.ts
|
|
14845
|
+
var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
|
|
14846
|
+
var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
|
|
14847
|
+
function isImpersonating() {
|
|
14848
|
+
return impersonatedAgentId() !== null;
|
|
14849
|
+
}
|
|
14850
|
+
function impersonatedAgentId() {
|
|
14851
|
+
const raw = process.env[ENV_VAR];
|
|
14852
|
+
if (typeof raw !== "string") return null;
|
|
14853
|
+
const trimmed = raw.trim();
|
|
14854
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
14855
|
+
}
|
|
14856
|
+
function channelsEnabledOverride() {
|
|
14857
|
+
const raw = process.env[OVERRIDE_ENV_VAR];
|
|
14858
|
+
if (typeof raw !== "string") return false;
|
|
14859
|
+
const trimmed = raw.trim().toLowerCase();
|
|
14860
|
+
return trimmed === "1" || trimmed === "true" || trimmed === "yes" || trimmed === "on";
|
|
14861
|
+
}
|
|
14862
|
+
var IMPERSONATION_REFUSAL_CODE = "CHANNEL.IMPERSONATION_DISABLED";
|
|
14863
|
+
var IMPERSONATION_REFUSAL_MESSAGE = "Posting as an agent under impersonation is disabled in v0 \u2014 quit impersonation with `agt impersonate exit` to send as yourself.";
|
|
14864
|
+
function buildImpersonationRefusal(toolName) {
|
|
14407
14865
|
return {
|
|
14408
|
-
|
|
14409
|
-
blocks: [
|
|
14866
|
+
content: [
|
|
14410
14867
|
{
|
|
14411
|
-
type: "
|
|
14412
|
-
text:
|
|
14868
|
+
type: "text",
|
|
14869
|
+
text: JSON.stringify({
|
|
14870
|
+
error: {
|
|
14871
|
+
code: IMPERSONATION_REFUSAL_CODE,
|
|
14872
|
+
message: IMPERSONATION_REFUSAL_MESSAGE,
|
|
14873
|
+
tool: toolName
|
|
14874
|
+
}
|
|
14875
|
+
})
|
|
14413
14876
|
}
|
|
14414
|
-
]
|
|
14877
|
+
],
|
|
14878
|
+
isError: true
|
|
14415
14879
|
};
|
|
14416
14880
|
}
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
var
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14881
|
+
|
|
14882
|
+
// src/channel-egress-tools.ts
|
|
14883
|
+
var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
|
|
14884
|
+
"slack.reply",
|
|
14885
|
+
"slack.react",
|
|
14886
|
+
"slack.send_structured",
|
|
14887
|
+
"slack.ask_user",
|
|
14888
|
+
"slack.upload_file"
|
|
14889
|
+
]);
|
|
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 {
|
|
14427
14901
|
}
|
|
14428
14902
|
};
|
|
14429
|
-
function
|
|
14430
|
-
|
|
14431
|
-
const
|
|
14432
|
-
let
|
|
14433
|
-
|
|
14434
|
-
const
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
try {
|
|
14439
|
-
response = await fetchImpl(url, {
|
|
14440
|
-
method: "POST",
|
|
14441
|
-
headers: {
|
|
14442
|
-
"Content-Type": "application/json",
|
|
14443
|
-
Authorization: `Bearer ${opts.botToken}`
|
|
14444
|
-
},
|
|
14445
|
-
body: JSON.stringify(body),
|
|
14446
|
-
signal: controller.signal
|
|
14447
|
-
});
|
|
14448
|
-
} finally {
|
|
14449
|
-
clearTimeout(timer);
|
|
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;
|
|
14450
14912
|
}
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
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;
|
|
14454
14927
|
}
|
|
14455
|
-
|
|
14928
|
+
} catch {
|
|
14456
14929
|
}
|
|
14457
|
-
return
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
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;
|
|
14466
14939
|
try {
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
throw new SlackApiError(
|
|
14470
|
-
"chat.postMessage",
|
|
14471
|
-
"missing ts in successful response",
|
|
14472
|
-
200
|
|
14473
|
-
);
|
|
14474
|
-
}
|
|
14475
|
-
anchorTs = res.ts;
|
|
14476
|
-
opts.onAnchorPosted?.(res.ts);
|
|
14477
|
-
} catch (err) {
|
|
14478
|
-
throw err;
|
|
14479
|
-
}
|
|
14480
|
-
return;
|
|
14481
|
-
}
|
|
14482
|
-
try {
|
|
14483
|
-
await slackCall("chat.update", {
|
|
14484
|
-
channel: opts.channel,
|
|
14485
|
-
ts: anchorTs,
|
|
14486
|
-
text: payload.text,
|
|
14487
|
-
blocks: payload.blocks
|
|
14488
|
-
});
|
|
14489
|
-
} catch (err) {
|
|
14490
|
-
if (opts.onApiError && err instanceof SlackApiError) {
|
|
14491
|
-
await opts.onApiError(err);
|
|
14492
|
-
return;
|
|
14940
|
+
mtime = statSync(full).mtimeMs;
|
|
14941
|
+
} catch {
|
|
14493
14942
|
}
|
|
14494
|
-
|
|
14495
|
-
}
|
|
14496
|
-
|
|
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
|
+
}
|
|
14497
14952
|
}
|
|
14498
14953
|
|
|
14499
|
-
//
|
|
14500
|
-
import
|
|
14954
|
+
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
14955
|
+
import process2 from "process";
|
|
14501
14956
|
|
|
14502
|
-
//
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
const clearTimer = opts.clearTimer ?? ((id) => clearTimeout(id));
|
|
14507
|
-
const debounceMs = opts.debounceMs ?? 1e3;
|
|
14508
|
-
const onPostTerminalUpdate = opts.onPostTerminalUpdate ?? defaultPostTerminalWarn;
|
|
14509
|
-
const state = {
|
|
14510
|
-
initialLabel: opts.initialLabel,
|
|
14511
|
-
mode: opts.initialMode ?? "working",
|
|
14512
|
-
lastChangeAt: now()
|
|
14513
|
-
};
|
|
14514
|
-
let lastFlushAt = 0;
|
|
14515
|
-
let pendingTimer = null;
|
|
14516
|
-
let inFlight = null;
|
|
14517
|
-
let terminal = false;
|
|
14518
|
-
async function doFlush() {
|
|
14519
|
-
while (inFlight) await inFlight;
|
|
14520
|
-
if (pendingTimer != null) {
|
|
14521
|
-
clearTimer(pendingTimer);
|
|
14522
|
-
pendingTimer = null;
|
|
14523
|
-
}
|
|
14524
|
-
lastFlushAt = now();
|
|
14525
|
-
const promise = opts.flush(snapshotState());
|
|
14526
|
-
inFlight = promise.then(
|
|
14527
|
-
() => {
|
|
14528
|
-
inFlight = null;
|
|
14529
|
-
},
|
|
14530
|
-
(err) => {
|
|
14531
|
-
inFlight = null;
|
|
14532
|
-
throw err;
|
|
14533
|
-
}
|
|
14534
|
-
);
|
|
14535
|
-
await inFlight;
|
|
14536
|
-
}
|
|
14537
|
-
function snapshotState() {
|
|
14538
|
-
return {
|
|
14539
|
-
initialLabel: state.initialLabel,
|
|
14540
|
-
mode: state.mode,
|
|
14541
|
-
stepLabel: state.stepLabel,
|
|
14542
|
-
stepNumber: state.stepNumber ? { ...state.stepNumber } : void 0,
|
|
14543
|
-
detail: state.detail,
|
|
14544
|
-
lastChangeAt: state.lastChangeAt,
|
|
14545
|
-
terminal: state.terminal ? { ...state.terminal } : void 0
|
|
14546
|
-
};
|
|
14957
|
+
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
14958
|
+
var ReadBuffer = class {
|
|
14959
|
+
append(chunk) {
|
|
14960
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
14547
14961
|
}
|
|
14548
|
-
|
|
14549
|
-
if (
|
|
14550
|
-
|
|
14551
|
-
if (next.stepNumber !== void 0) state.stepNumber = { ...next.stepNumber };
|
|
14552
|
-
if (next.detail !== void 0) state.detail = next.detail;
|
|
14553
|
-
state.lastChangeAt = now();
|
|
14554
|
-
}
|
|
14555
|
-
async function scheduleOrFlush() {
|
|
14556
|
-
const elapsed = now() - lastFlushAt;
|
|
14557
|
-
if (elapsed >= debounceMs) {
|
|
14558
|
-
await doFlush();
|
|
14559
|
-
return;
|
|
14560
|
-
}
|
|
14561
|
-
if (pendingTimer != null) return;
|
|
14562
|
-
const remaining = debounceMs - elapsed;
|
|
14563
|
-
pendingTimer = setTimer(() => {
|
|
14564
|
-
pendingTimer = null;
|
|
14565
|
-
void doFlush().catch(() => {
|
|
14566
|
-
});
|
|
14567
|
-
}, remaining);
|
|
14568
|
-
}
|
|
14569
|
-
lastFlushAt = now();
|
|
14570
|
-
await opts.flush(snapshotState());
|
|
14571
|
-
return {
|
|
14572
|
-
snapshot: snapshotState,
|
|
14573
|
-
async update(next) {
|
|
14574
|
-
if (terminal) {
|
|
14575
|
-
const peeked = snapshotState();
|
|
14576
|
-
Object.assign(peeked, {
|
|
14577
|
-
mode: next.mode ?? peeked.mode,
|
|
14578
|
-
stepLabel: next.stepLabel ?? peeked.stepLabel,
|
|
14579
|
-
stepNumber: next.stepNumber ?? peeked.stepNumber,
|
|
14580
|
-
detail: next.detail ?? peeked.detail
|
|
14581
|
-
});
|
|
14582
|
-
try {
|
|
14583
|
-
onPostTerminalUpdate(peeked);
|
|
14584
|
-
} catch (err) {
|
|
14585
|
-
console.warn(
|
|
14586
|
-
"[progress-handle] onPostTerminalUpdate threw \u2014 swallowed to keep update() non-throwing:",
|
|
14587
|
-
err
|
|
14588
|
-
);
|
|
14589
|
-
}
|
|
14590
|
-
return;
|
|
14591
|
-
}
|
|
14592
|
-
applyUpdate(next);
|
|
14593
|
-
await scheduleOrFlush();
|
|
14594
|
-
},
|
|
14595
|
-
async complete(summary) {
|
|
14596
|
-
if (terminal) return;
|
|
14597
|
-
terminal = true;
|
|
14598
|
-
state.terminal = { kind: "completed", message: summary };
|
|
14599
|
-
state.lastChangeAt = now();
|
|
14600
|
-
await doFlush();
|
|
14601
|
-
},
|
|
14602
|
-
async fail(reason) {
|
|
14603
|
-
if (terminal) return;
|
|
14604
|
-
terminal = true;
|
|
14605
|
-
state.terminal = { kind: "failed", message: reason };
|
|
14606
|
-
state.lastChangeAt = now();
|
|
14607
|
-
await doFlush();
|
|
14608
|
-
},
|
|
14609
|
-
isTerminal: () => terminal
|
|
14610
|
-
};
|
|
14611
|
-
}
|
|
14612
|
-
function defaultPostTerminalWarn(state) {
|
|
14613
|
-
console.warn(
|
|
14614
|
-
`[progress-handle] update() called after terminal transition (${state.terminal?.kind}) \u2014 ignored.`
|
|
14615
|
-
);
|
|
14616
|
-
}
|
|
14617
|
-
|
|
14618
|
-
// src/progress-tools.ts
|
|
14619
|
-
var ProgressToolRegistry = class {
|
|
14620
|
-
constructor(opts) {
|
|
14621
|
-
this.opts = opts;
|
|
14622
|
-
this.toolNames = {
|
|
14623
|
-
start: `${opts.prefix}_progress_start`,
|
|
14624
|
-
update: `${opts.prefix}_progress_update`,
|
|
14625
|
-
complete: `${opts.prefix}_progress_complete`,
|
|
14626
|
-
fail: `${opts.prefix}_progress_fail`
|
|
14627
|
-
};
|
|
14628
|
-
}
|
|
14629
|
-
handles = /* @__PURE__ */ new Map();
|
|
14630
|
-
toolNames;
|
|
14631
|
-
/** Tool definitions to splice into the MCP ListToolsRequest response. */
|
|
14632
|
-
getDefinitions() {
|
|
14633
|
-
const { surfaceDescription, startArgsSchema } = this.opts;
|
|
14634
|
-
const debounceMs = this.opts.debounceMs ?? 1e3;
|
|
14635
|
-
const debounceText = debounceMs === 1e3 ? "per second" : `every ${debounceMs}ms`;
|
|
14636
|
-
return [
|
|
14637
|
-
{
|
|
14638
|
-
name: this.toolNames.start,
|
|
14639
|
-
description: `Post a "still working" anchor to ${surfaceDescription} and return a progress_id. The anchor message updates in place as you call ${this.toolNames.update} \u2014 operators see one message that ticks forward instead of a flood of new ones. Always end with ${this.toolNames.complete} or ${this.toolNames.fail}; otherwise the anchor lingers as "working" until the stale-state sweep flips it. Opt-in \u2014 only call this for tasks that will take more than a few seconds.`,
|
|
14640
|
-
inputSchema: {
|
|
14641
|
-
type: "object",
|
|
14642
|
-
properties: {
|
|
14643
|
-
label: {
|
|
14644
|
-
type: "string",
|
|
14645
|
-
description: 'Short top-line label for the task (e.g. "diagnose vigil"). Stays constant for the lifetime of this progress anchor.'
|
|
14646
|
-
},
|
|
14647
|
-
initial_mode: {
|
|
14648
|
-
type: "string",
|
|
14649
|
-
enum: ["thinking", "working", "waiting"],
|
|
14650
|
-
description: 'Initial mode emoji \u2014 defaults to "working".'
|
|
14651
|
-
},
|
|
14652
|
-
...startArgsSchema.properties
|
|
14653
|
-
},
|
|
14654
|
-
required: ["label", ...startArgsSchema.required]
|
|
14655
|
-
}
|
|
14656
|
-
},
|
|
14657
|
-
{
|
|
14658
|
-
name: this.toolNames.update,
|
|
14659
|
-
description: `Update the progress anchor in place. The state machine debounces calls to one ${surfaceDescription} API call ${debounceText}, so spamming this every step is safe \u2014 only the latest pending state will paint. Any subset of fields can be passed; omitted ones retain their previous value.`,
|
|
14660
|
-
inputSchema: {
|
|
14661
|
-
type: "object",
|
|
14662
|
-
properties: {
|
|
14663
|
-
progress_id: { type: "string", description: "The progress_id returned from start." },
|
|
14664
|
-
step_label: { type: "string", description: 'Short label for the current step (e.g. "Querying CloudWatch logs").' },
|
|
14665
|
-
step_current: { type: "number", description: "1-based step counter \u2014 current." },
|
|
14666
|
-
step_total: { type: "number", description: "Total steps if known. Omit for open-ended progress." },
|
|
14667
|
-
mode: { type: "string", enum: ["thinking", "working", "waiting"] },
|
|
14668
|
-
detail: { type: "string", description: "Free-text detail line under the step label." }
|
|
14669
|
-
},
|
|
14670
|
-
required: ["progress_id"]
|
|
14671
|
-
}
|
|
14672
|
-
},
|
|
14673
|
-
{
|
|
14674
|
-
name: this.toolNames.complete,
|
|
14675
|
-
description: `Mark the progress anchor as \u2705 completed. Flushes any pending debounced state before painting the terminal banner \u2014 the operator never sees a stale "Step N" beside the \u2705. After this, the handle is dead; further updates are no-ops.`,
|
|
14676
|
-
inputSchema: {
|
|
14677
|
-
type: "object",
|
|
14678
|
-
properties: {
|
|
14679
|
-
progress_id: { type: "string" },
|
|
14680
|
-
summary: { type: "string", description: "Optional one-line summary of what was achieved." }
|
|
14681
|
-
},
|
|
14682
|
-
required: ["progress_id"]
|
|
14683
|
-
}
|
|
14684
|
-
},
|
|
14685
|
-
{
|
|
14686
|
-
name: this.toolNames.fail,
|
|
14687
|
-
description: `Mark the progress anchor as \u274C failed. Always include a one-sentence reason \u2014 the operator can't tell *why* from emoji alone. Same flush-then-paint behaviour as complete.`,
|
|
14688
|
-
inputSchema: {
|
|
14689
|
-
type: "object",
|
|
14690
|
-
properties: {
|
|
14691
|
-
progress_id: { type: "string" },
|
|
14692
|
-
reason: { type: "string", description: "One-sentence failure reason." }
|
|
14693
|
-
},
|
|
14694
|
-
required: ["progress_id", "reason"]
|
|
14695
|
-
}
|
|
14696
|
-
}
|
|
14697
|
-
];
|
|
14698
|
-
}
|
|
14699
|
-
/**
|
|
14700
|
-
* Dispatch a CallToolRequest. Returns `undefined` if the tool name
|
|
14701
|
-
* isn't one of ours (so the caller can keep walking its dispatch
|
|
14702
|
-
* chain). Returns a tool result otherwise.
|
|
14703
|
-
*/
|
|
14704
|
-
async handle(name, args) {
|
|
14705
|
-
if (name === this.toolNames.start) return this.handleStart(args);
|
|
14706
|
-
if (name === this.toolNames.update) return this.handleUpdate(args);
|
|
14707
|
-
if (name === this.toolNames.complete) return this.handleComplete(args);
|
|
14708
|
-
if (name === this.toolNames.fail) return this.handleFail(args);
|
|
14709
|
-
return void 0;
|
|
14710
|
-
}
|
|
14711
|
-
/** Live count of in-flight handles. Exposed for tests + diagnostics. */
|
|
14712
|
-
size() {
|
|
14713
|
-
return this.handles.size;
|
|
14714
|
-
}
|
|
14715
|
-
async handleStart(args) {
|
|
14716
|
-
const label = typeof args.label === "string" ? args.label : null;
|
|
14717
|
-
if (!label) return errResult(`${this.toolNames.start}: 'label' must be a non-empty string`);
|
|
14718
|
-
if ("initial_mode" in args && !isMode(args.initial_mode)) {
|
|
14719
|
-
return errResult(
|
|
14720
|
-
`${this.toolNames.start}: 'initial_mode' must be one of 'thinking', 'working', or 'waiting'`
|
|
14721
|
-
);
|
|
14722
|
-
}
|
|
14723
|
-
for (const k of this.opts.startArgsSchema.required) {
|
|
14724
|
-
if (typeof args[k] !== "string" || args[k].length === 0) {
|
|
14725
|
-
return errResult(`${this.toolNames.start}: '${k}' must be a non-empty string`);
|
|
14726
|
-
}
|
|
14727
|
-
}
|
|
14728
|
-
const initialMode = isMode(args.initial_mode) ? args.initial_mode : void 0;
|
|
14729
|
-
const progressId = randomUUID();
|
|
14730
|
-
try {
|
|
14731
|
-
const flush = this.opts.createFlush(args);
|
|
14732
|
-
const handle = await startProgressHandle({
|
|
14733
|
-
initialLabel: label,
|
|
14734
|
-
initialMode,
|
|
14735
|
-
flush,
|
|
14736
|
-
debounceMs: this.opts.debounceMs ?? 1e3
|
|
14737
|
-
});
|
|
14738
|
-
this.handles.set(progressId, handle);
|
|
14739
|
-
return okResult(JSON.stringify({ progress_id: progressId }));
|
|
14740
|
-
} catch (err) {
|
|
14741
|
-
return errResult(`${this.toolNames.start} failed: ${err.message ?? String(err)}`);
|
|
14742
|
-
}
|
|
14743
|
-
}
|
|
14744
|
-
async handleUpdate(args) {
|
|
14745
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14746
|
-
if (!id) return errResult(`${this.toolNames.update}: 'progress_id' must be a string`);
|
|
14747
|
-
const handle = this.handles.get(id);
|
|
14748
|
-
if (!handle) return errResult(`${this.toolNames.update}: unknown progress_id (already terminated, or never started)`);
|
|
14749
|
-
if ("mode" in args && !isMode(args.mode)) {
|
|
14750
|
-
return errResult(
|
|
14751
|
-
`${this.toolNames.update}: 'mode' must be one of 'thinking', 'working', or 'waiting'`
|
|
14752
|
-
);
|
|
14753
|
-
}
|
|
14754
|
-
if (typeof args.step_total === "number" && typeof args.step_current !== "number") {
|
|
14755
|
-
return errResult(
|
|
14756
|
-
`${this.toolNames.update}: 'step_total' requires 'step_current'`
|
|
14757
|
-
);
|
|
14758
|
-
}
|
|
14759
|
-
const next = {};
|
|
14760
|
-
if (typeof args.step_label === "string") next.stepLabel = args.step_label;
|
|
14761
|
-
if (typeof args.step_current === "number") {
|
|
14762
|
-
next.stepNumber = {
|
|
14763
|
-
current: args.step_current,
|
|
14764
|
-
...typeof args.step_total === "number" ? { total: args.step_total } : {}
|
|
14765
|
-
};
|
|
14766
|
-
}
|
|
14767
|
-
if (isMode(args.mode)) next.mode = args.mode;
|
|
14768
|
-
if (typeof args.detail === "string") next.detail = args.detail;
|
|
14769
|
-
try {
|
|
14770
|
-
await handle.update(next);
|
|
14771
|
-
return okResult("ok");
|
|
14772
|
-
} catch (err) {
|
|
14773
|
-
return errResult(`${this.toolNames.update} failed: ${err.message ?? String(err)}`);
|
|
14774
|
-
}
|
|
14775
|
-
}
|
|
14776
|
-
async handleComplete(args) {
|
|
14777
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14778
|
-
if (!id) return errResult(`${this.toolNames.complete}: 'progress_id' must be a string`);
|
|
14779
|
-
const handle = this.handles.get(id);
|
|
14780
|
-
if (!handle) return errResult(`${this.toolNames.complete}: unknown progress_id`);
|
|
14781
|
-
const summary = typeof args.summary === "string" ? args.summary : void 0;
|
|
14782
|
-
try {
|
|
14783
|
-
await handle.complete(summary);
|
|
14784
|
-
this.handles.delete(id);
|
|
14785
|
-
return okResult("ok");
|
|
14786
|
-
} catch (err) {
|
|
14787
|
-
return errResult(`${this.toolNames.complete} failed: ${err.message ?? String(err)}`);
|
|
14788
|
-
}
|
|
14789
|
-
}
|
|
14790
|
-
async handleFail(args) {
|
|
14791
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14792
|
-
if (!id) return errResult(`${this.toolNames.fail}: 'progress_id' must be a string`);
|
|
14793
|
-
const reason = typeof args.reason === "string" ? args.reason : null;
|
|
14794
|
-
if (!reason) return errResult(`${this.toolNames.fail}: 'reason' must be a non-empty string`);
|
|
14795
|
-
const handle = this.handles.get(id);
|
|
14796
|
-
if (!handle) return errResult(`${this.toolNames.fail}: unknown progress_id`);
|
|
14797
|
-
try {
|
|
14798
|
-
await handle.fail(reason);
|
|
14799
|
-
this.handles.delete(id);
|
|
14800
|
-
return okResult("ok");
|
|
14801
|
-
} catch (err) {
|
|
14802
|
-
return errResult(`${this.toolNames.fail} failed: ${err.message ?? String(err)}`);
|
|
14803
|
-
}
|
|
14804
|
-
}
|
|
14805
|
-
};
|
|
14806
|
-
function okResult(text) {
|
|
14807
|
-
return { content: [{ type: "text", text }] };
|
|
14808
|
-
}
|
|
14809
|
-
function errResult(text) {
|
|
14810
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
14811
|
-
}
|
|
14812
|
-
function isMode(value) {
|
|
14813
|
-
return value === "thinking" || value === "working" || value === "waiting";
|
|
14814
|
-
}
|
|
14815
|
-
|
|
14816
|
-
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
14817
|
-
import process2 from "process";
|
|
14818
|
-
|
|
14819
|
-
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
14820
|
-
var ReadBuffer = class {
|
|
14821
|
-
append(chunk) {
|
|
14822
|
-
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
14823
|
-
}
|
|
14824
|
-
readMessage() {
|
|
14825
|
-
if (!this._buffer) {
|
|
14826
|
-
return null;
|
|
14962
|
+
readMessage() {
|
|
14963
|
+
if (!this._buffer) {
|
|
14964
|
+
return null;
|
|
14827
14965
|
}
|
|
14828
14966
|
const index = this._buffer.indexOf("\n");
|
|
14829
14967
|
if (index === -1) {
|
|
@@ -14909,22 +15047,22 @@ var StdioServerTransport = class {
|
|
|
14909
15047
|
import {
|
|
14910
15048
|
chmodSync,
|
|
14911
15049
|
createWriteStream,
|
|
14912
|
-
existsSync as
|
|
15050
|
+
existsSync as existsSync3,
|
|
14913
15051
|
mkdirSync as mkdirSync3,
|
|
14914
|
-
readFileSync as
|
|
14915
|
-
readdirSync,
|
|
15052
|
+
readFileSync as readFileSync4,
|
|
15053
|
+
readdirSync as readdirSync3,
|
|
14916
15054
|
renameSync as renameSync2,
|
|
14917
|
-
statSync,
|
|
14918
|
-
unlinkSync as
|
|
15055
|
+
statSync as statSync2,
|
|
15056
|
+
unlinkSync as unlinkSync3,
|
|
14919
15057
|
watch,
|
|
14920
15058
|
writeFileSync as writeFileSync3
|
|
14921
15059
|
} from "fs";
|
|
14922
|
-
import { basename, join as
|
|
15060
|
+
import { basename, join as join5, resolve as resolve2 } from "path";
|
|
14923
15061
|
import { homedir as homedir2 } from "os";
|
|
14924
|
-
import { createHash, randomUUID
|
|
15062
|
+
import { createHash, randomUUID } from "crypto";
|
|
14925
15063
|
|
|
14926
15064
|
// src/slack-thread-store.ts
|
|
14927
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
15065
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
14928
15066
|
import { dirname } from "path";
|
|
14929
15067
|
var FILE_VERSION = 1;
|
|
14930
15068
|
var DEFAULT_TTL_DAYS = 30;
|
|
@@ -14935,7 +15073,7 @@ function loadThreadStore(filePath, opts = {}) {
|
|
|
14935
15073
|
const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
|
|
14936
15074
|
let raw;
|
|
14937
15075
|
try {
|
|
14938
|
-
raw =
|
|
15076
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
14939
15077
|
} catch {
|
|
14940
15078
|
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
14941
15079
|
}
|
|
@@ -15048,88 +15186,6 @@ async function runOrRetry(fn, opts) {
|
|
|
15048
15186
|
}
|
|
15049
15187
|
}
|
|
15050
15188
|
|
|
15051
|
-
// src/channel-attachments.ts
|
|
15052
|
-
import { homedir } from "os";
|
|
15053
|
-
import { join, resolve, sep } from "path";
|
|
15054
|
-
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
15055
|
-
const base = join(homedir(), ".augmented");
|
|
15056
|
-
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
15057
|
-
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
15058
|
-
throw new Error(
|
|
15059
|
-
`Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
|
|
15060
|
-
);
|
|
15061
|
-
}
|
|
15062
|
-
const candidate = resolve(base, codeName, channelSlug);
|
|
15063
|
-
if (!isPathInside(candidate, base)) {
|
|
15064
|
-
throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
|
|
15065
|
-
}
|
|
15066
|
-
return candidate;
|
|
15067
|
-
}
|
|
15068
|
-
function classifyMimetype(mimetype) {
|
|
15069
|
-
if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
|
|
15070
|
-
return "attachment";
|
|
15071
|
-
}
|
|
15072
|
-
function buildSafeInboundPath(root, fileId, mimetype) {
|
|
15073
|
-
const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
15074
|
-
if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
|
|
15075
|
-
const ext = extensionForMimetype(mimetype);
|
|
15076
|
-
const candidate = resolve(root, `${safeId}${ext}`);
|
|
15077
|
-
if (!isPathInside(candidate, root)) {
|
|
15078
|
-
throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
|
|
15079
|
-
}
|
|
15080
|
-
return candidate;
|
|
15081
|
-
}
|
|
15082
|
-
function extensionForMimetype(mimetype) {
|
|
15083
|
-
if (!mimetype) return ".bin";
|
|
15084
|
-
switch (mimetype) {
|
|
15085
|
-
// Images
|
|
15086
|
-
case "image/png":
|
|
15087
|
-
return ".png";
|
|
15088
|
-
case "image/jpeg":
|
|
15089
|
-
case "image/jpg":
|
|
15090
|
-
return ".jpg";
|
|
15091
|
-
case "image/gif":
|
|
15092
|
-
return ".gif";
|
|
15093
|
-
case "image/webp":
|
|
15094
|
-
return ".webp";
|
|
15095
|
-
case "image/svg+xml":
|
|
15096
|
-
return ".svg";
|
|
15097
|
-
// Docs
|
|
15098
|
-
case "application/pdf":
|
|
15099
|
-
return ".pdf";
|
|
15100
|
-
case "text/plain":
|
|
15101
|
-
return ".txt";
|
|
15102
|
-
case "text/csv":
|
|
15103
|
-
return ".csv";
|
|
15104
|
-
case "application/json":
|
|
15105
|
-
return ".json";
|
|
15106
|
-
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
15107
|
-
return ".docx";
|
|
15108
|
-
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
15109
|
-
return ".xlsx";
|
|
15110
|
-
// Audio (Telegram voice notes are typically audio/ogg; regular
|
|
15111
|
-
// audio messages are audio/mpeg)
|
|
15112
|
-
case "audio/ogg":
|
|
15113
|
-
return ".ogg";
|
|
15114
|
-
case "audio/mpeg":
|
|
15115
|
-
return ".mp3";
|
|
15116
|
-
case "audio/mp4":
|
|
15117
|
-
return ".m4a";
|
|
15118
|
-
// Video
|
|
15119
|
-
case "video/mp4":
|
|
15120
|
-
return ".mp4";
|
|
15121
|
-
case "video/quicktime":
|
|
15122
|
-
return ".mov";
|
|
15123
|
-
default:
|
|
15124
|
-
return ".bin";
|
|
15125
|
-
}
|
|
15126
|
-
}
|
|
15127
|
-
function isPathInside(target, root) {
|
|
15128
|
-
const normalizedRoot = resolve(root) + sep;
|
|
15129
|
-
const normalizedTarget = resolve(target);
|
|
15130
|
-
return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
|
|
15131
|
-
}
|
|
15132
|
-
|
|
15133
15189
|
// src/slack-inbound-files.ts
|
|
15134
15190
|
function classifySlackFile(file) {
|
|
15135
15191
|
if (!file || typeof file.id !== "string" || !file.id) return null;
|
|
@@ -15696,14 +15752,14 @@ function createSlackBotUserIdClient(args) {
|
|
|
15696
15752
|
|
|
15697
15753
|
// src/mcp-spawn-lock.ts
|
|
15698
15754
|
import {
|
|
15699
|
-
existsSync,
|
|
15755
|
+
existsSync as existsSync2,
|
|
15700
15756
|
mkdirSync as mkdirSync2,
|
|
15701
|
-
readFileSync as
|
|
15757
|
+
readFileSync as readFileSync3,
|
|
15702
15758
|
renameSync,
|
|
15703
|
-
unlinkSync,
|
|
15759
|
+
unlinkSync as unlinkSync2,
|
|
15704
15760
|
writeFileSync as writeFileSync2
|
|
15705
15761
|
} from "fs";
|
|
15706
|
-
import { join as
|
|
15762
|
+
import { join as join4 } from "path";
|
|
15707
15763
|
function defaultIsPidAlive(pid) {
|
|
15708
15764
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
15709
15765
|
try {
|
|
@@ -15721,7 +15777,7 @@ function acquireMcpSpawnLock(args) {
|
|
|
15721
15777
|
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
15722
15778
|
const selfPid = options.selfPid ?? process.pid;
|
|
15723
15779
|
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
15724
|
-
const path =
|
|
15780
|
+
const path = join4(agentDir, basename2);
|
|
15725
15781
|
const existing = readLockHolder(path);
|
|
15726
15782
|
if (existing) {
|
|
15727
15783
|
if (existing.pid === selfPid) {
|
|
@@ -15745,14 +15801,14 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
|
|
|
15745
15801
|
if (!existing) return;
|
|
15746
15802
|
if (existing.pid !== selfPid) return;
|
|
15747
15803
|
try {
|
|
15748
|
-
|
|
15804
|
+
unlinkSync2(lockPath);
|
|
15749
15805
|
} catch {
|
|
15750
15806
|
}
|
|
15751
15807
|
}
|
|
15752
15808
|
function readLockHolder(path) {
|
|
15753
|
-
if (!
|
|
15809
|
+
if (!existsSync2(path)) return null;
|
|
15754
15810
|
try {
|
|
15755
|
-
const raw =
|
|
15811
|
+
const raw = readFileSync3(path, "utf8");
|
|
15756
15812
|
const parsed = JSON.parse(raw);
|
|
15757
15813
|
const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
|
|
15758
15814
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
@@ -15767,22 +15823,117 @@ function readLockHolder(path) {
|
|
|
15767
15823
|
var BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
15768
15824
|
var APP_TOKEN = process.env.SLACK_APP_TOKEN;
|
|
15769
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
|
+
}
|
|
15770
15836
|
var AGT_HOST = process.env.AGT_HOST ?? null;
|
|
15771
15837
|
var AGT_API_KEY = process.env.AGT_API_KEY ?? null;
|
|
15772
15838
|
var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
|
|
15773
15839
|
var AGT_TEAM_ID = process.env.AGT_TEAM_ID ?? null;
|
|
15774
15840
|
var SLACK_SENDER_POLICY = (() => {
|
|
15775
15841
|
const raw = (process.env.SLACK_SENDER_POLICY ?? "all").trim().toLowerCase();
|
|
15776
|
-
|
|
15777
|
-
|
|
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 };
|
|
15778
15852
|
if (raw === "team_agents_only") {
|
|
15779
15853
|
if (!AGT_TEAM_ID) {
|
|
15780
15854
|
throw new Error("SLACK_SENDER_POLICY=team_agents_only requires AGT_TEAM_ID");
|
|
15781
15855
|
}
|
|
15782
|
-
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 };
|
|
15783
15882
|
}
|
|
15784
15883
|
throw new Error(`Invalid SLACK_SENDER_POLICY=${JSON.stringify(process.env.SLACK_SENDER_POLICY)}`);
|
|
15785
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
|
+
}
|
|
15786
15937
|
var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
|
|
15787
15938
|
var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
|
|
15788
15939
|
var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
|
|
@@ -15834,9 +15985,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
|
|
|
15834
15985
|
peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
|
|
15835
15986
|
peer_disabled_mode: SLACK_PEER_DISABLED_MODE
|
|
15836
15987
|
};
|
|
15837
|
-
var SLACK_AGENT_DIR = AGENT_CODE_NAME ?
|
|
15838
|
-
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ?
|
|
15839
|
-
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;
|
|
15840
15991
|
var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
|
|
15841
15992
|
function redactSlackId(id) {
|
|
15842
15993
|
if (!id) return "<none>";
|
|
@@ -15848,16 +15999,18 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
|
|
|
15848
15999
|
}
|
|
15849
16000
|
function slackPendingInboundPath(channel, threadTs, messageTs) {
|
|
15850
16001
|
if (!SLACK_PENDING_INBOUND_DIR) return null;
|
|
15851
|
-
return
|
|
16002
|
+
return join5(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
|
|
15852
16003
|
}
|
|
15853
|
-
function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
16004
|
+
function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
|
|
15854
16005
|
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
15855
16006
|
if (!path || !SLACK_PENDING_INBOUND_DIR) return;
|
|
15856
16007
|
const marker = {
|
|
15857
16008
|
channel,
|
|
15858
16009
|
thread_ts: threadTs,
|
|
15859
16010
|
message_ts: messageTs,
|
|
15860
|
-
received_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
16011
|
+
received_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16012
|
+
// Only persist the flag when set — keeps healthy-path markers byte-identical.
|
|
16013
|
+
...undeliverable ? { undeliverable: true } : {}
|
|
15861
16014
|
};
|
|
15862
16015
|
try {
|
|
15863
16016
|
mkdirSync3(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
|
|
@@ -15869,21 +16022,95 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
|
15869
16022
|
);
|
|
15870
16023
|
}
|
|
15871
16024
|
}
|
|
15872
|
-
function
|
|
15873
|
-
if (!
|
|
15874
|
-
const
|
|
15875
|
-
|
|
15876
|
-
|
|
16025
|
+
function healSlackUndeliverable(channel, messageTs) {
|
|
16026
|
+
if (!BOT_TOKEN || !channel || !messageTs) return;
|
|
16027
|
+
const headers = {
|
|
16028
|
+
"Content-Type": "application/json",
|
|
16029
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
16030
|
+
};
|
|
16031
|
+
const remove = (name) => fetch("https://slack.com/api/reactions.remove", {
|
|
16032
|
+
method: "POST",
|
|
16033
|
+
headers,
|
|
16034
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name })
|
|
16035
|
+
}).catch(() => {
|
|
16036
|
+
});
|
|
16037
|
+
void Promise.allSettled([
|
|
16038
|
+
remove(SLACK_UNDELIVERABLE_REACTION),
|
|
16039
|
+
remove(SLACK_UNDELIVERABLE_REACTION_LEGACY)
|
|
16040
|
+
]).finally(() => {
|
|
16041
|
+
fetch("https://slack.com/api/reactions.add", {
|
|
16042
|
+
method: "POST",
|
|
16043
|
+
headers,
|
|
16044
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
|
|
16045
|
+
}).catch(() => {
|
|
16046
|
+
});
|
|
16047
|
+
});
|
|
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
|
+
}
|
|
16075
|
+
function clearSlackMarkerFileWithHeal(fullPath) {
|
|
16076
|
+
let marker = null;
|
|
15877
16077
|
try {
|
|
15878
|
-
|
|
15879
|
-
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
15880
|
-
try {
|
|
15881
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
|
|
15882
|
-
} catch {
|
|
15883
|
-
}
|
|
15884
|
-
}
|
|
16078
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15885
16079
|
} catch {
|
|
15886
16080
|
}
|
|
16081
|
+
if (marker && decideRecoveryHeal({
|
|
16082
|
+
wasUndeliverable: marker.undeliverable === true,
|
|
16083
|
+
hasTarget: Boolean(marker.channel && marker.message_ts)
|
|
16084
|
+
}) === "heal") {
|
|
16085
|
+
healSlackUndeliverable(marker.channel, marker.message_ts);
|
|
16086
|
+
}
|
|
16087
|
+
try {
|
|
16088
|
+
if (existsSync3(fullPath)) unlinkSync3(fullPath);
|
|
16089
|
+
} catch {
|
|
16090
|
+
}
|
|
16091
|
+
}
|
|
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
|
+
);
|
|
15887
16114
|
}
|
|
15888
16115
|
function slackNextRetryName(filename) {
|
|
15889
16116
|
const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
|
|
@@ -15899,10 +16126,10 @@ function slackNextRetryName(filename) {
|
|
|
15899
16126
|
async function processSlackRecoveryOutboxFile(filename) {
|
|
15900
16127
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
15901
16128
|
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
|
|
15902
|
-
const fullPath =
|
|
16129
|
+
const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
|
|
15903
16130
|
let payload;
|
|
15904
16131
|
try {
|
|
15905
|
-
payload = JSON.parse(
|
|
16132
|
+
payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15906
16133
|
} catch (err) {
|
|
15907
16134
|
process.stderr.write(
|
|
15908
16135
|
`slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
|
|
@@ -15968,7 +16195,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
15968
16195
|
}
|
|
15969
16196
|
if (sendSucceeded) {
|
|
15970
16197
|
try {
|
|
15971
|
-
|
|
16198
|
+
unlinkSync3(fullPath);
|
|
15972
16199
|
} catch {
|
|
15973
16200
|
}
|
|
15974
16201
|
return;
|
|
@@ -15976,7 +16203,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
15976
16203
|
const next = slackNextRetryName(filename);
|
|
15977
16204
|
if (next) {
|
|
15978
16205
|
try {
|
|
15979
|
-
renameSync2(fullPath,
|
|
16206
|
+
renameSync2(fullPath, join5(SLACK_RECOVERY_OUTBOX_DIR, next.next));
|
|
15980
16207
|
if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
15981
16208
|
process.stderr.write(
|
|
15982
16209
|
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
|
|
@@ -16006,7 +16233,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16006
16233
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
16007
16234
|
let entries;
|
|
16008
16235
|
try {
|
|
16009
|
-
entries =
|
|
16236
|
+
entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
|
|
16010
16237
|
} catch {
|
|
16011
16238
|
return;
|
|
16012
16239
|
}
|
|
@@ -16015,7 +16242,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16015
16242
|
if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
|
|
16016
16243
|
let mtimeMs;
|
|
16017
16244
|
try {
|
|
16018
|
-
mtimeMs =
|
|
16245
|
+
mtimeMs = statSync2(join5(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
|
|
16019
16246
|
} catch {
|
|
16020
16247
|
continue;
|
|
16021
16248
|
}
|
|
@@ -16036,7 +16263,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16036
16263
|
return;
|
|
16037
16264
|
}
|
|
16038
16265
|
try {
|
|
16039
|
-
for (const f of
|
|
16266
|
+
for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
|
|
16040
16267
|
if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
|
|
16041
16268
|
}
|
|
16042
16269
|
} catch {
|
|
@@ -16045,7 +16272,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16045
16272
|
const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
|
|
16046
16273
|
if (event !== "rename" || !filename) return;
|
|
16047
16274
|
if (!isFirstAttemptSlackOutboxFile(filename)) return;
|
|
16048
|
-
if (
|
|
16275
|
+
if (existsSync3(join5(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
|
|
16049
16276
|
void processSlackRecoveryOutboxFile(filename);
|
|
16050
16277
|
}
|
|
16051
16278
|
});
|
|
@@ -16061,15 +16288,15 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16061
16288
|
}
|
|
16062
16289
|
startSlackRecoveryOutboxWatcher();
|
|
16063
16290
|
var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
|
|
16064
|
-
function trackPendingMessage(channel, threadTs, messageTs) {
|
|
16065
|
-
writeSlackPendingInboundMarker(channel, threadTs, messageTs);
|
|
16291
|
+
function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
|
|
16292
|
+
writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
|
|
16066
16293
|
}
|
|
16067
|
-
function
|
|
16294
|
+
function sweepSlackStaleMarkers(thresholdMs) {
|
|
16068
16295
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
16069
|
-
if (!
|
|
16296
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16070
16297
|
let filenames;
|
|
16071
16298
|
try {
|
|
16072
|
-
filenames =
|
|
16299
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16073
16300
|
} catch (err) {
|
|
16074
16301
|
process.stderr.write(
|
|
16075
16302
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
|
|
@@ -16082,35 +16309,26 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16082
16309
|
for (const filename of filenames) {
|
|
16083
16310
|
if (!filename.endsWith(".json")) continue;
|
|
16084
16311
|
if (filename.endsWith(".tmp")) continue;
|
|
16085
|
-
const fullPath =
|
|
16312
|
+
const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
|
|
16086
16313
|
let marker;
|
|
16087
16314
|
try {
|
|
16088
|
-
marker = JSON.parse(
|
|
16315
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
16089
16316
|
} catch (err) {
|
|
16090
16317
|
process.stderr.write(
|
|
16091
16318
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
|
|
16092
16319
|
`
|
|
16093
16320
|
);
|
|
16094
16321
|
try {
|
|
16095
|
-
|
|
16322
|
+
unlinkSync3(fullPath);
|
|
16096
16323
|
} catch {
|
|
16097
16324
|
}
|
|
16098
16325
|
cleared++;
|
|
16099
16326
|
continue;
|
|
16100
16327
|
}
|
|
16101
16328
|
const { channel, thread_ts, message_ts, received_at } = marker;
|
|
16102
|
-
if (!channel || !thread_ts || !message_ts ||
|
|
16329
|
+
if (!channel || !thread_ts || !message_ts || isPendingMarkerStale(received_at, now, thresholdMs)) {
|
|
16103
16330
|
try {
|
|
16104
|
-
|
|
16105
|
-
} catch {
|
|
16106
|
-
}
|
|
16107
|
-
cleared++;
|
|
16108
|
-
continue;
|
|
16109
|
-
}
|
|
16110
|
-
const receivedAtMs = Date.parse(received_at);
|
|
16111
|
-
if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
|
|
16112
|
-
try {
|
|
16113
|
-
unlinkSync2(fullPath);
|
|
16331
|
+
unlinkSync3(fullPath);
|
|
16114
16332
|
} catch {
|
|
16115
16333
|
}
|
|
16116
16334
|
cleared++;
|
|
@@ -16123,9 +16341,167 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16123
16341
|
);
|
|
16124
16342
|
}
|
|
16125
16343
|
}
|
|
16126
|
-
|
|
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
|
+
}
|
|
16127
16503
|
function clearPendingMessage(channel, threadTs) {
|
|
16128
|
-
|
|
16504
|
+
clearAllSlackPendingMarkersForThread2(channel, threadTs);
|
|
16129
16505
|
}
|
|
16130
16506
|
function noteThreadActivity(channel, threadTs) {
|
|
16131
16507
|
if (!channel || !threadTs) return;
|
|
@@ -16135,10 +16511,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16135
16511
|
if (!channel || !messageTs) return;
|
|
16136
16512
|
clearPendingMessage(channel, messageTs);
|
|
16137
16513
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
16138
|
-
if (!
|
|
16514
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16139
16515
|
let filenames;
|
|
16140
16516
|
try {
|
|
16141
|
-
filenames =
|
|
16517
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16142
16518
|
} catch {
|
|
16143
16519
|
return;
|
|
16144
16520
|
}
|
|
@@ -16149,13 +16525,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16149
16525
|
for (const filename of filenames) {
|
|
16150
16526
|
if (!filename.startsWith(channelPrefix)) continue;
|
|
16151
16527
|
if (!filename.endsWith(messageSuffix)) continue;
|
|
16152
|
-
|
|
16153
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16154
|
-
} catch {
|
|
16155
|
-
}
|
|
16528
|
+
clearSlackMarkerFileWithHeal(join5(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16156
16529
|
}
|
|
16157
16530
|
}
|
|
16158
|
-
var RESTART_FLAGS_DIR =
|
|
16531
|
+
var RESTART_FLAGS_DIR = join5(homedir2(), ".augmented", "restart-flags");
|
|
16159
16532
|
function buildAugmentedSlackMetadata() {
|
|
16160
16533
|
if (!AGT_TEAM_ID) return void 0;
|
|
16161
16534
|
return {
|
|
@@ -16205,12 +16578,13 @@ function buildSlackHelpMessage(codeName) {
|
|
|
16205
16578
|
return [
|
|
16206
16579
|
`\u{1F916} *Available commands for \`${codeName}\`*`,
|
|
16207
16580
|
"",
|
|
16208
|
-
"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:",
|
|
16209
16582
|
"\u2022 `/help` \u2014 show this help",
|
|
16210
|
-
|
|
16211
|
-
|
|
16583
|
+
`\u2022 \`${agentSlashCommand("/restart")}\` \u2014 restart this agent`,
|
|
16584
|
+
`\u2022 \`${agentSlashCommand("/agent-status")}\` \u2014 report whether this agent is online + last activity`,
|
|
16212
16585
|
"\u2022 `/kill` \u2014 silence all agents in this thread for 6h (use as a thread reply)",
|
|
16213
|
-
"\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)`
|
|
16214
16588
|
].join("\n");
|
|
16215
16589
|
}
|
|
16216
16590
|
var lastActivityAt = null;
|
|
@@ -16299,12 +16673,202 @@ async function postEphemeralViaResponseUrl(responseUrl, text, logTag) {
|
|
|
16299
16673
|
);
|
|
16300
16674
|
}
|
|
16301
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
|
+
}
|
|
16302
16866
|
async function handleSlashCommandEnvelope(payload) {
|
|
16303
16867
|
const command = payload.command;
|
|
16304
16868
|
const responseUrl = payload.response_url;
|
|
16305
16869
|
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16306
16870
|
if (!command || !responseUrl) return;
|
|
16307
|
-
if (command
|
|
16871
|
+
if (matchesAgentCommand(command, "/agent-status")) {
|
|
16308
16872
|
await postEphemeralViaResponseUrl(responseUrl, buildAgentStatusReply(codeName), codeName);
|
|
16309
16873
|
return;
|
|
16310
16874
|
}
|
|
@@ -16312,7 +16876,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16312
16876
|
await postEphemeralViaResponseUrl(responseUrl, buildSlackHelpMessage(codeName), codeName);
|
|
16313
16877
|
return;
|
|
16314
16878
|
}
|
|
16315
|
-
if (command
|
|
16879
|
+
if (matchesAgentCommand(command, "/restart")) {
|
|
16316
16880
|
if (ALLOWED_USERS.size > 0 && (!payload.user_id || !ALLOWED_USERS.has(payload.user_id))) {
|
|
16317
16881
|
process.stderr.write(
|
|
16318
16882
|
`slack-channel(${codeName}): /restart slash-command denied \u2014 user not in SLACK_ALLOWED_USERS
|
|
@@ -16334,10 +16898,10 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16334
16898
|
return;
|
|
16335
16899
|
}
|
|
16336
16900
|
try {
|
|
16337
|
-
if (!
|
|
16901
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16338
16902
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16339
16903
|
}
|
|
16340
|
-
const flagPath =
|
|
16904
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16341
16905
|
const flag = {
|
|
16342
16906
|
codeName,
|
|
16343
16907
|
source: "slack",
|
|
@@ -16347,7 +16911,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16347
16911
|
...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
|
|
16348
16912
|
}
|
|
16349
16913
|
};
|
|
16350
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
16914
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16351
16915
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16352
16916
|
renameSync2(tmpPath, flagPath);
|
|
16353
16917
|
process.stderr.write(
|
|
@@ -16361,7 +16925,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16361
16925
|
);
|
|
16362
16926
|
} catch (err) {
|
|
16363
16927
|
process.stderr.write(
|
|
16364
|
-
`slack-channel(${codeName}): /restart slash-command flag write failed: ${
|
|
16928
|
+
`slack-channel(${codeName}): /restart slash-command flag write failed: ${redactAugmentedPaths2(err.message)}
|
|
16365
16929
|
`
|
|
16366
16930
|
);
|
|
16367
16931
|
await postEphemeralViaResponseUrl(
|
|
@@ -16372,6 +16936,10 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16372
16936
|
}
|
|
16373
16937
|
return;
|
|
16374
16938
|
}
|
|
16939
|
+
if (command === "/debug" || matchesAgentCommand(command, "/investigate")) {
|
|
16940
|
+
await handleDebugSlashCommand(payload, responseUrl);
|
|
16941
|
+
return;
|
|
16942
|
+
}
|
|
16375
16943
|
if (command === "/kill" || command === "/unkill") {
|
|
16376
16944
|
if (!AGT_HOST || !AGT_API_KEY || !AGT_AGENT_ID) {
|
|
16377
16945
|
await postEphemeralViaResponseUrl(
|
|
@@ -16441,10 +17009,10 @@ async function handleHelpCommand(opts) {
|
|
|
16441
17009
|
async function handleRestartCommand(opts) {
|
|
16442
17010
|
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16443
17011
|
try {
|
|
16444
|
-
if (!
|
|
17012
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16445
17013
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16446
17014
|
}
|
|
16447
|
-
const flagPath =
|
|
17015
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16448
17016
|
const flag = {
|
|
16449
17017
|
codeName,
|
|
16450
17018
|
source: "slack",
|
|
@@ -16455,7 +17023,7 @@ async function handleRestartCommand(opts) {
|
|
|
16455
17023
|
message_ts: opts.ts
|
|
16456
17024
|
}
|
|
16457
17025
|
};
|
|
16458
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
17026
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16459
17027
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16460
17028
|
renameSync2(tmpPath, flagPath);
|
|
16461
17029
|
process.stderr.write(
|
|
@@ -16475,7 +17043,7 @@ async function handleRestartCommand(opts) {
|
|
|
16475
17043
|
}
|
|
16476
17044
|
} catch (err) {
|
|
16477
17045
|
process.stderr.write(
|
|
16478
|
-
`slack-channel(${codeName}): /restart flag write failed: ${
|
|
17046
|
+
`slack-channel(${codeName}): /restart flag write failed: ${redactAugmentedPaths2(err.message)}
|
|
16479
17047
|
`
|
|
16480
17048
|
);
|
|
16481
17049
|
await postSlackMessage({
|
|
@@ -16503,7 +17071,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
|
|
|
16503
17071
|
var threadPersister = null;
|
|
16504
17072
|
function resolveThreadStorePath() {
|
|
16505
17073
|
if (!AGENT_CODE_NAME) return null;
|
|
16506
|
-
return
|
|
17074
|
+
return join5(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
|
|
16507
17075
|
}
|
|
16508
17076
|
function parseTtlDays(raw) {
|
|
16509
17077
|
if (!raw) return void 0;
|
|
@@ -16538,9 +17106,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
|
|
|
16538
17106
|
var slackStderrLogStream = null;
|
|
16539
17107
|
if (AGENT_CODE_NAME) {
|
|
16540
17108
|
try {
|
|
16541
|
-
const logDir =
|
|
17109
|
+
const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
|
|
16542
17110
|
mkdirSync3(logDir, { recursive: true });
|
|
16543
|
-
slackStderrLogStream = createWriteStream(
|
|
17111
|
+
slackStderrLogStream = createWriteStream(join5(logDir, "slack-channel-stderr.log"), {
|
|
16544
17112
|
flags: "a",
|
|
16545
17113
|
mode: 384
|
|
16546
17114
|
});
|
|
@@ -16610,7 +17178,6 @@ void resolveBotUserIdOrThrow().then((id) => {
|
|
|
16610
17178
|
);
|
|
16611
17179
|
if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
|
|
16612
17180
|
});
|
|
16613
|
-
var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
|
|
16614
17181
|
var mcp = new Server(
|
|
16615
17182
|
{ name: "slack", version: "0.1.0" },
|
|
16616
17183
|
{
|
|
@@ -16625,43 +17192,18 @@ var mcp = new Server(
|
|
|
16625
17192
|
// Highest-priority lines first — Claude Code truncates this string at
|
|
16626
17193
|
// 2048 chars, so anything appended late silently disappears.
|
|
16627
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.",
|
|
16628
|
-
|
|
16629
|
-
"Long task (3+ tool calls or >5s)? slack_progress_start (channel + thread_ts), slack_progress_update between steps, slack_progress_complete/_fail to close. One anchor edits in place; avoids thread spam. Not for one-shots.",
|
|
16630
|
-
selfIdentityInstruction,
|
|
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.`,
|
|
16631
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.",
|
|
16632
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.",
|
|
16633
|
-
'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if
|
|
16634
|
-
"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 \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).",
|
|
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).',
|
|
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.)",
|
|
16635
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.`,
|
|
16636
17201
|
"To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
|
|
16637
17202
|
].join(" ")
|
|
16638
17203
|
}
|
|
16639
17204
|
);
|
|
16640
|
-
var progressRegistry = new ProgressToolRegistry({
|
|
16641
|
-
prefix: "slack",
|
|
16642
|
-
surfaceDescription: "a Slack thread",
|
|
16643
|
-
startArgsSchema: {
|
|
16644
|
-
properties: {
|
|
16645
|
-
channel: {
|
|
16646
|
-
type: "string",
|
|
16647
|
-
description: "Slack channel id (from the `channel` attribute on the inbound <channel> tag)."
|
|
16648
|
-
},
|
|
16649
|
-
thread_ts: {
|
|
16650
|
-
type: "string",
|
|
16651
|
-
description: "Thread `ts` so the anchor lands in the same thread as the request (from the `thread_ts` attribute on the inbound <channel> tag). Omit to post at channel-root."
|
|
16652
|
-
}
|
|
16653
|
-
},
|
|
16654
|
-
required: ["channel"]
|
|
16655
|
-
},
|
|
16656
|
-
createFlush: (args) => createSlackProgressFlush({
|
|
16657
|
-
botToken: BOT_TOKEN,
|
|
16658
|
-
channel: args.channel,
|
|
16659
|
-
threadTs: typeof args.thread_ts === "string" ? args.thread_ts : void 0
|
|
16660
|
-
})
|
|
16661
|
-
});
|
|
16662
17205
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16663
17206
|
tools: [
|
|
16664
|
-
...progressRegistry.getDefinitions(),
|
|
16665
17207
|
{
|
|
16666
17208
|
name: "slack.reply",
|
|
16667
17209
|
description: "Send a message back to a Slack channel or thread",
|
|
@@ -16675,7 +17217,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16675
17217
|
text: { type: "string", description: "The message to send" },
|
|
16676
17218
|
thread_ts: {
|
|
16677
17219
|
type: "string",
|
|
16678
|
-
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."
|
|
16679
17232
|
}
|
|
16680
17233
|
},
|
|
16681
17234
|
required: ["channel", "text"]
|
|
@@ -16683,7 +17236,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16683
17236
|
},
|
|
16684
17237
|
{
|
|
16685
17238
|
name: "slack.react",
|
|
16686
|
-
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 \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.",
|
|
16687
17240
|
inputSchema: {
|
|
16688
17241
|
type: "object",
|
|
16689
17242
|
properties: {
|
|
@@ -16696,7 +17249,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16696
17249
|
},
|
|
16697
17250
|
{
|
|
16698
17251
|
name: "slack.read_thread",
|
|
16699
|
-
description: "Read the full message history of a Slack thread.
|
|
17252
|
+
description: "Read the full message history of a Slack thread. Inbound thread replies already carry the surrounding thread inline as the <channel> tag's thread_context attribute \u2014 ground your reply in that first, and only call this tool when you need MORE history than thread_context shows (it is capped to recent messages) or to catch up on an auto-followed thread.",
|
|
16700
17253
|
inputSchema: {
|
|
16701
17254
|
type: "object",
|
|
16702
17255
|
properties: {
|
|
@@ -16741,7 +17294,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16741
17294
|
...blockKitToolsAvailable() ? [
|
|
16742
17295
|
{
|
|
16743
17296
|
name: "slack.send_structured",
|
|
16744
|
-
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.",
|
|
16745
17298
|
inputSchema: {
|
|
16746
17299
|
type: "object",
|
|
16747
17300
|
properties: {
|
|
@@ -16751,18 +17304,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16751
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."
|
|
16752
17305
|
},
|
|
16753
17306
|
text: { type: "string", description: "Plain-text fallback for push notifications and unfurls. Required." },
|
|
16754
|
-
thread_ts: { type: "string", description: "Thread timestamp for threaded replies (optional)" }
|
|
16755
|
-
interactive: {
|
|
16756
|
-
type: "object",
|
|
16757
|
-
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.`,
|
|
16758
|
-
properties: {
|
|
16759
|
-
type: { type: "string", enum: ["rate_run"], description: 'Affordance kind. Only "rate_run" is supported today.' },
|
|
16760
|
-
run_id: { type: "string", description: "The runs.run_id this rating belongs to (returned by /host/runs/start)." },
|
|
16761
|
-
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.' },
|
|
16762
|
-
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." }
|
|
16763
|
-
},
|
|
16764
|
-
required: ["type", "run_id"]
|
|
16765
|
-
}
|
|
17307
|
+
thread_ts: { type: "string", description: "Thread timestamp for threaded replies (optional)" }
|
|
16766
17308
|
},
|
|
16767
17309
|
required: ["channel", "blocks", "text"]
|
|
16768
17310
|
}
|
|
@@ -16909,10 +17451,11 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16909
17451
|
}));
|
|
16910
17452
|
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
16911
17453
|
const { name, arguments: args } = req.params;
|
|
16912
|
-
|
|
16913
|
-
|
|
17454
|
+
if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
|
|
17455
|
+
return buildImpersonationRefusal(name);
|
|
17456
|
+
}
|
|
16914
17457
|
if (name === "slack.reply") {
|
|
16915
|
-
const { channel, text, thread_ts } = args;
|
|
17458
|
+
const { channel, text, thread_ts, message_ts } = args;
|
|
16916
17459
|
if (channel && thread_ts) {
|
|
16917
17460
|
const killed = await isThreadKilled({
|
|
16918
17461
|
channelType: "slack",
|
|
@@ -16971,8 +17514,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
16971
17514
|
isError: true
|
|
16972
17515
|
};
|
|
16973
17516
|
}
|
|
16974
|
-
if (channel
|
|
16975
|
-
|
|
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
|
+
}
|
|
16976
17526
|
}
|
|
16977
17527
|
try {
|
|
16978
17528
|
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
@@ -17067,46 +17617,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
17067
17617
|
const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
|
|
17068
17618
|
noteThreadActivity(channel, thread_ts);
|
|
17069
17619
|
try {
|
|
17070
|
-
const
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
|
|
17080
|
-
});
|
|
17081
|
-
const res = await fetch(`https://slack.com/api/conversations.replies?${params}`, {
|
|
17082
|
-
headers: { Authorization: `Bearer ${BOT_TOKEN}` }
|
|
17083
|
-
});
|
|
17084
|
-
const data = await res.json();
|
|
17085
|
-
if (!data.ok) {
|
|
17086
|
-
return {
|
|
17087
|
-
content: [{ type: "text", text: `Slack error: ${data.error}` }],
|
|
17088
|
-
isError: true
|
|
17089
|
-
};
|
|
17090
|
-
}
|
|
17091
|
-
for (const msg of data.messages ?? []) {
|
|
17092
|
-
allMessages.push({
|
|
17093
|
-
user: msg.user ?? msg.bot_id ?? "unknown",
|
|
17094
|
-
text: msg.text ?? "",
|
|
17095
|
-
ts: msg.ts ?? ""
|
|
17096
|
-
});
|
|
17097
|
-
}
|
|
17098
|
-
cursor = data.response_metadata?.next_cursor || void 0;
|
|
17099
|
-
} while (cursor && allMessages.length < limit);
|
|
17100
|
-
const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
|
|
17101
|
-
const resolved = await Promise.all(uniqueUserIds.map(async (id) => [id, await resolveUserName(id)]));
|
|
17102
|
-
const nameById = new Map(resolved);
|
|
17103
|
-
const formatted = allMessages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
|
|
17620
|
+
const result = await fetchThreadTranscript(channel, thread_ts, limit, {
|
|
17621
|
+
botToken: BOT_TOKEN,
|
|
17622
|
+
resolveUserName
|
|
17623
|
+
});
|
|
17624
|
+
if (!result.ok) {
|
|
17625
|
+
return {
|
|
17626
|
+
content: [{ type: "text", text: `Slack error: ${result.error}` }],
|
|
17627
|
+
isError: true
|
|
17628
|
+
};
|
|
17629
|
+
}
|
|
17104
17630
|
return {
|
|
17105
17631
|
content: [{
|
|
17106
17632
|
type: "text",
|
|
17107
|
-
text:
|
|
17633
|
+
text: result.count > 0 ? `Thread (${result.count} messages):
|
|
17108
17634
|
|
|
17109
|
-
${formatted}` : "Thread is empty or not found."
|
|
17635
|
+
${result.formatted}` : "Thread is empty or not found."
|
|
17110
17636
|
}]
|
|
17111
17637
|
};
|
|
17112
17638
|
} catch (err) {
|
|
@@ -17148,15 +17674,15 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
17148
17674
|
let bytes;
|
|
17149
17675
|
let size;
|
|
17150
17676
|
try {
|
|
17151
|
-
const
|
|
17152
|
-
if (!
|
|
17677
|
+
const stat2 = statSync2(resolvedPath);
|
|
17678
|
+
if (!stat2.isFile()) {
|
|
17153
17679
|
return {
|
|
17154
17680
|
content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
|
|
17155
17681
|
isError: true
|
|
17156
17682
|
};
|
|
17157
17683
|
}
|
|
17158
|
-
size =
|
|
17159
|
-
bytes =
|
|
17684
|
+
size = stat2.size;
|
|
17685
|
+
bytes = readFileSync4(resolvedPath);
|
|
17160
17686
|
} catch (err) {
|
|
17161
17687
|
return {
|
|
17162
17688
|
content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
|
|
@@ -17266,7 +17792,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
17266
17792
|
return {
|
|
17267
17793
|
content: [{
|
|
17268
17794
|
type: "text",
|
|
17269
|
-
text: `Failed to download attachment: ${
|
|
17795
|
+
text: `Failed to download attachment: ${redactAugmentedPaths2(err.message)}`
|
|
17270
17796
|
}],
|
|
17271
17797
|
isError: true
|
|
17272
17798
|
};
|
|
@@ -17453,109 +17979,28 @@ async function handleSendStructured(args) {
|
|
|
17453
17979
|
}
|
|
17454
17980
|
const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
|
|
17455
17981
|
const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
|
|
17456
|
-
const { channel, blocks, text, thread_ts
|
|
17982
|
+
const { channel, blocks, text, thread_ts } = args;
|
|
17457
17983
|
if (typeof channel !== "string" || !channel) {
|
|
17458
|
-
return
|
|
17984
|
+
return errResult("channel is required");
|
|
17459
17985
|
}
|
|
17460
17986
|
if (typeof text !== "string" || !text) {
|
|
17461
|
-
return
|
|
17987
|
+
return errResult("text is required (used as fallback for push notifications and unfurls)");
|
|
17462
17988
|
}
|
|
17463
17989
|
noteThreadActivity(channel, thread_ts);
|
|
17464
17990
|
const validation = validateSlackBlocks2(blocks);
|
|
17465
17991
|
if (!validation.ok) {
|
|
17466
|
-
return
|
|
17467
|
-
}
|
|
17468
|
-
let effectiveBlocks = blocks;
|
|
17469
|
-
let ratingCallbackId = null;
|
|
17470
|
-
let ratingExpiresAt = null;
|
|
17471
|
-
let ratingTokenised = [];
|
|
17472
|
-
if (interactive) {
|
|
17473
|
-
if (interactive.type !== "rate_run") {
|
|
17474
|
-
return errResult2(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
|
|
17475
|
-
}
|
|
17476
|
-
if (typeof interactive.run_id !== "string" || !interactive.run_id) {
|
|
17477
|
-
return errResult2("interactive.run_id is required when interactive is set");
|
|
17478
|
-
}
|
|
17479
|
-
if (!interactiveHostAvailable()) {
|
|
17480
|
-
return errResult2(
|
|
17481
|
-
"interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
|
|
17482
|
-
);
|
|
17483
|
-
}
|
|
17484
|
-
const scale = interactive.scale === "sentiment" ? "sentiment" : "1-5";
|
|
17485
|
-
const requestedTtl = typeof interactive.expires_in_seconds === "number" ? interactive.expires_in_seconds : 86400;
|
|
17486
|
-
const ttl = Math.max(60, Math.min(7 * 86400, requestedTtl));
|
|
17487
|
-
const { randomUUID: rndUUID } = await import("crypto");
|
|
17488
|
-
ratingCallbackId = rndUUID();
|
|
17489
|
-
ratingExpiresAt = new Date(Date.now() + ttl * 1e3);
|
|
17490
|
-
const values = runtime.ratingValuesForScale(scale);
|
|
17491
|
-
const labels = runtime.defaultRatingLabels(scale);
|
|
17492
|
-
ratingTokenised = values.map((v, i) => ({
|
|
17493
|
-
token: runtime.generateOptionToken(),
|
|
17494
|
-
value: String(v),
|
|
17495
|
-
label: labels[i] ?? String(v)
|
|
17496
|
-
}));
|
|
17497
|
-
const ratingBlock = runtime.buildRatingActionsBlock({
|
|
17498
|
-
callbackId: ratingCallbackId,
|
|
17499
|
-
options: ratingTokenised.map(({ label, token }) => ({
|
|
17500
|
-
// The numeric value is carried by token → options mapping in the
|
|
17501
|
-
// pending_interaction row, not by the button itself, so this
|
|
17502
|
-
// cast-back to RatingOption only needs label + token + a placeholder.
|
|
17503
|
-
value: 0,
|
|
17504
|
-
label,
|
|
17505
|
-
token
|
|
17506
|
-
}))
|
|
17507
|
-
});
|
|
17508
|
-
effectiveBlocks = [...blocks, ratingBlock];
|
|
17509
|
-
const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
|
|
17510
|
-
if (!finalValidation.ok) {
|
|
17511
|
-
return errResult2(
|
|
17512
|
-
`Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17513
|
-
);
|
|
17514
|
-
}
|
|
17515
|
-
const cfg = {
|
|
17516
|
-
apiHost: AGT_HOST,
|
|
17517
|
-
apiKey: AGT_API_KEY,
|
|
17518
|
-
agentId: AGT_AGENT_ID
|
|
17519
|
-
};
|
|
17520
|
-
try {
|
|
17521
|
-
await runtime.createPendingInteraction(cfg, {
|
|
17522
|
-
callbackId: ratingCallbackId,
|
|
17523
|
-
channelId: channel,
|
|
17524
|
-
messageTs: "",
|
|
17525
|
-
threadTs: thread_ts,
|
|
17526
|
-
options: ratingTokenised,
|
|
17527
|
-
expiresAt: ratingExpiresAt,
|
|
17528
|
-
kind: "rate_run",
|
|
17529
|
-
kindData: { run_id: interactive.run_id, scale }
|
|
17530
|
-
});
|
|
17531
|
-
} catch (err) {
|
|
17532
|
-
return errResult2(`Failed to register rating row: ${err.message}`);
|
|
17533
|
-
}
|
|
17992
|
+
return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17534
17993
|
}
|
|
17535
17994
|
const result = await postSlackMessageWithTs({
|
|
17536
17995
|
channel,
|
|
17537
|
-
blocks
|
|
17996
|
+
blocks,
|
|
17538
17997
|
text,
|
|
17539
17998
|
...thread_ts ? { thread_ts } : {}
|
|
17540
17999
|
});
|
|
17541
18000
|
if (!result.ok || !result.ts) {
|
|
17542
|
-
return
|
|
18001
|
+
return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
|
|
17543
18002
|
}
|
|
17544
|
-
if (
|
|
17545
|
-
try {
|
|
17546
|
-
await runtime.updatePendingInteractionMessageTs(
|
|
17547
|
-
{ apiHost: AGT_HOST, apiKey: AGT_API_KEY, agentId: AGT_AGENT_ID },
|
|
17548
|
-
ratingCallbackId,
|
|
17549
|
-
result.ts
|
|
17550
|
-
);
|
|
17551
|
-
} catch (err) {
|
|
17552
|
-
process.stderr.write(
|
|
17553
|
-
`slack-channel(${AGENT_CODE_NAME}): updatePendingInteractionMessageTs failed for rating ${hashId(ratingCallbackId)}: ${err.message}
|
|
17554
|
-
`
|
|
17555
|
-
);
|
|
17556
|
-
}
|
|
17557
|
-
}
|
|
17558
|
-
if (interactiveHostAvailable() && !ratingCallbackId) {
|
|
18003
|
+
if (interactiveHostAvailable()) {
|
|
17559
18004
|
const cfg = {
|
|
17560
18005
|
apiHost: AGT_HOST,
|
|
17561
18006
|
apiKey: AGT_API_KEY,
|
|
@@ -17585,8 +18030,7 @@ async function handleSendStructured(args) {
|
|
|
17585
18030
|
type: "text",
|
|
17586
18031
|
text: JSON.stringify({
|
|
17587
18032
|
message_ts: result.ts,
|
|
17588
|
-
permalink: permalink ?? ""
|
|
17589
|
-
...ratingCallbackId ? { rating_callback_id: ratingCallbackId } : {}
|
|
18033
|
+
permalink: permalink ?? ""
|
|
17590
18034
|
})
|
|
17591
18035
|
}
|
|
17592
18036
|
]
|
|
@@ -17594,17 +18038,17 @@ async function handleSendStructured(args) {
|
|
|
17594
18038
|
}
|
|
17595
18039
|
async function handleAskUser(args) {
|
|
17596
18040
|
if (!askUserToolAvailable()) {
|
|
17597
|
-
return
|
|
18041
|
+
return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
|
|
17598
18042
|
}
|
|
17599
18043
|
const { randomUUID: rndUUID } = await import("crypto");
|
|
17600
18044
|
const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
|
|
17601
18045
|
const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
|
|
17602
18046
|
const { channel, question, options, timeout_seconds, thread_ts } = args;
|
|
17603
|
-
if (typeof channel !== "string" || !channel) return
|
|
17604
|
-
if (typeof question !== "string" || !question) return
|
|
18047
|
+
if (typeof channel !== "string" || !channel) return errResult("channel is required");
|
|
18048
|
+
if (typeof question !== "string" || !question) return errResult("question is required");
|
|
17605
18049
|
const optsValidation = validateAskUserOptions2(options);
|
|
17606
18050
|
if (!optsValidation.ok) {
|
|
17607
|
-
return
|
|
18051
|
+
return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17608
18052
|
}
|
|
17609
18053
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17610
18054
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17637,7 +18081,7 @@ async function handleAskUser(args) {
|
|
|
17637
18081
|
expiresAt
|
|
17638
18082
|
});
|
|
17639
18083
|
} catch (err) {
|
|
17640
|
-
return
|
|
18084
|
+
return errResult(`Failed to register pending interaction: ${err.message}`);
|
|
17641
18085
|
}
|
|
17642
18086
|
const slackResult = await postSlackMessageWithTs({
|
|
17643
18087
|
channel,
|
|
@@ -17647,7 +18091,7 @@ async function handleAskUser(args) {
|
|
|
17647
18091
|
...thread_ts ? { thread_ts } : {}
|
|
17648
18092
|
});
|
|
17649
18093
|
if (!slackResult.ok || !slackResult.ts) {
|
|
17650
|
-
return
|
|
18094
|
+
return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
|
|
17651
18095
|
}
|
|
17652
18096
|
try {
|
|
17653
18097
|
await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
|
|
@@ -17694,7 +18138,7 @@ async function handleAskUser(args) {
|
|
|
17694
18138
|
}
|
|
17695
18139
|
async function handleChannelRequestInput(args) {
|
|
17696
18140
|
if (!askUserToolAvailable()) {
|
|
17697
|
-
return
|
|
18141
|
+
return errResult(
|
|
17698
18142
|
"channel_request_input is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring."
|
|
17699
18143
|
);
|
|
17700
18144
|
}
|
|
@@ -17702,10 +18146,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17702
18146
|
const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
|
|
17703
18147
|
const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
|
|
17704
18148
|
if (typeof thread_id !== "string" || !thread_id) {
|
|
17705
|
-
return
|
|
18149
|
+
return errResult("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
|
|
17706
18150
|
}
|
|
17707
18151
|
if (typeof prompt_text !== "string" || !prompt_text) {
|
|
17708
|
-
return
|
|
18152
|
+
return errResult("prompt_text is required");
|
|
17709
18153
|
}
|
|
17710
18154
|
const optsValidation = validateAskUserOptions2(options, {
|
|
17711
18155
|
maxOptions: 4,
|
|
@@ -17713,25 +18157,25 @@ async function handleChannelRequestInput(args) {
|
|
|
17713
18157
|
maxValueChars: 2e3
|
|
17714
18158
|
});
|
|
17715
18159
|
if (!optsValidation.ok) {
|
|
17716
|
-
return
|
|
18160
|
+
return errResult(
|
|
17717
18161
|
`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17718
18162
|
);
|
|
17719
18163
|
}
|
|
17720
18164
|
const optionsArr = options;
|
|
17721
18165
|
if (optionsArr.length < 2) {
|
|
17722
|
-
return
|
|
18166
|
+
return errResult("options must contain at least 2 entries (this is a picker, not a notification)");
|
|
17723
18167
|
}
|
|
17724
18168
|
if (!schema || typeof schema.type !== "string") {
|
|
17725
|
-
return
|
|
18169
|
+
return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
|
|
17726
18170
|
}
|
|
17727
18171
|
if (schema.type !== "yes_no" && schema.type !== "choice") {
|
|
17728
|
-
return
|
|
18172
|
+
return errResult("schema.type must be 'yes_no' or 'choice'");
|
|
17729
18173
|
}
|
|
17730
18174
|
if (schema.type === "yes_no" && optionsArr.length !== 2) {
|
|
17731
|
-
return
|
|
18175
|
+
return errResult("schema.type='yes_no' requires exactly 2 options");
|
|
17732
18176
|
}
|
|
17733
18177
|
if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
|
|
17734
|
-
return
|
|
18178
|
+
return errResult("request_id must be a non-empty string when provided");
|
|
17735
18179
|
}
|
|
17736
18180
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17737
18181
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17753,10 +18197,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17753
18197
|
dispatchPayload = await res.json().catch(() => ({}));
|
|
17754
18198
|
if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
|
|
17755
18199
|
const reason = dispatchPayload.reason ?? `http_${res.status}`;
|
|
17756
|
-
return
|
|
18200
|
+
return errResult(`channel_request_input dispatch failed: ${reason}`);
|
|
17757
18201
|
}
|
|
17758
18202
|
} catch (err) {
|
|
17759
|
-
return
|
|
18203
|
+
return errResult(
|
|
17760
18204
|
`channel_request_input dispatch failed: ${err.message}`
|
|
17761
18205
|
);
|
|
17762
18206
|
}
|
|
@@ -17794,7 +18238,7 @@ async function handleChannelRequestInput(args) {
|
|
|
17794
18238
|
]
|
|
17795
18239
|
};
|
|
17796
18240
|
}
|
|
17797
|
-
function
|
|
18241
|
+
function errResult(text) {
|
|
17798
18242
|
return { content: [{ type: "text", text }], isError: true };
|
|
17799
18243
|
}
|
|
17800
18244
|
var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -17823,7 +18267,7 @@ function isDownloadableFileId(fileId, channel) {
|
|
|
17823
18267
|
}
|
|
17824
18268
|
return entry.channel === channel;
|
|
17825
18269
|
}
|
|
17826
|
-
function
|
|
18270
|
+
function redactAugmentedPaths2(msg) {
|
|
17827
18271
|
return msg.replaceAll(
|
|
17828
18272
|
new RegExp(`${homedir2().replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}/\\.augmented/[^\\s'"\`]*`, "g"),
|
|
17829
18273
|
"<augmented-path>"
|
|
@@ -17905,7 +18349,7 @@ async function buildInboundFileMeta(rawFiles, codeName, channel) {
|
|
|
17905
18349
|
});
|
|
17906
18350
|
} catch (err) {
|
|
17907
18351
|
process.stderr.write(
|
|
17908
|
-
`slack-channel: image auto-download failed for ${classified.id}: ${
|
|
18352
|
+
`slack-channel: image auto-download failed for ${classified.id}: ${redactAugmentedPaths2(err.message)}
|
|
17909
18353
|
`
|
|
17910
18354
|
);
|
|
17911
18355
|
registerDownloadableFileId(classified.id, channel);
|
|
@@ -17988,6 +18432,7 @@ async function connectSocketMode() {
|
|
|
17988
18432
|
process.stderr.write("slack-channel: Socket Mode connected\n");
|
|
17989
18433
|
recordActivity("connect");
|
|
17990
18434
|
void setBotStatus(":large_green_circle:", "Active");
|
|
18435
|
+
void notifyStrandedInboundsOnFirstConnect();
|
|
17991
18436
|
};
|
|
17992
18437
|
ws.onmessage = async (event) => {
|
|
17993
18438
|
try {
|
|
@@ -18049,8 +18494,27 @@ async function connectSocketMode() {
|
|
|
18049
18494
|
}
|
|
18050
18495
|
const senderPolicyDecision = decideSenderPolicyForward(evt, SLACK_SENDER_POLICY);
|
|
18051
18496
|
if (!senderPolicyDecision.forward) {
|
|
18052
|
-
|
|
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"})
|
|
18053
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
|
+
});
|
|
18054
18518
|
return;
|
|
18055
18519
|
}
|
|
18056
18520
|
recordActivity("inbound");
|
|
@@ -18060,7 +18524,8 @@ async function connectSocketMode() {
|
|
|
18060
18524
|
const threadKey = buildThreadKey(evt.channel, trackTs);
|
|
18061
18525
|
const rawText = evt.text ?? "";
|
|
18062
18526
|
const strippedText = rawText.replace(/^\s*<@[^>]+>\s*/, "").trim();
|
|
18063
|
-
const
|
|
18527
|
+
const restartSuffixed = agentSlashCommand("/restart");
|
|
18528
|
+
const isRestartCommand = strippedText === "/restart" || strippedText.startsWith("/restart ") || strippedText === restartSuffixed || strippedText.startsWith(`${restartSuffixed} `);
|
|
18064
18529
|
const isHelpCommand = strippedText === "/help" || strippedText.startsWith("/help ");
|
|
18065
18530
|
if (isHelpCommand) {
|
|
18066
18531
|
await handleHelpCommand({
|
|
@@ -18176,19 +18641,41 @@ async function connectSocketMode() {
|
|
|
18176
18641
|
isAutoFollowed,
|
|
18177
18642
|
botUserId
|
|
18178
18643
|
});
|
|
18179
|
-
|
|
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
|
+
}
|
|
18653
|
+
const ackDecision = decideAckReaction({
|
|
18654
|
+
hasTarget: decideSlackAckReaction({ channel, ts }),
|
|
18655
|
+
integrationReady: Boolean(BOT_TOKEN),
|
|
18656
|
+
tmux: ackProbe.tmux,
|
|
18657
|
+
claude: ackProbe.claude,
|
|
18658
|
+
withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
|
|
18659
|
+
oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR),
|
|
18660
|
+
paneLogFreshAgeMs
|
|
18661
|
+
});
|
|
18662
|
+
if (ackDecision !== "none") {
|
|
18663
|
+
const reactionName = ackDecision === "undeliverable" ? SLACK_UNDELIVERABLE_REACTION : "eyes";
|
|
18180
18664
|
fetch("https://slack.com/api/reactions.add", {
|
|
18181
18665
|
method: "POST",
|
|
18182
18666
|
headers: {
|
|
18183
18667
|
"Content-Type": "application/json",
|
|
18184
18668
|
Authorization: `Bearer ${BOT_TOKEN}`
|
|
18185
18669
|
},
|
|
18186
|
-
body: JSON.stringify({ channel, timestamp: ts, name:
|
|
18670
|
+
body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
|
|
18187
18671
|
}).catch(() => {
|
|
18188
18672
|
});
|
|
18673
|
+
if (ackDecision === "undeliverable" && channel && shouldEngage) {
|
|
18674
|
+
postUndeliverableNotice(channel, threadTs, isThreadReply);
|
|
18675
|
+
}
|
|
18189
18676
|
}
|
|
18190
18677
|
if (channel && ts && shouldEngage) {
|
|
18191
|
-
trackPendingMessage(channel, threadTs, ts);
|
|
18678
|
+
trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
|
|
18192
18679
|
}
|
|
18193
18680
|
const userName = await resolveUserName(user);
|
|
18194
18681
|
const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
|
|
@@ -18196,6 +18683,29 @@ async function connectSocketMode() {
|
|
|
18196
18683
|
(f) => f.kind === "image" && typeof f.path === "string"
|
|
18197
18684
|
);
|
|
18198
18685
|
const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
|
|
18686
|
+
let threadContext;
|
|
18687
|
+
if (isThreadReply && channel && threadTs) {
|
|
18688
|
+
try {
|
|
18689
|
+
const transcript = await fetchThreadTranscript(
|
|
18690
|
+
channel,
|
|
18691
|
+
threadTs,
|
|
18692
|
+
SLACK_AUTOLOAD_THREAD_LIMIT,
|
|
18693
|
+
{ botToken: BOT_TOKEN, resolveUserName }
|
|
18694
|
+
);
|
|
18695
|
+
if (transcript.ok && transcript.count >= 2) {
|
|
18696
|
+
threadContext = capThreadContext(
|
|
18697
|
+
transcript.formatted,
|
|
18698
|
+
SLACK_AUTOLOAD_THREAD_MAX_CHARS
|
|
18699
|
+
);
|
|
18700
|
+
}
|
|
18701
|
+
} catch (err) {
|
|
18702
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
18703
|
+
process.stderr.write(
|
|
18704
|
+
`slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
|
|
18705
|
+
`
|
|
18706
|
+
);
|
|
18707
|
+
}
|
|
18708
|
+
}
|
|
18199
18709
|
await mcp.notification({
|
|
18200
18710
|
method: "notifications/claude/channel",
|
|
18201
18711
|
params: {
|
|
@@ -18205,12 +18715,21 @@ async function connectSocketMode() {
|
|
|
18205
18715
|
user_name: userName,
|
|
18206
18716
|
channel,
|
|
18207
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,
|
|
18208
18725
|
event_type: evt.type,
|
|
18209
18726
|
...isAutoFollowed ? { auto_followed: "true" } : {},
|
|
18210
18727
|
// Only set these when we actually have attachments to avoid
|
|
18211
18728
|
// bloating every notification with empty metadata.
|
|
18212
18729
|
...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
|
|
18213
|
-
...imagePath ? { image_path: imagePath } : {}
|
|
18730
|
+
...imagePath ? { image_path: imagePath } : {},
|
|
18731
|
+
// ENG-5830: the pre-loaded surrounding thread (thread replies only).
|
|
18732
|
+
...threadContext ? { thread_context: threadContext } : {}
|
|
18214
18733
|
}
|
|
18215
18734
|
}
|
|
18216
18735
|
});
|
|
@@ -18323,3 +18842,7 @@ if (THREAD_STORE_PATH) {
|
|
|
18323
18842
|
);
|
|
18324
18843
|
}
|
|
18325
18844
|
connectSocketModeSafely();
|
|
18845
|
+
export {
|
|
18846
|
+
__resetSlackGiveUpNoticeStateForTests,
|
|
18847
|
+
__resetSlackUndeliverableNoticeThrottle
|
|
18848
|
+
};
|