@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.
Files changed (28) hide show
  1. package/dist/assets/impersonate-statusline.sh +7 -2
  2. package/dist/bin/agt.js +946 -129
  3. package/dist/bin/agt.js.map +1 -1
  4. package/dist/{chunk-YSBGIXJG.js → chunk-I2OTQJH3.js} +361 -26
  5. package/dist/chunk-I2OTQJH3.js.map +1 -0
  6. package/dist/{chunk-GN4XPQWJ.js → chunk-MQJ5DMPT.js} +535 -94
  7. package/dist/chunk-MQJ5DMPT.js.map +1 -0
  8. package/dist/{chunk-IB655E5U.js → chunk-SXBZDUKN.js} +1086 -285
  9. package/dist/chunk-SXBZDUKN.js.map +1 -0
  10. package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-F6USL3EW.js} +2 -2
  11. package/dist/lib/manager-worker.js +2071 -517
  12. package/dist/lib/manager-worker.js.map +1 -1
  13. package/dist/mcp/direct-chat-channel.js +49 -455
  14. package/dist/mcp/index.js +232 -137
  15. package/dist/mcp/slack-channel.js +1379 -856
  16. package/dist/mcp/teams-channel.js +16018 -0
  17. package/dist/mcp/telegram-channel.js +1264 -618
  18. package/dist/{persistent-session-ICYFLUAM.js → persistent-session-OL5EDYB4.js} +5 -3
  19. package/dist/responsiveness-probe-B6LJJRUD.js +74 -0
  20. package/dist/responsiveness-probe-B6LJJRUD.js.map +1 -0
  21. package/package.json +2 -2
  22. package/dist/chunk-GN4XPQWJ.js.map +0 -1
  23. package/dist/chunk-IB655E5U.js.map +0 -1
  24. package/dist/chunk-YSBGIXJG.js.map +0 -1
  25. package/dist/responsiveness-probe-WZNQ2762.js +0 -33
  26. package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
  27. /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-F6USL3EW.js.map} +0 -0
  28. /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 decideSenderPolicyForward(evt, policy) {
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-progress.ts
14355
- var MODE_EMOJI = {
14356
- thinking: "\u{1F4AD}",
14357
- working: "\u{1F6E0}\uFE0F",
14358
- waiting: "\u23F3"
14359
- };
14360
- var TERMINAL_EMOJI = {
14361
- completed: "\u2705",
14362
- failed: "\u274C"
14363
- };
14364
- function formatWallClock(ms, timeZone) {
14365
- const fmt = new Intl.DateTimeFormat("en-GB", {
14366
- hour: "2-digit",
14367
- minute: "2-digit",
14368
- second: "2-digit",
14369
- hour12: false,
14370
- timeZone,
14371
- timeZoneName: "short"
14372
- });
14373
- const parts = fmt.formatToParts(new Date(ms));
14374
- const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
14375
- return `${get("hour")}:${get("minute")}:${get("second")} ${get("timeZoneName")}`;
14376
- }
14377
- function renderHeaderLine(state) {
14378
- if (state.terminal) {
14379
- return `${TERMINAL_EMOJI[state.terminal.kind]} *${state.terminal.kind === "completed" ? "Done" : "Failed"}* \u2014 ${state.initialLabel}`;
14380
- }
14381
- return `${MODE_EMOJI[state.mode]} *Working on:* ${state.initialLabel}`;
14382
- }
14383
- function renderStepLine(state) {
14384
- if (state.terminal) {
14385
- return state.terminal.message ? state.terminal.message : void 0;
14386
- }
14387
- const parts = [];
14388
- if (state.stepNumber) {
14389
- if (state.stepNumber.total != null) {
14390
- parts.push(`*Step ${state.stepNumber.current} of ${state.stepNumber.total}*`);
14391
- } else {
14392
- parts.push(`*Step ${state.stepNumber.current}*`);
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
- if (state.stepLabel) parts.push(state.stepLabel);
14396
- return parts.length > 0 ? parts.join(": ") : void 0;
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
- function renderProgressBlocks(state) {
14399
- const header = renderHeaderLine(state);
14400
- const step = renderStepLine(state);
14401
- const footer = state.terminal ? `_${state.terminal.kind === "completed" ? "completed" : "failed"}: ${formatWallClock(state.lastChangeAt)}_` : `_last update: ${formatWallClock(state.lastChangeAt)}_`;
14402
- const lines = [header];
14403
- if (step) lines.push(step);
14404
- if (state.detail) lines.push(`_${state.detail}_`);
14405
- lines.push(footer);
14406
- const fallbackText = state.terminal ? `${state.terminal.kind === "completed" ? "Done" : "Failed"} \u2014 ${state.initialLabel}` : `Working on: ${state.initialLabel}`;
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
- text: fallbackText,
14409
- blocks: [
14866
+ content: [
14410
14867
  {
14411
- type: "section",
14412
- text: { type: "mrkdwn", text: lines.join("\n") }
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
- var SLACK_POST_URL = "https://slack.com/api/chat.postMessage";
14418
- var SLACK_UPDATE_URL = "https://slack.com/api/chat.update";
14419
- var DEFAULT_TIMEOUT_MS = 5e3;
14420
- var SlackApiError = class extends Error {
14421
- constructor(endpoint, slackError, status) {
14422
- super(`Slack ${endpoint} failed: ${slackError} (status=${status})`);
14423
- this.endpoint = endpoint;
14424
- this.slackError = slackError;
14425
- this.status = status;
14426
- this.name = "SlackApiError";
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 createSlackProgressFlush(opts) {
14430
- const fetchImpl = opts.fetchImpl ?? fetch;
14431
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
14432
- let anchorTs = null;
14433
- async function slackCall(endpoint, body) {
14434
- const url = endpoint === "chat.postMessage" ? SLACK_POST_URL : SLACK_UPDATE_URL;
14435
- const controller = new AbortController();
14436
- const timer = setTimeout(() => controller.abort(), timeoutMs);
14437
- let response;
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
- const parsed = await response.json().catch(() => ({ ok: false, error: "invalid_json" }));
14452
- if (!parsed.ok) {
14453
- throw new SlackApiError(endpoint, parsed.error ?? "unknown", response.status);
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
- return parsed;
14928
+ } catch {
14456
14929
  }
14457
- return async function flush(state) {
14458
- const payload = renderProgressBlocks(state);
14459
- if (anchorTs == null) {
14460
- const body = {
14461
- channel: opts.channel,
14462
- text: payload.text,
14463
- blocks: payload.blocks
14464
- };
14465
- if (opts.threadTs) body.thread_ts = opts.threadTs;
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
- const res = await slackCall("chat.postMessage", body);
14468
- if (!res.ts) {
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
- throw err;
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
- // src/progress-tools.ts
14500
- import { randomUUID } from "crypto";
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
- // src/progress-handle.ts
14503
- async function startProgressHandle(opts) {
14504
- const now = opts.now ?? (() => Date.now());
14505
- const setTimer = opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
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
- function applyUpdate(next) {
14549
- if (next.mode !== void 0) state.mode = next.mode;
14550
- if (next.stepLabel !== void 0) state.stepLabel = next.stepLabel;
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 existsSync2,
15050
+ existsSync as existsSync3,
14913
15051
  mkdirSync as mkdirSync3,
14914
- readFileSync as readFileSync3,
14915
- readdirSync,
15052
+ readFileSync as readFileSync4,
15053
+ readdirSync as readdirSync3,
14916
15054
  renameSync as renameSync2,
14917
- statSync,
14918
- unlinkSync as unlinkSync2,
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 join3, resolve as resolve2 } from "path";
15060
+ import { basename, join as join5, resolve as resolve2 } from "path";
14923
15061
  import { homedir as homedir2 } from "os";
14924
- import { createHash, randomUUID as randomUUID2 } from "crypto";
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 = readFileSync(filePath, "utf-8");
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 readFileSync2,
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 join2 } from "path";
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 = join2(agentDir, basename2);
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
- unlinkSync(lockPath);
15804
+ unlinkSync2(lockPath);
15749
15805
  } catch {
15750
15806
  }
15751
15807
  }
15752
15808
  function readLockHolder(path) {
15753
- if (!existsSync(path)) return null;
15809
+ if (!existsSync2(path)) return null;
15754
15810
  try {
15755
- const raw = readFileSync2(path, "utf8");
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
- if (raw === "all") return { mode: "all" };
15777
- if (raw === "agents_only") return { mode: "agents_only" };
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 ? join3(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15838
- var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join3(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15839
- var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join3(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
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 join3(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
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 clearAllSlackPendingMarkersForThread(channel, threadTs) {
15873
- if (!SLACK_PENDING_INBOUND_DIR) return;
15874
- const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
15875
- const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
15876
- const prefix = `${safeChan}__${safeThread}__`;
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
- for (const f of readdirSync(SLACK_PENDING_INBOUND_DIR)) {
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 = join3(SLACK_RECOVERY_OUTBOX_DIR, filename);
16129
+ const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
15903
16130
  let payload;
15904
16131
  try {
15905
- payload = JSON.parse(readFileSync3(fullPath, "utf-8"));
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
- unlinkSync2(fullPath);
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, join3(SLACK_RECOVERY_OUTBOX_DIR, next.next));
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 = readdirSync(SLACK_RECOVERY_OUTBOX_DIR);
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 = statSync(join3(SLACK_RECOVERY_OUTBOX_DIR, f)).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 readdirSync(SLACK_RECOVERY_OUTBOX_DIR)) {
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 (existsSync2(join3(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
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 sweepSlackStaleMarkersOnBoot() {
16294
+ function sweepSlackStaleMarkers(thresholdMs) {
16068
16295
  if (!SLACK_PENDING_INBOUND_DIR) return;
16069
- if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16296
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
16070
16297
  let filenames;
16071
16298
  try {
16072
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
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 = join3(SLACK_PENDING_INBOUND_DIR, filename);
16312
+ const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
16086
16313
  let marker;
16087
16314
  try {
16088
- marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
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
- unlinkSync2(fullPath);
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 || !received_at) {
16329
+ if (!channel || !thread_ts || !message_ts || isPendingMarkerStale(received_at, now, thresholdMs)) {
16103
16330
  try {
16104
- unlinkSync2(fullPath);
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
- sweepSlackStaleMarkersOnBoot();
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
- clearAllSlackPendingMarkersForThread(channel, threadTs);
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 (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16514
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
16139
16515
  let filenames;
16140
16516
  try {
16141
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
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
- try {
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 = join3(homedir2(), ".augmented", "restart-flags");
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 `/restart` can also be typed as plain messages in any channel where the bot is present:",
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
- "\u2022 `/restart` \u2014 restart this agent",
16211
- "\u2022 `/agent-status` \u2014 report whether this agent is online + last activity",
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 === "/agent-status") {
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 === "/restart") {
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 (!existsSync2(RESTART_FLAGS_DIR)) {
16901
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16338
16902
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16339
16903
  }
16340
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
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}.${randomUUID2()}.tmp`;
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: ${redactAugmentedPaths(err.message)}
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 (!existsSync2(RESTART_FLAGS_DIR)) {
17012
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16445
17013
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16446
17014
  }
16447
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
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}.${randomUUID2()}.tmp`;
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: ${redactAugmentedPaths(err.message)}
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 join3(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
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 = join3(homedir2(), ".augmented", AGENT_CODE_NAME);
17109
+ const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
16542
17110
  mkdirSync3(logDir, { recursive: true });
16543
- slackStderrLogStream = createWriteStream(join3(logDir, "slack-channel-stderr.log"), {
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
- 'Messages from Slack arrive as <channel source="slack" user="<slack-id>" user_name="<display-name>" channel="..." thread_ts="...">. Pass channel + thread_ts from the tag to slack.reply; always include thread_ts on threaded replies.',
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 you have something useful to add.',
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. Use this to catch up on thread context before replying, especially for auto-followed threads.",
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: '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. Pass `interactive: { type: "rate_run", run_id }` to append a 1-5 rating button row that records the score against this scheduled-task run (ENG-4572).',
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
- const progressResult = await progressRegistry.handle(name, args ?? {});
16913
- if (progressResult !== void 0) return progressResult;
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 && thread_ts) {
16975
- clearPendingMessage(channel, thread_ts);
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 allMessages = [];
17071
- let cursor;
17072
- do {
17073
- const remaining = limit - allMessages.length;
17074
- if (remaining <= 0) break;
17075
- const params = new URLSearchParams({
17076
- channel,
17077
- ts: thread_ts,
17078
- limit: String(Math.min(remaining, 100)),
17079
- ...cursor ? { cursor } : {}
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: allMessages.length > 0 ? `Thread (${allMessages.length} messages):
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 stat = statSync(resolvedPath);
17152
- if (!stat.isFile()) {
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 = stat.size;
17159
- bytes = readFileSync3(resolvedPath);
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: ${redactAugmentedPaths(err.message)}`
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, interactive } = args;
17982
+ const { channel, blocks, text, thread_ts } = args;
17457
17983
  if (typeof channel !== "string" || !channel) {
17458
- return errResult2("channel is required");
17984
+ return errResult("channel is required");
17459
17985
  }
17460
17986
  if (typeof text !== "string" || !text) {
17461
- return errResult2("text is required (used as fallback for push notifications and unfurls)");
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 errResult2(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
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: effectiveBlocks,
17996
+ blocks,
17538
17997
  text,
17539
17998
  ...thread_ts ? { thread_ts } : {}
17540
17999
  });
17541
18000
  if (!result.ok || !result.ts) {
17542
- return errResult2(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
18001
+ return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
17543
18002
  }
17544
- if (ratingCallbackId) {
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 errResult2("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
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 errResult2("channel is required");
17604
- if (typeof question !== "string" || !question) return errResult2("question is required");
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 errResult2(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
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 errResult2(`Failed to register pending interaction: ${err.message}`);
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 errResult2(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
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 errResult2(
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 errResult2("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
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 errResult2("prompt_text is required");
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 errResult2(
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 errResult2("options must contain at least 2 entries (this is a picker, not a notification)");
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 errResult2("schema is required with shape { type: 'yes_no' | 'choice' }");
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 errResult2("schema.type must be 'yes_no' or 'choice'");
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 errResult2("schema.type='yes_no' requires exactly 2 options");
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 errResult2("request_id must be a non-empty string when provided");
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 errResult2(`channel_request_input dispatch failed: ${reason}`);
18200
+ return errResult(`channel_request_input dispatch failed: ${reason}`);
17757
18201
  }
17758
18202
  } catch (err) {
17759
- return errResult2(
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 errResult2(text) {
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 redactAugmentedPaths(msg) {
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}: ${redactAugmentedPaths(err.message)}
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
- process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
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 isRestartCommand = strippedText === "/restart" || strippedText.startsWith("/restart ");
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
- if (decideSlackAckReaction({ channel, ts })) {
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: "eyes" })
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
+ };