@integrity-labs/agt-cli 0.27.7-test.7 → 0.27.8-test.10

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.
@@ -14237,17 +14237,184 @@ function extractAugmentedSlackLabel(evt) {
14237
14237
  agentId: evt.metadata.event_payload?.["augmented_agent_id"]
14238
14238
  };
14239
14239
  }
14240
- function decideSenderPolicyForward(evt, policy) {
14240
+ function decideModeForward(evt, policy) {
14241
14241
  if (policy.mode === "all") return { forward: true };
14242
+ if (policy.mode === "manager_only") {
14243
+ if (policy.principalSlackUserId && evt.user === policy.principalSlackUserId) {
14244
+ return { forward: true };
14245
+ }
14246
+ }
14242
14247
  const label = extractAugmentedSlackLabel(evt);
14243
14248
  if (!label) return { forward: false, reason: "sender_policy" };
14244
- if (policy.mode === "team_agents_only") {
14249
+ if (policy.mode === "team_agents_only" || policy.mode === "manager_only") {
14245
14250
  if (!policy.teamId || label.teamId !== policy.teamId) {
14246
14251
  return { forward: false, reason: "sender_policy" };
14247
14252
  }
14248
14253
  }
14249
14254
  return { forward: true };
14250
14255
  }
14256
+ function decideSenderPolicyForward(evt, policy) {
14257
+ if (policy.internalOnly) {
14258
+ if (!policy.homeTeamId) return { forward: false, reason: "sender_policy" };
14259
+ if (evt.team !== policy.homeTeamId) return { forward: false, reason: "sender_policy" };
14260
+ }
14261
+ return decideModeForward(evt, policy);
14262
+ }
14263
+
14264
+ // src/sender-policy-decline.ts
14265
+ function classifySlackPolicyBlock(evt, policy) {
14266
+ if (policy.internalOnly && (!policy.homeTeamId || evt.team !== policy.homeTeamId)) {
14267
+ return "internal_only";
14268
+ }
14269
+ switch (policy.mode) {
14270
+ case "manager_only":
14271
+ return "mode_manager_only";
14272
+ case "team_agents_only":
14273
+ return "mode_team_agents_only";
14274
+ case "agents_only":
14275
+ return "mode_agents_only";
14276
+ case "all":
14277
+ return "internal_only";
14278
+ }
14279
+ }
14280
+ function politeDeclineCopy(reason) {
14281
+ switch (reason) {
14282
+ case "internal_only":
14283
+ return "Sorry, I can only respond to people inside our organisation. This conversation appears to be from outside.";
14284
+ case "mode_manager_only":
14285
+ return "Sorry, I only respond to my manager in this channel.";
14286
+ case "mode_team_agents_only":
14287
+ return "Sorry, I only respond to agents on my team in this channel.";
14288
+ case "mode_agents_only":
14289
+ return "Sorry, I only respond to other agents in this channel.";
14290
+ }
14291
+ }
14292
+ function decideDeclineReply(input) {
14293
+ const last = input.cache.get(input.key);
14294
+ if (last !== void 0) {
14295
+ const elapsed = input.now - last;
14296
+ if (elapsed < input.cooldownMs) {
14297
+ return { reply: false, remainingMs: input.cooldownMs - elapsed };
14298
+ }
14299
+ }
14300
+ input.cache.set(input.key, input.now);
14301
+ return { reply: true };
14302
+ }
14303
+ function declineCacheKey(channelId, senderId, reason) {
14304
+ return `${channelId}|${senderId}|${reason}`;
14305
+ }
14306
+ function readDeclineCooldownMs(envVarName, defaultSeconds = 1800) {
14307
+ const raw = process.env[envVarName];
14308
+ if (!raw) return defaultSeconds * 1e3;
14309
+ const parsed = Number(raw);
14310
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultSeconds * 1e3;
14311
+ return parsed * 1e3;
14312
+ }
14313
+
14314
+ // src/ack-reaction.ts
14315
+ import { readdirSync, readFileSync } from "fs";
14316
+ import { join } from "path";
14317
+ var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
14318
+ var ACK_STARTUP_GRACE_MS = 6e4;
14319
+ var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
14320
+ function decideAckReaction(i) {
14321
+ if (!i.hasTarget) return "none";
14322
+ if (!i.integrationReady) return "undeliverable";
14323
+ if (i.tmux === "dead") return "undeliverable";
14324
+ if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
14325
+ const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
14326
+ if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
14327
+ const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
14328
+ const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
14329
+ if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
14330
+ return "ack";
14331
+ }
14332
+ return "undeliverable";
14333
+ }
14334
+ return "ack";
14335
+ }
14336
+ function decideRecoveryHeal(i) {
14337
+ return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
14338
+ }
14339
+ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
14340
+ if (!dir) return null;
14341
+ let names;
14342
+ try {
14343
+ names = readdirSync(dir);
14344
+ } catch {
14345
+ return null;
14346
+ }
14347
+ let oldest = null;
14348
+ for (const name of names) {
14349
+ if (!name.endsWith(".json")) continue;
14350
+ let receivedAt;
14351
+ try {
14352
+ const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
14353
+ receivedAt = raw.received_at;
14354
+ } catch {
14355
+ continue;
14356
+ }
14357
+ if (!receivedAt) continue;
14358
+ const t = Date.parse(receivedAt);
14359
+ if (Number.isNaN(t)) continue;
14360
+ const age = now - t;
14361
+ if (age < 0) continue;
14362
+ if (oldest == null || age > oldest) oldest = age;
14363
+ }
14364
+ return oldest;
14365
+ }
14366
+
14367
+ // src/session-probe-runtime.ts
14368
+ import { execFileSync } from "child_process";
14369
+ function agentTmuxSessionName(codeName) {
14370
+ return `agt-${codeName}`;
14371
+ }
14372
+ function escapePgrepRegex(value) {
14373
+ return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
14374
+ }
14375
+ function probeClaudeProcessInTmux(tmuxSession) {
14376
+ const escapedSession = escapePgrepRegex(tmuxSession);
14377
+ const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
14378
+ try {
14379
+ const out = execFileSync("pgrep", ["-f", "--", pattern], {
14380
+ encoding: "utf-8",
14381
+ timeout: 3e3
14382
+ }).trim();
14383
+ return out.length > 0 ? "alive" : "dead";
14384
+ } catch (err) {
14385
+ const e = err;
14386
+ if (e?.code === "ENOENT") return "unknown";
14387
+ return e?.status === 1 ? "dead" : "unknown";
14388
+ }
14389
+ }
14390
+ function probeTmuxSession(tmuxSession) {
14391
+ try {
14392
+ execFileSync("tmux", ["has-session", "-t", tmuxSession], {
14393
+ stdio: "ignore",
14394
+ timeout: 3e3
14395
+ });
14396
+ return "alive";
14397
+ } catch (err) {
14398
+ const e = err;
14399
+ if (e?.code === "ENOENT") return "unknown";
14400
+ return "dead";
14401
+ }
14402
+ }
14403
+ function probeAgentSession(codeName) {
14404
+ const session = agentTmuxSessionName(codeName);
14405
+ const tmux = probeTmuxSession(session);
14406
+ const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
14407
+ return { tmux, claude };
14408
+ }
14409
+ var probeCache = /* @__PURE__ */ new Map();
14410
+ var SESSION_PROBE_TTL_MS = 15e3;
14411
+ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
14412
+ const cached2 = probeCache.get(codeName);
14413
+ if (cached2 && now - cached2.at < ttlMs) return cached2.value;
14414
+ const value = probeAgentSession(codeName);
14415
+ probeCache.set(codeName, { at: now, value });
14416
+ return value;
14417
+ }
14251
14418
 
14252
14419
  // src/slack-loop-throttle.ts
14253
14420
  var DEFAULT_THROTTLE = {
@@ -14351,466 +14518,62 @@ async function isThreadKilled(opts) {
14351
14518
  }
14352
14519
  }
