@integrity-labs/agt-cli 0.27.8-test.9 → 0.27.9-test.11

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