14353
14520
 
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}*`);
14393
- }
14394
- }
14395
- if (state.stepLabel) parts.push(state.stepLabel);
14396
- return parts.length > 0 ? parts.join(": ") : void 0;
14397
- }
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}`;
14407
- return {
14408
- text: fallbackText,
14409
- blocks: [
14410
- {
14411
- type: "section",
14412
- text: { type: "mrkdwn", text: lines.join("\n") }
14413
- }
14414
- ]
14415
- };
14416
- }
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";
14427
- }
14428
- };
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);
14450
- }
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);
14454
- }
14455
- return parsed;
14456
- }
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;
14466
- 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
- }
14521
+ // src/slack-thread-context.ts
14522
+ var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
14523
+ var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
14524
+ function formatThreadMessages(messages, nameById) {
14525
+ return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
14526
+ }
14527
+ function capThreadContext(formatted, maxChars) {
14528
+ if (formatted.length <= maxChars) return formatted;
14529
+ const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
14530
+ if (maxChars <= marker.length) return marker.slice(0, maxChars);
14531
+ const tailBudget = maxChars - marker.length;
14532
+ const tail = formatted.slice(formatted.length - tailBudget);
14533
+ const firstNewline = tail.indexOf("\n");
14534
+ const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
14535
+ return `${marker}${clean}`;
14536
+ }
14537
+ async function fetchThreadTranscript(channel, threadTs, limit, deps) {
14538
+ const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
14539
+ const cappedLimit = Math.min(Math.max(limit, 1), 200);
14540
+ const allMessages = [];
14541
+ let cursor;
14542
+ do {
14543
+ const remaining = cappedLimit - allMessages.length;
14544
+ if (remaining <= 0) break;
14545
+ const params = new URLSearchParams({
14546
+ channel,
14547
+ ts: threadTs,
14548
+ limit: String(Math.min(remaining, 100)),
14549
+ ...cursor ? { cursor } : {}
14550
+ });
14551
+ let data;
14482
14552
  try {
14483
- await slackCall("chat.update", {
14484
- channel: opts.channel,
14485
- ts: anchorTs,
14486
- text: payload.text,
14487
- blocks: payload.blocks
14553
+ const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
14554
+ headers: { Authorization: `Bearer ${botToken}` }
14488
14555
  });
14556
+ if (!res.ok) return { ok: false, error: `http_${res.status}` };
14557
+ data = await res.json();
14489
14558
  } catch (err) {
14490
- if (opts.onApiError && err instanceof SlackApiError) {
14491
- await opts.onApiError(err);
14492
- return;
14493
- }
14494
- throw err;
14495
- }
14496
- };
14497
- }
14498
-
14499
- // src/progress-tools.ts
14500
- import { randomUUID } from "crypto";
14501
-
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
- };
14547
- }
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(() => {
14559
+ return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
14560
+ }
14561
+ if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
14562
+ for (const msg of data.messages ?? []) {
14563
+ allMessages.push({
14564
+ user: msg.user ?? msg.bot_id ?? "unknown",
14565
+ text: msg.text ?? "",
14566
+ ts: msg.ts ?? ""
14566
14567
  });
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
14568
  }
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";
14569
+ cursor = data.response_metadata?.next_cursor || void 0;
14570
+ } while (cursor && allMessages.length < cappedLimit);
14571
+ const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
14572
+ const resolved = await Promise.all(
14573
+ uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
14574
+ );
14575
+ const nameById = new Map(resolved);
14576
+ return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
14814
14577
  }
14815
14578
 
14816
14579
  // src/impersonation.ts
@@ -14860,6 +14623,69 @@ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
14860
14623
  "slack.upload_file"
14861
14624
  ]);
14862
14625
 
14626
+ // src/slack-pending-inbound-cleanup.ts
14627
+ import { existsSync, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
14628
+ import { join as join2 } from "path";
14629
+ function sanitizeMarkerSegment(value) {
14630
+ return value.replace(/[^A-Za-z0-9_-]/g, "_");
14631
+ }
14632
+ var defaultClearMarkerFile = (fullPath) => {
14633
+ try {
14634
+ if (existsSync(fullPath)) unlinkSync(fullPath);
14635
+ } catch {
14636
+ }
14637
+ };
14638
+ function clearAllSlackPendingMarkersForThread(dir, channel, threadTs, clear = defaultClearMarkerFile) {
14639
+ if (!dir) return 0;
14640
+ const prefix = `${sanitizeMarkerSegment(channel)}__${sanitizeMarkerSegment(threadTs)}__`;
14641
+ let cleared = 0;
14642
+ try {
14643
+ for (const f of readdirSync2(dir)) {
14644
+ if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
14645
+ clear(join2(dir, f));
14646
+ cleared += 1;
14647
+ }
14648
+ } catch {
14649
+ }
14650
+ return cleared;
14651
+ }
14652
+ function clearSlackPendingMarkerByMessageTs(dir, channel, messageTs, clear = defaultClearMarkerFile) {
14653
+ if (!dir) return 0;
14654
+ const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
14655
+ const messageSuffix = `__${sanitizeMarkerSegment(messageTs)}.json`;
14656
+ let cleared = 0;
14657
+ try {
14658
+ for (const f of readdirSync2(dir)) {
14659
+ if (!f.startsWith(channelPrefix) || !f.endsWith(messageSuffix)) continue;
14660
+ clear(join2(dir, f));
14661
+ cleared += 1;
14662
+ }
14663
+ } catch {
14664
+ }
14665
+ return cleared;
14666
+ }
14667
+ function clearOldestSlackPendingMarkerInChannel(dir, channel, clear = defaultClearMarkerFile) {
14668
+ if (!dir) return null;
14669
+ const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
14670
+ try {
14671
+ const entries = readdirSync2(dir).filter((f) => f.startsWith(channelPrefix) && f.endsWith(".json")).map((f) => {
14672
+ const full = join2(dir, f);
14673
+ let mtime = 0;
14674
+ try {
14675
+ mtime = statSync(full).mtimeMs;
14676
+ } catch {
14677
+ }
14678
+ return { name: f, full, mtime };
14679
+ }).filter((e) => e.mtime > 0).sort((a, b) => a.mtime - b.mtime);
14680
+ const oldest = entries[0];
14681
+ if (!oldest) return null;
14682
+ clear(oldest.full);
14683
+ return oldest.name;
14684
+ } catch {
14685
+ return null;
14686
+ }
14687
+ }
14688
+
14863
14689
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
14864
14690
  import process2 from "process";
14865
14691
 
@@ -14956,22 +14782,22 @@ var StdioServerTransport = class {
14956
14782
  import {
14957
14783
  chmodSync,
14958
14784
  createWriteStream,
14959
- existsSync as existsSync2,
14785
+ existsSync as existsSync3,
14960
14786
  mkdirSync as mkdirSync3,
14961
- readFileSync as readFileSync3,
14962
- readdirSync,
14787
+ readFileSync as readFileSync4,
14788
+ readdirSync as readdirSync3,
14963
14789
  renameSync as renameSync2,
14964
- statSync,
14965
- unlinkSync as unlinkSync2,
14790
+ statSync as statSync2,
14791
+ unlinkSync as unlinkSync3,
14966
14792
  watch,
14967
14793
  writeFileSync as writeFileSync3
14968
14794
  } from "fs";
14969
- import { basename, join as join3, resolve as resolve2 } from "path";
14795
+ import { basename, join as join5, resolve as resolve2 } from "path";
14970
14796
  import { homedir as homedir2 } from "os";
14971
- import { createHash, randomUUID as randomUUID2 } from "crypto";
14797
+ import { createHash, randomUUID } from "crypto";
14972
14798
 
14973
14799
  // src/slack-thread-store.ts
14974
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
14800
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
14975
14801
  import { dirname } from "path";
14976
14802
  var FILE_VERSION = 1;
14977
14803
  var DEFAULT_TTL_DAYS = 30;
@@ -14982,7 +14808,7 @@ function loadThreadStore(filePath, opts = {}) {
14982
14808
  const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
14983
14809
  let raw;
14984
14810
  try {
14985
- raw = readFileSync(filePath, "utf-8");
14811
+ raw = readFileSync2(filePath, "utf-8");
14986
14812
  } catch {
14987
14813
  return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
14988
14814
  }
@@ -15097,9 +14923,9 @@ async function runOrRetry(fn, opts) {
15097
14923
 
15098
14924
  // src/channel-attachments.ts
15099
14925
  import { homedir } from "os";
15100
- import { join, resolve, sep } from "path";
14926
+ import { join as join3, resolve, sep } from "path";
15101
14927
  function resolveChannelInboundDir(codeName, channelSlug) {
15102
- const base = join(homedir(), ".augmented");
14928
+ const base = join3(homedir(), ".augmented");
15103
14929
  const allowedSegment = /^[A-Za-z0-9_-]+$/;
15104
14930
  if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
15105
14931
  throw new Error(
@@ -15743,14 +15569,14 @@ function createSlackBotUserIdClient(args) {
15743
15569
 
15744
15570
  // src/mcp-spawn-lock.ts
15745
15571
  import {
15746
- existsSync,
15572
+ existsSync as existsSync2,
15747
15573
  mkdirSync as mkdirSync2,
15748
- readFileSync as readFileSync2,
15574
+ readFileSync as readFileSync3,
15749
15575
  renameSync,
15750
- unlinkSync,
15576
+ unlinkSync as unlinkSync2,
15751
15577
  writeFileSync as writeFileSync2
15752
15578
  } from "fs";
15753
- import { join as join2 } from "path";
15579
+ import { join as join4 } from "path";
15754
15580
  function defaultIsPidAlive(pid) {
15755
15581
  if (!Number.isFinite(pid) || pid <= 0) return false;
15756
15582
  try {
@@ -15768,7 +15594,7 @@ function acquireMcpSpawnLock(args) {
15768
15594
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15769
15595
  const selfPid = options.selfPid ?? process.pid;
15770
15596
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15771
- const path = join2(agentDir, basename2);
15597
+ const path = join4(agentDir, basename2);
15772
15598
  const existing = readLockHolder(path);
15773
15599
  if (existing) {
15774
15600
  if (existing.pid === selfPid) {
@@ -15792,14 +15618,14 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
15792
15618
  if (!existing) return;
15793
15619
  if (existing.pid !== selfPid) return;
15794
15620
  try {
15795
- unlinkSync(lockPath);
15621
+ unlinkSync2(lockPath);
15796
15622
  } catch {
15797
15623
  }
15798
15624
  }
15799
15625
  function readLockHolder(path) {
15800
- if (!existsSync(path)) return null;
15626
+ if (!existsSync2(path)) return null;
15801
15627
  try {
15802
- const raw = readFileSync2(path, "utf8");
15628
+ const raw = readFileSync3(path, "utf8");
15803
15629
  const parsed = JSON.parse(raw);
15804
15630
  const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
15805
15631
  if (!Number.isFinite(pid) || pid <= 0) return null;
@@ -15820,16 +15646,88 @@ var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
15820
15646
  var AGT_TEAM_ID = process.env.AGT_TEAM_ID ?? null;
15821
15647
  var SLACK_SENDER_POLICY = (() => {
15822
15648
  const raw = (process.env.SLACK_SENDER_POLICY ?? "all").trim().toLowerCase();
15823
- if (raw === "all") return { mode: "all" };
15824
- if (raw === "agents_only") return { mode: "agents_only" };
15649
+ const internalOnly = process.env.SLACK_INTERNAL_ONLY === "true";
15650
+ const homeTeamId = process.env.SLACK_HOME_TEAM_ID;
15651
+ if (internalOnly && !homeTeamId) {
15652
+ throw new Error(
15653
+ "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)."
15654
+ );
15655
+ }
15656
+ const internalOnlyFields = internalOnly ? { internalOnly: true, homeTeamId } : {};
15657
+ if (raw === "all") return { mode: "all", ...internalOnlyFields };
15658
+ if (raw === "agents_only") return { mode: "agents_only", ...internalOnlyFields };
15825
15659
  if (raw === "team_agents_only") {
15826
15660
  if (!AGT_TEAM_ID) {
15827
15661
  throw new Error("SLACK_SENDER_POLICY=team_agents_only requires AGT_TEAM_ID");
15828
15662
  }
15829
- return { mode: "team_agents_only", teamId: AGT_TEAM_ID };
15663
+ return { mode: "team_agents_only", teamId: AGT_TEAM_ID, ...internalOnlyFields };
15664
+ }
15665
+ if (raw === "manager_only") {
15666
+ const principalSlackUserId = process.env.SLACK_SENDER_POLICY_PRINCIPAL_ID;
15667
+ if (!AGT_TEAM_ID) {
15668
+ throw new Error("SLACK_SENDER_POLICY=manager_only requires AGT_TEAM_ID");
15669
+ }
15670
+ if (!principalSlackUserId) {
15671
+ throw new Error(
15672
+ "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."
15673
+ );
15674
+ }
15675
+ return { mode: "manager_only", teamId: AGT_TEAM_ID, principalSlackUserId, ...internalOnlyFields };
15830
15676
  }
15831
15677
  throw new Error(`Invalid SLACK_SENDER_POLICY=${JSON.stringify(process.env.SLACK_SENDER_POLICY)}`);
15832
15678
  })();
15679
+ var SLACK_POLICY_DECLINE_CACHE = /* @__PURE__ */ new Map();
15680
+ var SLACK_POLICY_DECLINE_COOLDOWN_MS = readDeclineCooldownMs(
15681
+ "SLACK_SENDER_POLICY_REPLY_COOLDOWN_SEC"
15682
+ );
15683
+ async function maybeSendSenderPolicyDecline(args) {
15684
+ if (!BOT_TOKEN) return;
15685
+ if (!args.channel || !args.senderId) return;
15686
+ const key2 = declineCacheKey(args.channel, args.senderId, args.subReason);
15687
+ const decision = decideDeclineReply({
15688
+ cache: SLACK_POLICY_DECLINE_CACHE,
15689
+ key: key2,
15690
+ cooldownMs: SLACK_POLICY_DECLINE_COOLDOWN_MS,
15691
+ now: Date.now()
15692
+ });
15693
+ if (!decision.reply) {
15694
+ process.stderr.write(
15695
+ `slack-channel(${AGENT_CODE_NAME}): decline suppressed by cooldown (channel=${redactSlackId(args.channel)}, sender=${redactSlackId(args.senderId)}, reason=${args.subReason}, remaining=${decision.remainingMs}ms)
15696
+ `
15697
+ );
15698
+ return;
15699
+ }
15700
+ const text = politeDeclineCopy(args.subReason);
15701
+ const controller = new AbortController();
15702
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
15703
+ try {
15704
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
15705
+ method: "POST",
15706
+ signal: controller.signal,
15707
+ headers: {
15708
+ "Content-Type": "application/json",
15709
+ Authorization: `Bearer ${BOT_TOKEN}`
15710
+ },
15711
+ body: JSON.stringify({
15712
+ channel: args.channel,
15713
+ text,
15714
+ // Reply in-thread if the inbound was a thread message — keeps
15715
+ // the decline next to the message that caused it. For top-level
15716
+ // posts (DMs / channel root) omit thread_ts and post inline.
15717
+ ...args.threadTs ? { thread_ts: args.threadTs } : {}
15718
+ })
15719
+ });
15720
+ const data = await res.json();
15721
+ if (!data.ok) {
15722
+ process.stderr.write(
15723
+ `slack-channel(${AGENT_CODE_NAME}): decline post failed: ${data.error ?? "unknown"}
15724
+ `
15725
+ );
15726
+ }
15727
+ } finally {
15728
+ clearTimeout(timeoutId);
15729
+ }
15730
+ }
15833
15731
  var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
15834
15732
  var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
15835
15733
  var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
@@ -15881,9 +15779,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15881
15779
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15882
15780
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15883
15781
  };
15884
- var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join3(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15885
- var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join3(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15886
- var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join3(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15782
+ var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join5(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15783
+ var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15784
+ var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15887
15785
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
15888
15786
  function redactSlackId(id) {
15889
15787
  if (!id) return "<none>";
@@ -15895,16 +15793,18 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
15895
15793
  }
15896
15794
  function slackPendingInboundPath(channel, threadTs, messageTs) {
15897
15795
  if (!SLACK_PENDING_INBOUND_DIR) return null;
15898
- return join3(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15796
+ return join5(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15899
15797
  }
15900
- function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15798
+ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
15901
15799
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
15902
15800
  if (!path || !SLACK_PENDING_INBOUND_DIR) return;
15903
15801
  const marker = {
15904
15802
  channel,
15905
15803
  thread_ts: threadTs,
15906
15804
  message_ts: messageTs,
15907
- received_at: (/* @__PURE__ */ new Date()).toISOString()
15805
+ received_at: (/* @__PURE__ */ new Date()).toISOString(),
15806
+ // Only persist the flag when set — keeps healthy-path markers byte-identical.
15807
+ ...undeliverable ? { undeliverable: true } : {}
15908
15808
  };
15909
15809
  try {
15910
15810
  mkdirSync3(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
@@ -15916,21 +15816,65 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15916
15816
  );
15917
15817
  }
15918
15818
  }
15919
- function clearAllSlackPendingMarkersForThread(channel, threadTs) {
15920
- if (!SLACK_PENDING_INBOUND_DIR) return;
15921
- const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
15922
- const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
15923
- const prefix = `${safeChan}__${safeThread}__`;
15819
+ function healSlackUndeliverable(channel, messageTs) {
15820
+ if (!BOT_TOKEN || !channel || !messageTs) return;
15821
+ const headers = {
15822
+ "Content-Type": "application/json",
15823
+ Authorization: `Bearer ${BOT_TOKEN}`
15824
+ };
15825
+ fetch("https://slack.com/api/reactions.remove", {
15826
+ method: "POST",
15827
+ headers,
15828
+ body: JSON.stringify({ channel, timestamp: messageTs, name: "x" })
15829
+ }).catch(() => {
15830
+ }).finally(() => {
15831
+ fetch("https://slack.com/api/reactions.add", {
15832
+ method: "POST",
15833
+ headers,
15834
+ body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
15835
+ }).catch(() => {
15836
+ });
15837
+ });
15838
+ }
15839
+ function clearSlackMarkerFileWithHeal(fullPath) {
15840
+ let marker = null;
15924
15841
  try {
15925
- for (const f of readdirSync(SLACK_PENDING_INBOUND_DIR)) {
15926
- if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
15927
- try {
15928
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
15929
- } catch {
15930
- }
15931
- }
15842
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
15932
15843
  } catch {
15933
15844
  }
15845
+ if (marker && decideRecoveryHeal({
15846
+ wasUndeliverable: marker.undeliverable === true,
15847
+ hasTarget: Boolean(marker.channel && marker.message_ts)
15848
+ }) === "heal") {
15849
+ healSlackUndeliverable(marker.channel, marker.message_ts);
15850
+ }
15851
+ try {
15852
+ if (existsSync3(fullPath)) unlinkSync3(fullPath);
15853
+ } catch {
15854
+ }
15855
+ }
15856
+ function clearAllSlackPendingMarkersForThread2(channel, threadTs) {
15857
+ clearAllSlackPendingMarkersForThread(
15858
+ SLACK_PENDING_INBOUND_DIR,
15859
+ channel,
15860
+ threadTs,
15861
+ clearSlackMarkerFileWithHeal
15862
+ );
15863
+ }
15864
+ function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
15865
+ clearSlackPendingMarkerByMessageTs(
15866
+ SLACK_PENDING_INBOUND_DIR,
15867
+ channel,
15868
+ messageTs,
15869
+ clearSlackMarkerFileWithHeal
15870
+ );
15871
+ }
15872
+ function clearOldestSlackPendingMarkerInChannel2(channel) {
15873
+ clearOldestSlackPendingMarkerInChannel(
15874
+ SLACK_PENDING_INBOUND_DIR,
15875
+ channel,
15876
+ clearSlackMarkerFileWithHeal
15877
+ );
15934
15878
  }
15935
15879
  function slackNextRetryName(filename) {
15936
15880
  const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
@@ -15946,10 +15890,10 @@ function slackNextRetryName(filename) {
15946
15890
  async function processSlackRecoveryOutboxFile(filename) {
15947
15891
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
15948
15892
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
15949
- const fullPath = join3(SLACK_RECOVERY_OUTBOX_DIR, filename);
15893
+ const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
15950
15894
  let payload;
15951
15895
  try {
15952
- payload = JSON.parse(readFileSync3(fullPath, "utf-8"));
15896
+ payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
15953
15897
  } catch (err) {
15954
15898
  process.stderr.write(
15955
15899
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
@@ -16015,7 +15959,7 @@ async function processSlackRecoveryOutboxFile(filename) {
16015
15959
  }
16016
15960
  if (sendSucceeded) {
16017
15961
  try {
16018
- unlinkSync2(fullPath);
15962
+ unlinkSync3(fullPath);
16019
15963
  } catch {
16020
15964
  }
16021
15965
  return;
@@ -16023,7 +15967,7 @@ async function processSlackRecoveryOutboxFile(filename) {
16023
15967
  const next = slackNextRetryName(filename);
16024
15968
  if (next) {
16025
15969
  try {
16026
- renameSync2(fullPath, join3(SLACK_RECOVERY_OUTBOX_DIR, next.next));
15970
+ renameSync2(fullPath, join5(SLACK_RECOVERY_OUTBOX_DIR, next.next));
16027
15971
  if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
16028
15972
  process.stderr.write(
16029
15973
  `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -16053,7 +15997,7 @@ function scanSlackRecoveryRetries() {
16053
15997
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
16054
15998
  let entries;
16055
15999
  try {
16056
- entries = readdirSync(SLACK_RECOVERY_OUTBOX_DIR);
16000
+ entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
16057
16001
  } catch {
16058
16002
  return;
16059
16003
  }
@@ -16062,7 +16006,7 @@ function scanSlackRecoveryRetries() {
16062
16006
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
16063
16007
  let mtimeMs;
16064
16008
  try {
16065
- mtimeMs = statSync(join3(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16009
+ mtimeMs = statSync2(join5(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16066
16010
  } catch {
16067
16011
  continue;
16068
16012
  }
@@ -16083,7 +16027,7 @@ function startSlackRecoveryOutboxWatcher() {
16083
16027
  return;
16084
16028
  }
16085
16029
  try {
16086
- for (const f of readdirSync(SLACK_RECOVERY_OUTBOX_DIR)) {
16030
+ for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
16087
16031
  if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
16088
16032
  }
16089
16033
  } catch {
@@ -16092,7 +16036,7 @@ function startSlackRecoveryOutboxWatcher() {
16092
16036
  const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
16093
16037
  if (event !== "rename" || !filename) return;
16094
16038
  if (!isFirstAttemptSlackOutboxFile(filename)) return;
16095
- if (existsSync2(join3(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16039
+ if (existsSync3(join5(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16096
16040
  void processSlackRecoveryOutboxFile(filename);
16097
16041
  }
16098
16042
  });
@@ -16108,15 +16052,15 @@ function startSlackRecoveryOutboxWatcher() {
16108
16052
  }
16109
16053
  startSlackRecoveryOutboxWatcher();
16110
16054
  var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
16111
- function trackPendingMessage(channel, threadTs, messageTs) {
16112
- writeSlackPendingInboundMarker(channel, threadTs, messageTs);
16055
+ function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
16056
+ writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
16113
16057
  }
16114
16058
  function sweepSlackStaleMarkersOnBoot() {
16115
16059
  if (!SLACK_PENDING_INBOUND_DIR) return;
16116
- if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16060
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
16117
16061
  let filenames;
16118
16062
  try {
16119
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
16063
+ filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
16120
16064
  } catch (err) {
16121
16065
  process.stderr.write(
16122
16066
  `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -16129,17 +16073,17 @@ function sweepSlackStaleMarkersOnBoot() {
16129
16073
  for (const filename of filenames) {
16130
16074
  if (!filename.endsWith(".json")) continue;
16131
16075
  if (filename.endsWith(".tmp")) continue;
16132
- const fullPath = join3(SLACK_PENDING_INBOUND_DIR, filename);
16076
+ const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
16133
16077
  let marker;
16134
16078
  try {
16135
- marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
16079
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
16136
16080
  } catch (err) {
16137
16081
  process.stderr.write(
16138
16082
  `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
16139
16083
  `
16140
16084
  );
16141
16085
  try {
16142
- unlinkSync2(fullPath);
16086
+ unlinkSync3(fullPath);
16143
16087
  } catch {
16144
16088
  }
16145
16089
  cleared++;
@@ -16148,7 +16092,7 @@ function sweepSlackStaleMarkersOnBoot() {
16148
16092
  const { channel, thread_ts, message_ts, received_at } = marker;
16149
16093
  if (!channel || !thread_ts || !message_ts || !received_at) {
16150
16094
  try {
16151
- unlinkSync2(fullPath);
16095
+ unlinkSync3(fullPath);
16152
16096
  } catch {
16153
16097
  }
16154
16098
  cleared++;
@@ -16157,7 +16101,7 @@ function sweepSlackStaleMarkersOnBoot() {
16157
16101
  const receivedAtMs = Date.parse(received_at);
16158
16102
  if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
16159
16103
  try {
16160
- unlinkSync2(fullPath);
16104
+ unlinkSync3(fullPath);
16161
16105
  } catch {
16162
16106
  }
16163
16107
  cleared++;
@@ -16171,8 +16115,83 @@ function sweepSlackStaleMarkersOnBoot() {
16171
16115
  }
16172
16116
  }
16173
16117
  sweepSlackStaleMarkersOnBoot();
16118
+ var SLACK_PROCESS_BOOT_MS = Date.now();
16119
+ var STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS = 6e4;
16120
+ var STRANDED_INBOUND_MAX_AGE_MS = 5 * 60 * 1e3;
16121
+ var strandedInboundNoticeFired = false;
16122
+ var strandedInboundNoticeInFlight = false;
16123
+ async function notifyStrandedInboundsOnFirstConnect() {
16124
+ if (strandedInboundNoticeFired || strandedInboundNoticeInFlight) return;
16125
+ strandedInboundNoticeInFlight = true;
16126
+ let hadFailure = false;
16127
+ try {
16128
+ if (!SLACK_PENDING_INBOUND_DIR || !existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
16129
+ let filenames;
16130
+ try {
16131
+ filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
16132
+ } catch {
16133
+ hadFailure = true;
16134
+ return;
16135
+ }
16136
+ const now = Date.now();
16137
+ const seen = /* @__PURE__ */ new Set();
16138
+ let notified = 0;
16139
+ for (const filename of filenames) {
16140
+ if (!filename.endsWith(".json")) continue;
16141
+ const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
16142
+ let marker;
16143
+ try {
16144
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
16145
+ } catch {
16146
+ continue;
16147
+ }
16148
+ const { channel, thread_ts, received_at } = marker;
16149
+ if (!channel || !thread_ts || !received_at) continue;
16150
+ const receivedAtMs = Date.parse(received_at);
16151
+ if (!Number.isFinite(receivedAtMs)) continue;
16152
+ if (receivedAtMs >= SLACK_PROCESS_BOOT_MS - STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS) continue;
16153
+ if (now - receivedAtMs > STRANDED_INBOUND_MAX_AGE_MS) continue;
16154
+ const key2 = `${channel}__${thread_ts}`;
16155
+ if (seen.has(key2)) {
16156
+ try {
16157
+ unlinkSync3(fullPath);
16158
+ } catch {
16159
+ }
16160
+ continue;
16161
+ }
16162
+ seen.add(key2);
16163
+ const res = await postSlackMessage({
16164
+ channel,
16165
+ thread_ts,
16166
+ text: "I was restarted mid-conversation and may have missed your last message. Please re-send if I didn't reply."
16167
+ });
16168
+ if (res.ok) {
16169
+ notified += 1;
16170
+ try {
16171
+ unlinkSync3(fullPath);
16172
+ } catch {
16173
+ }
16174
+ } else {
16175
+ hadFailure = true;
16176
+ process.stderr.write(
16177
+ `slack-channel(${AGENT_CODE_NAME}): stranded-inbound notify failed channel=${redactSlackId(channel)} thread=${redactSlackId(thread_ts)} error=${res.error}
16178
+ `
16179
+ );
16180
+ }
16181
+ }
16182
+ if (notified > 0) {
16183
+ process.stderr.write(
16184
+ `slack-channel(${AGENT_CODE_NAME}): notified ${notified} stranded inbound thread(s) after restart
16185
+ `
16186
+ );
16187
+ }
16188
+ } finally {
16189
+ if (!hadFailure) strandedInboundNoticeFired = true;
16190
+ strandedInboundNoticeInFlight = false;
16191
+ }
16192
+ }
16174
16193
  function clearPendingMessage(channel, threadTs) {
16175
- clearAllSlackPendingMarkersForThread(channel, threadTs);
16194
+ clearAllSlackPendingMarkersForThread2(channel, threadTs);
16176
16195
  }
16177
16196
  function noteThreadActivity(channel, threadTs) {
16178
16197
  if (!channel || !threadTs) return;
@@ -16182,10 +16201,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16182
16201
  if (!channel || !messageTs) return;
16183
16202
  clearPendingMessage(channel, messageTs);
16184
16203
  if (!SLACK_PENDING_INBOUND_DIR) return;
16185
- if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16204
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
16186
16205
  let filenames;
16187
16206
  try {
16188
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
16207
+ filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
16189
16208
  } catch {
16190
16209
  return;
16191
16210
  }
@@ -16196,13 +16215,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16196
16215
  for (const filename of filenames) {
16197
16216
  if (!filename.startsWith(channelPrefix)) continue;
16198
16217
  if (!filename.endsWith(messageSuffix)) continue;
16199
- try {
16200
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
16201
- } catch {
16202
- }
16218
+ clearSlackMarkerFileWithHeal(join5(SLACK_PENDING_INBOUND_DIR, filename));
16203
16219
  }
16204
16220
  }
16205
- var RESTART_FLAGS_DIR = join3(homedir2(), ".augmented", "restart-flags");
16221
+ var RESTART_FLAGS_DIR = join5(homedir2(), ".augmented", "restart-flags");
16206
16222
  function buildAugmentedSlackMetadata() {
16207
16223
  if (!AGT_TEAM_ID) return void 0;
16208
16224
  return {
@@ -16381,10 +16397,10 @@ async function handleSlashCommandEnvelope(payload) {
16381
16397
  return;
16382
16398
  }
16383
16399
  try {
16384
- if (!existsSync2(RESTART_FLAGS_DIR)) {
16400
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16385
16401
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16386
16402
  }
16387
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16403
+ const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
16388
16404
  const flag = {
16389
16405
  codeName,
16390
16406
  source: "slack",
@@ -16394,7 +16410,7 @@ async function handleSlashCommandEnvelope(payload) {
16394
16410
  ...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
16395
16411
  }
16396
16412
  };
16397
- const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
16413
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
16398
16414
  writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
16399
16415
  renameSync2(tmpPath, flagPath);
16400
16416
  process.stderr.write(
@@ -16488,10 +16504,10 @@ async function handleHelpCommand(opts) {
16488
16504
  async function handleRestartCommand(opts) {
16489
16505
  const codeName = AGENT_CODE_NAME ?? "unknown";
16490
16506
  try {
16491
- if (!existsSync2(RESTART_FLAGS_DIR)) {
16507
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16492
16508
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16493
16509
  }
16494
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16510
+ const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
16495
16511
  const flag = {
16496
16512
  codeName,
16497
16513
  source: "slack",
@@ -16502,7 +16518,7 @@ async function handleRestartCommand(opts) {
16502
16518
  message_ts: opts.ts
16503
16519
  }
16504
16520
  };
16505
- const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
16521
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
16506
16522
  writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
16507
16523
  renameSync2(tmpPath, flagPath);
16508
16524
  process.stderr.write(
@@ -16550,7 +16566,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
16550
16566
  var threadPersister = null;
16551
16567
  function resolveThreadStorePath() {
16552
16568
  if (!AGENT_CODE_NAME) return null;
16553
- return join3(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16569
+ return join5(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16554
16570
  }
16555
16571
  function parseTtlDays(raw) {
16556
16572
  if (!raw) return void 0;
@@ -16585,9 +16601,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
16585
16601
  var slackStderrLogStream = null;
16586
16602
  if (AGENT_CODE_NAME) {
16587
16603
  try {
16588
- const logDir = join3(homedir2(), ".augmented", AGENT_CODE_NAME);
16604
+ const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
16589
16605
  mkdirSync3(logDir, { recursive: true });
16590
- slackStderrLogStream = createWriteStream(join3(logDir, "slack-channel-stderr.log"), {
16606
+ slackStderrLogStream = createWriteStream(join5(logDir, "slack-channel-stderr.log"), {
16591
16607
  flags: "a",
16592
16608
  mode: 384
16593
16609
  });
@@ -16657,7 +16673,6 @@ void resolveBotUserIdOrThrow().then((id) => {
16657
16673
  );
16658
16674
  if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
16659
16675
  });
16660
- var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
16661
16676
  var mcp = new Server(
16662
16677
  { name: "slack", version: "0.1.0" },
16663
16678
  {
@@ -16672,43 +16687,18 @@ var mcp = new Server(
16672
16687
  // Highest-priority lines first — Claude Code truncates this string at
16673
16688
  // 2048 chars, so anything appended late silently disappears.
16674
16689
  "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.",
16675
- '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.',
16676
- "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.",
16677
- selfIdentityInstruction,
16690
+ `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.`,
16678
16691
  "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.",
16679
16692
  "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.",
16680
- '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.',
16681
- "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).",
16693
+ '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).',
16694
+ "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.)",
16682
16695
  `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.`,
16683
16696
  "To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
16684
16697
  ].join(" ")
16685
16698
  }
16686
16699
  );
16687
- var progressRegistry = new ProgressToolRegistry({
16688
- prefix: "slack",
16689
- surfaceDescription: "a Slack thread",
16690
- startArgsSchema: {
16691
- properties: {
16692
- channel: {
16693
- type: "string",
16694
- description: "Slack channel id (from the `channel` attribute on the inbound <channel> tag)."
16695
- },
16696
- thread_ts: {
16697
- type: "string",
16698
- 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."
16699
- }
16700
- },
16701
- required: ["channel"]
16702
- },
16703
- createFlush: (args) => createSlackProgressFlush({
16704
- botToken: BOT_TOKEN,
16705
- channel: args.channel,
16706
- threadTs: typeof args.thread_ts === "string" ? args.thread_ts : void 0
16707
- })
16708
- });
16709
16700
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16710
16701
  tools: [
16711
- ...progressRegistry.getDefinitions(),
16712
16702
  {
16713
16703
  name: "slack.reply",
16714
16704
  description: "Send a message back to a Slack channel or thread",
@@ -16722,7 +16712,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16722
16712
  text: { type: "string", description: "The message to send" },
16723
16713
  thread_ts: {
16724
16714
  type: "string",
16725
- description: "Thread timestamp for threaded replies (from the thread_ts attribute)"
16715
+ description: "Thread timestamp for threaded replies (from the thread_ts attribute). Omit for a top-level reply (e.g. a fresh DM)."
16716
+ },
16717
+ // ENG-5861: explicit message_ts so the cleanup gate can match the
16718
+ // exact pending-inbound marker — necessary because top-level DM
16719
+ // replies legitimately have no thread_ts and the marker filename
16720
+ // is `<channel>__<thread_ts>__<message_ts>.json`. Without this
16721
+ // the marker sat undrained, eventually tripping ENG-5832\'s
16722
+ // "wedged" threshold and firing a false-positive red ✗ on the
16723
+ // next inbound.
16724
+ message_ts: {
16725
+ type: "string",
16726
+ 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."
16726
16727
  }
16727
16728
  },
16728
16729
  required: ["channel", "text"]
@@ -16730,7 +16731,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16730
16731
  },
16731
16732
  {
16732
16733
  name: "slack.react",
16733
- 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.",
16734
+ 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.",
16734
16735
  inputSchema: {
16735
16736
  type: "object",
16736
16737
  properties: {
@@ -16743,7 +16744,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16743
16744
  },
16744
16745
  {
16745
16746
  name: "slack.read_thread",
16746
- 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.",
16747
+ 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.",
16747
16748
  inputSchema: {
16748
16749
  type: "object",
16749
16750
  properties: {
@@ -16959,10 +16960,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16959
16960
  if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
16960
16961
  return buildImpersonationRefusal(name);
16961
16962
  }
16962
- const progressResult = await progressRegistry.handle(name, args ?? {});
16963
- if (progressResult !== void 0) return progressResult;
16964
16963
  if (name === "slack.reply") {
16965
- const { channel, text, thread_ts } = args;
16964
+ const { channel, text, thread_ts, message_ts } = args;
16966
16965
  if (channel && thread_ts) {
16967
16966
  const killed = await isThreadKilled({
16968
16967
  channelType: "slack",
@@ -17021,8 +17020,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
17021
17020
  isError: true
17022
17021
  };
17023
17022
  }
17024
- if (channel && thread_ts) {
17025
- clearPendingMessage(channel, thread_ts);
17023
+ if (channel) {
17024
+ if (message_ts) {
17025
+ clearSlackPendingMarkerByMessageTs2(channel, message_ts);
17026
+ if (thread_ts) clearPendingMessage(channel, thread_ts);
17027
+ } else if (thread_ts) {
17028
+ clearPendingMessage(channel, thread_ts);
17029
+ } else {
17030
+ clearOldestSlackPendingMarkerInChannel2(channel);
17031
+ }
17026
17032
  }
17027
17033
  try {
17028
17034
  const res = await fetch("https://slack.com/api/chat.postMessage", {
@@ -17117,46 +17123,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
17117
17123
  const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
17118
17124
  noteThreadActivity(channel, thread_ts);
17119
17125
  try {
17120
- const allMessages = [];
17121
- let cursor;
17122
- do {
17123
- const remaining = limit - allMessages.length;
17124
- if (remaining <= 0) break;
17125
- const params = new URLSearchParams({
17126
- channel,
17127
- ts: thread_ts,
17128
- limit: String(Math.min(remaining, 100)),
17129
- ...cursor ? { cursor } : {}
17130
- });
17131
- const res = await fetch(`https://slack.com/api/conversations.replies?${params}`, {
17132
- headers: { Authorization: `Bearer ${BOT_TOKEN}` }
17133
- });
17134
- const data = await res.json();
17135
- if (!data.ok) {
17136
- return {
17137
- content: [{ type: "text", text: `Slack error: ${data.error}` }],
17138
- isError: true
17139
- };
17140
- }
17141
- for (const msg of data.messages ?? []) {
17142
- allMessages.push({
17143
- user: msg.user ?? msg.bot_id ?? "unknown",
17144
- text: msg.text ?? "",
17145
- ts: msg.ts ?? ""
17146
- });
17147
- }
17148
- cursor = data.response_metadata?.next_cursor || void 0;
17149
- } while (cursor && allMessages.length < limit);
17150
- const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
17151
- const resolved = await Promise.all(uniqueUserIds.map(async (id) => [id, await resolveUserName(id)]));
17152
- const nameById = new Map(resolved);
17153
- const formatted = allMessages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
17126
+ const result = await fetchThreadTranscript(channel, thread_ts, limit, {
17127
+ botToken: BOT_TOKEN,
17128
+ resolveUserName
17129
+ });
17130
+ if (!result.ok) {
17131
+ return {
17132
+ content: [{ type: "text", text: `Slack error: ${result.error}` }],
17133
+ isError: true
17134
+ };
17135
+ }
17154
17136
  return {
17155
17137
  content: [{
17156
17138
  type: "text",
17157
- text: allMessages.length > 0 ? `Thread (${allMessages.length} messages):
17139
+ text: result.count > 0 ? `Thread (${result.count} messages):
17158
17140
 
17159
- ${formatted}` : "Thread is empty or not found."
17141
+ ${result.formatted}` : "Thread is empty or not found."
17160
17142
  }]
17161
17143
  };
17162
17144
  } catch (err) {
@@ -17198,7 +17180,7 @@ ${formatted}` : "Thread is empty or not found."
17198
17180
  let bytes;
17199
17181
  let size;
17200
17182
  try {
17201
- const stat = statSync(resolvedPath);
17183
+ const stat = statSync2(resolvedPath);
17202
17184
  if (!stat.isFile()) {
17203
17185
  return {
17204
17186
  content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
@@ -17206,7 +17188,7 @@ ${formatted}` : "Thread is empty or not found."
17206
17188
  };
17207
17189
  }
17208
17190
  size = stat.size;
17209
- bytes = readFileSync3(resolvedPath);
17191
+ bytes = readFileSync4(resolvedPath);
17210
17192
  } catch (err) {
17211
17193
  return {
17212
17194
  content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
@@ -17505,15 +17487,15 @@ async function handleSendStructured(args) {
17505
17487
  const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
17506
17488
  const { channel, blocks, text, thread_ts, interactive } = args;
17507
17489
  if (typeof channel !== "string" || !channel) {
17508
- return errResult2("channel is required");
17490
+ return errResult("channel is required");
17509
17491
  }
17510
17492
  if (typeof text !== "string" || !text) {
17511
- return errResult2("text is required (used as fallback for push notifications and unfurls)");
17493
+ return errResult("text is required (used as fallback for push notifications and unfurls)");
17512
17494
  }
17513
17495
  noteThreadActivity(channel, thread_ts);
17514
17496
  const validation = validateSlackBlocks2(blocks);
17515
17497
  if (!validation.ok) {
17516
- return errResult2(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17498
+ return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17517
17499
  }
17518
17500
  let effectiveBlocks = blocks;
17519
17501
  let ratingCallbackId = null;
@@ -17521,13 +17503,13 @@ async function handleSendStructured(args) {
17521
17503
  let ratingTokenised = [];
17522
17504
  if (interactive) {
17523
17505
  if (interactive.type !== "rate_run") {
17524
- return errResult2(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
17506
+ return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
17525
17507
  }
17526
17508
  if (typeof interactive.run_id !== "string" || !interactive.run_id) {
17527
- return errResult2("interactive.run_id is required when interactive is set");
17509
+ return errResult("interactive.run_id is required when interactive is set");
17528
17510
  }
17529
17511
  if (!interactiveHostAvailable()) {
17530
- return errResult2(
17512
+ return errResult(
17531
17513
  "interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
17532
17514
  );
17533
17515
  }
@@ -17558,7 +17540,7 @@ async function handleSendStructured(args) {
17558
17540
  effectiveBlocks = [...blocks, ratingBlock];
17559
17541
  const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
17560
17542
  if (!finalValidation.ok) {
17561
- return errResult2(
17543
+ return errResult(
17562
17544
  `Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
17563
17545
  );
17564
17546
  }
@@ -17579,7 +17561,7 @@ async function handleSendStructured(args) {
17579
17561
  kindData: { run_id: interactive.run_id, scale }
17580
17562
  });
17581
17563
  } catch (err) {
17582
- return errResult2(`Failed to register rating row: ${err.message}`);
17564
+ return errResult(`Failed to register rating row: ${err.message}`);
17583
17565
  }
17584
17566
  }
17585
17567
  const result = await postSlackMessageWithTs({
@@ -17589,7 +17571,7 @@ async function handleSendStructured(args) {
17589
17571
  ...thread_ts ? { thread_ts } : {}
17590
17572
  });
17591
17573
  if (!result.ok || !result.ts) {
17592
- return errResult2(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
17574
+ return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
17593
17575
  }
17594
17576
  if (ratingCallbackId) {
17595
17577
  try {
@@ -17644,17 +17626,17 @@ async function handleSendStructured(args) {
17644
17626
  }
17645
17627
  async function handleAskUser(args) {
17646
17628
  if (!askUserToolAvailable()) {
17647
- return errResult2("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
17629
+ return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
17648
17630
  }
17649
17631
  const { randomUUID: rndUUID } = await import("crypto");
17650
17632
  const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
17651
17633
  const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
17652
17634
  const { channel, question, options, timeout_seconds, thread_ts } = args;
17653
- if (typeof channel !== "string" || !channel) return errResult2("channel is required");
17654
- if (typeof question !== "string" || !question) return errResult2("question is required");
17635
+ if (typeof channel !== "string" || !channel) return errResult("channel is required");
17636
+ if (typeof question !== "string" || !question) return errResult("question is required");
17655
17637
  const optsValidation = validateAskUserOptions2(options);
17656
17638
  if (!optsValidation.ok) {
17657
- return errResult2(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17639
+ return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17658
17640
  }
17659
17641
  const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
17660
17642
  const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
@@ -17687,7 +17669,7 @@ async function handleAskUser(args) {
17687
17669
  expiresAt
17688
17670
  });
17689
17671
  } catch (err) {
17690
- return errResult2(`Failed to register pending interaction: ${err.message}`);
17672
+ return errResult(`Failed to register pending interaction: ${err.message}`);
17691
17673
  }
17692
17674
  const slackResult = await postSlackMessageWithTs({
17693
17675
  channel,
@@ -17697,7 +17679,7 @@ async function handleAskUser(args) {
17697
17679
  ...thread_ts ? { thread_ts } : {}
17698
17680
  });
17699
17681
  if (!slackResult.ok || !slackResult.ts) {
17700
- return errResult2(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
17682
+ return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
17701
17683
  }
17702
17684
  try {
17703
17685
  await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
@@ -17744,7 +17726,7 @@ async function handleAskUser(args) {
17744
17726
  }
17745
17727
  async function handleChannelRequestInput(args) {
17746
17728
  if (!askUserToolAvailable()) {
17747
- return errResult2(
17729
+ return errResult(
17748
17730
  "channel_request_input is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring."
17749
17731
  );
17750
17732
  }
@@ -17752,10 +17734,10 @@ async function handleChannelRequestInput(args) {
17752
17734
  const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
17753
17735
  const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
17754
17736
  if (typeof thread_id !== "string" || !thread_id) {
17755
- return errResult2("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
17737
+ return errResult("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
17756
17738
  }
17757
17739
  if (typeof prompt_text !== "string" || !prompt_text) {
17758
- return errResult2("prompt_text is required");
17740
+ return errResult("prompt_text is required");
17759
17741
  }
17760
17742
  const optsValidation = validateAskUserOptions2(options, {
17761
17743
  maxOptions: 4,
@@ -17763,25 +17745,25 @@ async function handleChannelRequestInput(args) {
17763
17745
  maxValueChars: 2e3
17764
17746
  });
17765
17747
  if (!optsValidation.ok) {
17766
- return errResult2(
17748
+ return errResult(
17767
17749
  `Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
17768
17750
  );
17769
17751
  }
17770
17752
  const optionsArr = options;
17771
17753
  if (optionsArr.length < 2) {
17772
- return errResult2("options must contain at least 2 entries (this is a picker, not a notification)");
17754
+ return errResult("options must contain at least 2 entries (this is a picker, not a notification)");
17773
17755
  }
17774
17756
  if (!schema || typeof schema.type !== "string") {
17775
- return errResult2("schema is required with shape { type: 'yes_no' | 'choice' }");
17757
+ return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
17776
17758
  }
17777
17759
  if (schema.type !== "yes_no" && schema.type !== "choice") {
17778
- return errResult2("schema.type must be 'yes_no' or 'choice'");
17760
+ return errResult("schema.type must be 'yes_no' or 'choice'");
17779
17761
  }
17780
17762
  if (schema.type === "yes_no" && optionsArr.length !== 2) {
17781
- return errResult2("schema.type='yes_no' requires exactly 2 options");
17763
+ return errResult("schema.type='yes_no' requires exactly 2 options");
17782
17764
  }
17783
17765
  if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
17784
- return errResult2("request_id must be a non-empty string when provided");
17766
+ return errResult("request_id must be a non-empty string when provided");
17785
17767
  }
17786
17768
  const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
17787
17769
  const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
@@ -17803,10 +17785,10 @@ async function handleChannelRequestInput(args) {
17803
17785
  dispatchPayload = await res.json().catch(() => ({}));
17804
17786
  if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
17805
17787
  const reason = dispatchPayload.reason ?? `http_${res.status}`;
17806
- return errResult2(`channel_request_input dispatch failed: ${reason}`);
17788
+ return errResult(`channel_request_input dispatch failed: ${reason}`);
17807
17789
  }
17808
17790
  } catch (err) {
17809
- return errResult2(
17791
+ return errResult(
17810
17792
  `channel_request_input dispatch failed: ${err.message}`
17811
17793
  );
17812
17794
  }
@@ -17844,7 +17826,7 @@ async function handleChannelRequestInput(args) {
17844
17826
  ]
17845
17827
  };
17846
17828
  }
17847
- function errResult2(text) {
17829
+ function errResult(text) {
17848
17830
  return { content: [{ type: "text", text }], isError: true };
17849
17831
  }
17850
17832
  var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
@@ -18038,6 +18020,7 @@ async function connectSocketMode() {
18038
18020
  process.stderr.write("slack-channel: Socket Mode connected\n");
18039
18021
  recordActivity("connect");
18040
18022
  void setBotStatus(":large_green_circle:", "Active");
18023
+ void notifyStrandedInboundsOnFirstConnect();
18041
18024
  };
18042
18025
  ws.onmessage = async (event) => {
18043
18026
  try {
@@ -18099,8 +18082,27 @@ async function connectSocketMode() {
18099
18082
  }
18100
18083
  const senderPolicyDecision = decideSenderPolicyForward(evt, SLACK_SENDER_POLICY);
18101
18084
  if (!senderPolicyDecision.forward) {
18102
- process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
18085
+ const subReason = classifySlackPolicyBlock(evt, SLACK_SENDER_POLICY);
18086
+ process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, sub=${subReason}, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
18103
18087
  `);
18088
+ await maybeSendSenderPolicyDecline({
18089
+ channel: evt.channel,
18090
+ senderId: evt.user,
18091
+ // CR on PR #1623: top-level posts (DMs, channel root messages)
18092
+ // arrive with no `thread_ts` and MUST decline inline, not in a
18093
+ // new thread off the inbound message. The `?? evt.ts` fallback
18094
+ // forced every root message into a thread reply — exactly the
18095
+ // case the helper's "omit thread_ts when undefined" branch was
18096
+ // meant to handle. Pass through as-is: in-thread for thread
18097
+ // replies (thread_ts present), inline for everything else.
18098
+ threadTs: evt.thread_ts,
18099
+ subReason
18100
+ }).catch((err) => {
18101
+ process.stderr.write(
18102
+ `slack-channel(${AGENT_CODE_NAME}): decline reply failed: ${err.message}
18103
+ `
18104
+ );
18105
+ });
18104
18106
  return;
18105
18107
  }
18106
18108
  recordActivity("inbound");
@@ -18226,19 +18228,38 @@ async function connectSocketMode() {
18226
18228
  isAutoFollowed,
18227
18229
  botUserId
18228
18230
  });
18229
- if (decideSlackAckReaction({ channel, ts })) {
18231
+ const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
18232
+ let paneLogFreshAgeMs = null;
18233
+ if (SLACK_AGENT_DIR) {
18234
+ try {
18235
+ const paneMtimeMs = statSync2(join5(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
18236
+ paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
18237
+ } catch {
18238
+ }
18239
+ }
18240
+ const ackDecision = decideAckReaction({
18241
+ hasTarget: decideSlackAckReaction({ channel, ts }),
18242
+ integrationReady: Boolean(BOT_TOKEN),
18243
+ tmux: ackProbe.tmux,
18244
+ claude: ackProbe.claude,
18245
+ withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
18246
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR),
18247
+ paneLogFreshAgeMs
18248
+ });
18249
+ if (ackDecision !== "none") {
18250
+ const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
18230
18251
  fetch("https://slack.com/api/reactions.add", {
18231
18252
  method: "POST",
18232
18253
  headers: {
18233
18254
  "Content-Type": "application/json",
18234
18255
  Authorization: `Bearer ${BOT_TOKEN}`
18235
18256
  },
18236
- body: JSON.stringify({ channel, timestamp: ts, name: "eyes" })
18257
+ body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
18237
18258
  }).catch(() => {
18238
18259
  });
18239
18260
  }
18240
18261
  if (channel && ts && shouldEngage) {
18241
- trackPendingMessage(channel, threadTs, ts);
18262
+ trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
18242
18263
  }
18243
18264
  const userName = await resolveUserName(user);
18244
18265
  const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
@@ -18246,6 +18267,29 @@ async function connectSocketMode() {
18246
18267
  (f) => f.kind === "image" && typeof f.path === "string"
18247
18268
  );
18248
18269
  const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
18270
+ let threadContext;
18271
+ if (isThreadReply && channel && threadTs) {
18272
+ try {
18273
+ const transcript = await fetchThreadTranscript(
18274
+ channel,
18275
+ threadTs,
18276
+ SLACK_AUTOLOAD_THREAD_LIMIT,
18277
+ { botToken: BOT_TOKEN, resolveUserName }
18278
+ );
18279
+ if (transcript.ok && transcript.count >= 2) {
18280
+ threadContext = capThreadContext(
18281
+ transcript.formatted,
18282
+ SLACK_AUTOLOAD_THREAD_MAX_CHARS
18283
+ );
18284
+ }
18285
+ } catch (err) {
18286
+ const msg2 = err instanceof Error ? err.message : String(err);
18287
+ process.stderr.write(
18288
+ `slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
18289
+ `
18290
+ );
18291
+ }
18292
+ }
18249
18293
  await mcp.notification({
18250
18294
  method: "notifications/claude/channel",
18251
18295
  params: {
@@ -18255,12 +18299,21 @@ async function connectSocketMode() {
18255
18299
  user_name: userName,
18256
18300
  channel,
18257
18301
  thread_ts: threadTs,
18302
+ // ENG-5861: explicit message_ts so the agent can pass it back
18303
+ // to slack.reply for reliable pending-inbound cleanup. For
18304
+ // top-level messages threadTs === ts; for threaded replies
18305
+ // threadTs is the thread root and message_ts is the specific
18306
+ // reply being addressed — different keys, both needed by the
18307
+ // marker-cleanup gate.
18308
+ message_ts: ts,
18258
18309
  event_type: evt.type,
18259
18310
  ...isAutoFollowed ? { auto_followed: "true" } : {},
18260
18311
  // Only set these when we actually have attachments to avoid
18261
18312
  // bloating every notification with empty metadata.
18262
18313
  ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
18263
- ...imagePath ? { image_path: imagePath } : {}
18314
+ ...imagePath ? { image_path: imagePath } : {},
18315
+ // ENG-5830: the pre-loaded surrounding thread (thread replies only).
18316
+ ...threadContext ? { thread_context: threadContext } : {}
18264
18317
  }
18265
18318
  }
18266
18319
  });