@integrity-labs/agt-cli 0.27.13 → 0.27.15

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.
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-ICYFLUAM.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-SBSOZG74.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-ZBQKBBMT.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-OBAJZDXK.js.map
@@ -15,7 +15,7 @@ import {
15
15
  provisionOrientHook,
16
16
  provisionStopHook,
17
17
  requireHost
18
- } from "../chunk-Q4MWFZ5Y.js";
18
+ } from "../chunk-LJEV2QHN.js";
19
19
  import {
20
20
  getProjectDir as getProjectDir2,
21
21
  getReadyTasks,
@@ -46,7 +46,7 @@ import {
46
46
  stopAllSessionsAndWait,
47
47
  stopPersistentSession,
48
48
  takeZombieDetection
49
- } from "../chunk-GN4XPQWJ.js";
49
+ } from "../chunk-F4NG4EXD.js";
50
50
  import {
51
51
  KANBAN_CHECK_COMMAND,
52
52
  appendDmFooter,
@@ -69,7 +69,7 @@ import {
69
69
  resolveConnectivityProbe,
70
70
  resolveDmTarget,
71
71
  wrapScheduledTaskPrompt
72
- } from "../chunk-YSBGIXJG.js";
72
+ } from "../chunk-HT6EETEL.js";
73
73
  import {
74
74
  parsePsRows,
75
75
  reapOrphanChannelMcps
@@ -3166,7 +3166,7 @@ var cachedFrameworkVersion = null;
3166
3166
  var lastVersionCheckAt = 0;
3167
3167
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3168
3168
  var lastResponsivenessProbeAt = 0;
3169
- var agtCliVersion = true ? "0.27.13" : "dev";
3169
+ var agtCliVersion = true ? "0.27.15" : "dev";
3170
3170
  function resolveBrewPath(execFileSync4) {
3171
3171
  try {
3172
3172
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -4180,7 +4180,7 @@ async function pollCycle() {
4180
4180
  }
4181
4181
  try {
4182
4182
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
4183
- const { collectDiagnostics } = await import("../persistent-session-ICYFLUAM.js");
4183
+ const { collectDiagnostics } = await import("../persistent-session-SBSOZG74.js");
4184
4184
  const diagCodeNames = [...agentState.persistentSessionAgents];
4185
4185
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
4186
4186
  let tailscaleHostname;
@@ -4248,7 +4248,7 @@ async function pollCycle() {
4248
4248
  const {
4249
4249
  collectResponsivenessProbes,
4250
4250
  getResponsivenessIntervalMs
4251
- } = await import("../responsiveness-probe-WZNQ2762.js");
4251
+ } = await import("../responsiveness-probe-DU4IJ2RZ.js");
4252
4252
  const probeIntervalMs = getResponsivenessIntervalMs();
4253
4253
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
4254
4254
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -8196,7 +8196,7 @@ async function processClaudePairSessions(agents) {
8196
8196
  killPairSession,
8197
8197
  pairTmuxSession,
8198
8198
  finalizeClaudePairOnboarding
8199
- } = await import("../claude-pair-runtime-ZBQKBBMT.js");
8199
+ } = await import("../claude-pair-runtime-OBAJZDXK.js");
8200
8200
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
8201
8201
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
8202
8202
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -14249,6 +14249,102 @@ function decideSenderPolicyForward(evt, policy) {
14249
14249
  return { forward: true };
14250
14250
  }
14251
14251
 
14252
+ // src/ack-reaction.ts
14253
+ import { readdirSync, readFileSync } from "fs";
14254
+ import { join } from "path";
14255
+ var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
14256
+ var ACK_STARTUP_GRACE_MS = 6e4;
14257
+ function decideAckReaction(i) {
14258
+ if (!i.hasTarget) return "none";
14259
+ if (!i.integrationReady) return "undeliverable";
14260
+ if (i.tmux === "dead") return "undeliverable";
14261
+ if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
14262
+ const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
14263
+ if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
14264
+ return "undeliverable";
14265
+ }
14266
+ return "ack";
14267
+ }
14268
+ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
14269
+ if (!dir) return null;
14270
+ let names;
14271
+ try {
14272
+ names = readdirSync(dir);
14273
+ } catch {
14274
+ return null;
14275
+ }
14276
+ let oldest = null;
14277
+ for (const name of names) {
14278
+ if (!name.endsWith(".json")) continue;
14279
+ let receivedAt;
14280
+ try {
14281
+ const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
14282
+ receivedAt = raw.received_at;
14283
+ } catch {
14284
+ continue;
14285
+ }
14286
+ if (!receivedAt) continue;
14287
+ const t = Date.parse(receivedAt);
14288
+ if (Number.isNaN(t)) continue;
14289
+ const age = now - t;
14290
+ if (age < 0) continue;
14291
+ if (oldest == null || age > oldest) oldest = age;
14292
+ }
14293
+ return oldest;
14294
+ }
14295
+
14296
+ // src/session-probe-runtime.ts
14297
+ import { execFileSync } from "child_process";
14298
+ function agentTmuxSessionName(codeName) {
14299
+ return `agt-${codeName}`;
14300
+ }
14301
+ function escapePgrepRegex(value) {
14302
+ return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
14303
+ }
14304
+ function probeClaudeProcessInTmux(tmuxSession) {
14305
+ const escapedSession = escapePgrepRegex(tmuxSession);
14306
+ const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
14307
+ try {
14308
+ const out = execFileSync("pgrep", ["-f", "--", pattern], {
14309
+ encoding: "utf-8",
14310
+ timeout: 3e3
14311
+ }).trim();
14312
+ return out.length > 0 ? "alive" : "dead";
14313
+ } catch (err) {
14314
+ const e = err;
14315
+ if (e?.code === "ENOENT") return "unknown";
14316
+ return e?.status === 1 ? "dead" : "unknown";
14317
+ }
14318
+ }
14319
+ function probeTmuxSession(tmuxSession) {
14320
+ try {
14321
+ execFileSync("tmux", ["has-session", "-t", tmuxSession], {
14322
+ stdio: "ignore",
14323
+ timeout: 3e3
14324
+ });
14325
+ return "alive";
14326
+ } catch (err) {
14327
+ const e = err;
14328
+ if (e?.code === "ENOENT") return "unknown";
14329
+ return "dead";
14330
+ }
14331
+ }
14332
+ function probeAgentSession(codeName) {
14333
+ const session = agentTmuxSessionName(codeName);
14334
+ const tmux = probeTmuxSession(session);
14335
+ const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
14336
+ return { tmux, claude };
14337
+ }
14338
+ var probeCache = /* @__PURE__ */ new Map();
14339
+ var SESSION_PROBE_TTL_MS = 15e3;
14340
+ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
14341
+ const cached2 = probeCache.get(codeName);
14342
+ if (cached2 && now - cached2.at < ttlMs) return cached2.value;
14343
+ const value = probeAgentSession(codeName);
14344
+ probeCache.set(codeName, { at: now, value });
14345
+ return value;
14346
+ }
14347
+
14252
14348
  // src/slack-loop-throttle.ts
14253
14349
  var DEFAULT_THROTTLE = {
14254
14350
  threshold: 3,
@@ -14813,6 +14909,64 @@ function isMode(value) {
14813
14909
  return value === "thinking" || value === "working" || value === "waiting";
14814
14910
  }
14815
14911
 
14912
+ // src/slack-thread-context.ts
14913
+ var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
14914
+ var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
14915
+ function formatThreadMessages(messages, nameById) {
14916
+ return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
14917
+ }
14918
+ function capThreadContext(formatted, maxChars) {
14919
+ if (formatted.length <= maxChars) return formatted;
14920
+ const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
14921
+ if (maxChars <= marker.length) return marker.slice(0, maxChars);
14922
+ const tailBudget = maxChars - marker.length;
14923
+ const tail = formatted.slice(formatted.length - tailBudget);
14924
+ const firstNewline = tail.indexOf("\n");
14925
+ const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
14926
+ return `${marker}${clean}`;
14927
+ }
14928
+ async function fetchThreadTranscript(channel, threadTs, limit, deps) {
14929
+ const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
14930
+ const cappedLimit = Math.min(Math.max(limit, 1), 200);
14931
+ const allMessages = [];
14932
+ let cursor;
14933
+ do {
14934
+ const remaining = cappedLimit - allMessages.length;
14935
+ if (remaining <= 0) break;
14936
+ const params = new URLSearchParams({
14937
+ channel,
14938
+ ts: threadTs,
14939
+ limit: String(Math.min(remaining, 100)),
14940
+ ...cursor ? { cursor } : {}
14941
+ });
14942
+ let data;
14943
+ try {
14944
+ const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
14945
+ headers: { Authorization: `Bearer ${botToken}` }
14946
+ });
14947
+ if (!res.ok) return { ok: false, error: `http_${res.status}` };
14948
+ data = await res.json();
14949
+ } catch (err) {
14950
+ return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
14951
+ }
14952
+ if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
14953
+ for (const msg of data.messages ?? []) {
14954
+ allMessages.push({
14955
+ user: msg.user ?? msg.bot_id ?? "unknown",
14956
+ text: msg.text ?? "",
14957
+ ts: msg.ts ?? ""
14958
+ });
14959
+ }
14960
+ cursor = data.response_metadata?.next_cursor || void 0;
14961
+ } while (cursor && allMessages.length < cappedLimit);
14962
+ const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
14963
+ const resolved = await Promise.all(
14964
+ uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
14965
+ );
14966
+ const nameById = new Map(resolved);
14967
+ return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
14968
+ }
14969
+
14816
14970
  // src/impersonation.ts
14817
14971
  var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
14818
14972
  var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
@@ -14958,20 +15112,20 @@ import {
14958
15112
  createWriteStream,
14959
15113
  existsSync as existsSync2,
14960
15114
  mkdirSync as mkdirSync3,
14961
- readFileSync as readFileSync3,
14962
- readdirSync,
15115
+ readFileSync as readFileSync4,
15116
+ readdirSync as readdirSync2,
14963
15117
  renameSync as renameSync2,
14964
15118
  statSync,
14965
15119
  unlinkSync as unlinkSync2,
14966
15120
  watch,
14967
15121
  writeFileSync as writeFileSync3
14968
15122
  } from "fs";
14969
- import { basename, join as join3, resolve as resolve2 } from "path";
15123
+ import { basename, join as join4, resolve as resolve2 } from "path";
14970
15124
  import { homedir as homedir2 } from "os";
14971
15125
  import { createHash, randomUUID as randomUUID2 } from "crypto";
14972
15126
 
14973
15127
  // src/slack-thread-store.ts
14974
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
15128
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
14975
15129
  import { dirname } from "path";
14976
15130
  var FILE_VERSION = 1;
14977
15131
  var DEFAULT_TTL_DAYS = 30;
@@ -14982,7 +15136,7 @@ function loadThreadStore(filePath, opts = {}) {
14982
15136
  const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
14983
15137
  let raw;
14984
15138
  try {
14985
- raw = readFileSync(filePath, "utf-8");
15139
+ raw = readFileSync2(filePath, "utf-8");
14986
15140
  } catch {
14987
15141
  return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
14988
15142
  }
@@ -15097,9 +15251,9 @@ async function runOrRetry(fn, opts) {
15097
15251
 
15098
15252
  // src/channel-attachments.ts
15099
15253
  import { homedir } from "os";
15100
- import { join, resolve, sep } from "path";
15254
+ import { join as join2, resolve, sep } from "path";
15101
15255
  function resolveChannelInboundDir(codeName, channelSlug) {
15102
- const base = join(homedir(), ".augmented");
15256
+ const base = join2(homedir(), ".augmented");
15103
15257
  const allowedSegment = /^[A-Za-z0-9_-]+$/;
15104
15258
  if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
15105
15259
  throw new Error(
@@ -15745,12 +15899,12 @@ function createSlackBotUserIdClient(args) {
15745
15899
  import {
15746
15900
  existsSync,
15747
15901
  mkdirSync as mkdirSync2,
15748
- readFileSync as readFileSync2,
15902
+ readFileSync as readFileSync3,
15749
15903
  renameSync,
15750
15904
  unlinkSync,
15751
15905
  writeFileSync as writeFileSync2
15752
15906
  } from "fs";
15753
- import { join as join2 } from "path";
15907
+ import { join as join3 } from "path";
15754
15908
  function defaultIsPidAlive(pid) {
15755
15909
  if (!Number.isFinite(pid) || pid <= 0) return false;
15756
15910
  try {
@@ -15768,7 +15922,7 @@ function acquireMcpSpawnLock(args) {
15768
15922
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15769
15923
  const selfPid = options.selfPid ?? process.pid;
15770
15924
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15771
- const path = join2(agentDir, basename2);
15925
+ const path = join3(agentDir, basename2);
15772
15926
  const existing = readLockHolder(path);
15773
15927
  if (existing) {
15774
15928
  if (existing.pid === selfPid) {
@@ -15799,7 +15953,7 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
15799
15953
  function readLockHolder(path) {
15800
15954
  if (!existsSync(path)) return null;
15801
15955
  try {
15802
- const raw = readFileSync2(path, "utf8");
15956
+ const raw = readFileSync3(path, "utf8");
15803
15957
  const parsed = JSON.parse(raw);
15804
15958
  const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
15805
15959
  if (!Number.isFinite(pid) || pid <= 0) return null;
@@ -15881,9 +16035,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15881
16035
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15882
16036
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15883
16037
  };
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;
16038
+ var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
16039
+ var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
16040
+ var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15887
16041
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
15888
16042
  function redactSlackId(id) {
15889
16043
  if (!id) return "<none>";
@@ -15895,7 +16049,7 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
15895
16049
  }
15896
16050
  function slackPendingInboundPath(channel, threadTs, messageTs) {
15897
16051
  if (!SLACK_PENDING_INBOUND_DIR) return null;
15898
- return join3(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
16052
+ return join4(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15899
16053
  }
15900
16054
  function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15901
16055
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
@@ -15922,10 +16076,10 @@ function clearAllSlackPendingMarkersForThread(channel, threadTs) {
15922
16076
  const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
15923
16077
  const prefix = `${safeChan}__${safeThread}__`;
15924
16078
  try {
15925
- for (const f of readdirSync(SLACK_PENDING_INBOUND_DIR)) {
16079
+ for (const f of readdirSync2(SLACK_PENDING_INBOUND_DIR)) {
15926
16080
  if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
15927
16081
  try {
15928
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
16082
+ unlinkSync2(join4(SLACK_PENDING_INBOUND_DIR, f));
15929
16083
  } catch {
15930
16084
  }
15931
16085
  }
@@ -15946,10 +16100,10 @@ function slackNextRetryName(filename) {
15946
16100
  async function processSlackRecoveryOutboxFile(filename) {
15947
16101
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
15948
16102
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
15949
- const fullPath = join3(SLACK_RECOVERY_OUTBOX_DIR, filename);
16103
+ const fullPath = join4(SLACK_RECOVERY_OUTBOX_DIR, filename);
15950
16104
  let payload;
15951
16105
  try {
15952
- payload = JSON.parse(readFileSync3(fullPath, "utf-8"));
16106
+ payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
15953
16107
  } catch (err) {
15954
16108
  process.stderr.write(
15955
16109
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
@@ -16023,7 +16177,7 @@ async function processSlackRecoveryOutboxFile(filename) {
16023
16177
  const next = slackNextRetryName(filename);
16024
16178
  if (next) {
16025
16179
  try {
16026
- renameSync2(fullPath, join3(SLACK_RECOVERY_OUTBOX_DIR, next.next));
16180
+ renameSync2(fullPath, join4(SLACK_RECOVERY_OUTBOX_DIR, next.next));
16027
16181
  if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
16028
16182
  process.stderr.write(
16029
16183
  `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -16053,7 +16207,7 @@ function scanSlackRecoveryRetries() {
16053
16207
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
16054
16208
  let entries;
16055
16209
  try {
16056
- entries = readdirSync(SLACK_RECOVERY_OUTBOX_DIR);
16210
+ entries = readdirSync2(SLACK_RECOVERY_OUTBOX_DIR);
16057
16211
  } catch {
16058
16212
  return;
16059
16213
  }
@@ -16062,7 +16216,7 @@ function scanSlackRecoveryRetries() {
16062
16216
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
16063
16217
  let mtimeMs;
16064
16218
  try {
16065
- mtimeMs = statSync(join3(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16219
+ mtimeMs = statSync(join4(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16066
16220
  } catch {
16067
16221
  continue;
16068
16222
  }
@@ -16083,7 +16237,7 @@ function startSlackRecoveryOutboxWatcher() {
16083
16237
  return;
16084
16238
  }
16085
16239
  try {
16086
- for (const f of readdirSync(SLACK_RECOVERY_OUTBOX_DIR)) {
16240
+ for (const f of readdirSync2(SLACK_RECOVERY_OUTBOX_DIR)) {
16087
16241
  if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
16088
16242
  }
16089
16243
  } catch {
@@ -16092,7 +16246,7 @@ function startSlackRecoveryOutboxWatcher() {
16092
16246
  const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
16093
16247
  if (event !== "rename" || !filename) return;
16094
16248
  if (!isFirstAttemptSlackOutboxFile(filename)) return;
16095
- if (existsSync2(join3(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16249
+ if (existsSync2(join4(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16096
16250
  void processSlackRecoveryOutboxFile(filename);
16097
16251
  }
16098
16252
  });
@@ -16116,7 +16270,7 @@ function sweepSlackStaleMarkersOnBoot() {
16116
16270
  if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16117
16271
  let filenames;
16118
16272
  try {
16119
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
16273
+ filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16120
16274
  } catch (err) {
16121
16275
  process.stderr.write(
16122
16276
  `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -16129,10 +16283,10 @@ function sweepSlackStaleMarkersOnBoot() {
16129
16283
  for (const filename of filenames) {
16130
16284
  if (!filename.endsWith(".json")) continue;
16131
16285
  if (filename.endsWith(".tmp")) continue;
16132
- const fullPath = join3(SLACK_PENDING_INBOUND_DIR, filename);
16286
+ const fullPath = join4(SLACK_PENDING_INBOUND_DIR, filename);
16133
16287
  let marker;
16134
16288
  try {
16135
- marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
16289
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
16136
16290
  } catch (err) {
16137
16291
  process.stderr.write(
16138
16292
  `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
@@ -16185,7 +16339,7 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16185
16339
  if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16186
16340
  let filenames;
16187
16341
  try {
16188
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
16342
+ filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16189
16343
  } catch {
16190
16344
  return;
16191
16345
  }
@@ -16197,12 +16351,12 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16197
16351
  if (!filename.startsWith(channelPrefix)) continue;
16198
16352
  if (!filename.endsWith(messageSuffix)) continue;
16199
16353
  try {
16200
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
16354
+ unlinkSync2(join4(SLACK_PENDING_INBOUND_DIR, filename));
16201
16355
  } catch {
16202
16356
  }
16203
16357
  }
16204
16358
  }
16205
- var RESTART_FLAGS_DIR = join3(homedir2(), ".augmented", "restart-flags");
16359
+ var RESTART_FLAGS_DIR = join4(homedir2(), ".augmented", "restart-flags");
16206
16360
  function buildAugmentedSlackMetadata() {
16207
16361
  if (!AGT_TEAM_ID) return void 0;
16208
16362
  return {
@@ -16384,7 +16538,7 @@ async function handleSlashCommandEnvelope(payload) {
16384
16538
  if (!existsSync2(RESTART_FLAGS_DIR)) {
16385
16539
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16386
16540
  }
16387
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16541
+ const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16388
16542
  const flag = {
16389
16543
  codeName,
16390
16544
  source: "slack",
@@ -16491,7 +16645,7 @@ async function handleRestartCommand(opts) {
16491
16645
  if (!existsSync2(RESTART_FLAGS_DIR)) {
16492
16646
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16493
16647
  }
16494
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16648
+ const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16495
16649
  const flag = {
16496
16650
  codeName,
16497
16651
  source: "slack",
@@ -16550,7 +16704,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
16550
16704
  var threadPersister = null;
16551
16705
  function resolveThreadStorePath() {
16552
16706
  if (!AGENT_CODE_NAME) return null;
16553
- return join3(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16707
+ return join4(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16554
16708
  }
16555
16709
  function parseTtlDays(raw) {
16556
16710
  if (!raw) return void 0;
@@ -16585,9 +16739,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
16585
16739
  var slackStderrLogStream = null;
16586
16740
  if (AGENT_CODE_NAME) {
16587
16741
  try {
16588
- const logDir = join3(homedir2(), ".augmented", AGENT_CODE_NAME);
16742
+ const logDir = join4(homedir2(), ".augmented", AGENT_CODE_NAME);
16589
16743
  mkdirSync3(logDir, { recursive: true });
16590
- slackStderrLogStream = createWriteStream(join3(logDir, "slack-channel-stderr.log"), {
16744
+ slackStderrLogStream = createWriteStream(join4(logDir, "slack-channel-stderr.log"), {
16591
16745
  flags: "a",
16592
16746
  mode: 384
16593
16747
  });
@@ -16657,7 +16811,6 @@ void resolveBotUserIdOrThrow().then((id) => {
16657
16811
  );
16658
16812
  if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
16659
16813
  });
16660
- var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
16661
16814
  var mcp = new Server(
16662
16815
  { name: "slack", version: "0.1.0" },
16663
16816
  {
@@ -16672,13 +16825,12 @@ var mcp = new Server(
16672
16825
  // Highest-priority lines first — Claude Code truncates this string at
16673
16826
  // 2048 chars, so anything appended late silently disappears.
16674
16827
  "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.',
16828
+ `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.`,
16676
16829
  "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,
16678
16830
  "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
16831
  "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).",
16832
+ '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).',
16833
+ "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
16834
  `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
16835
  "To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
16684
16836
  ].join(" ")
@@ -16730,7 +16882,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16730
16882
  },
16731
16883
  {
16732
16884
  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.",
16885
+ 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
16886
  inputSchema: {
16735
16887
  type: "object",
16736
16888
  properties: {
@@ -16743,7 +16895,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16743
16895
  },
16744
16896
  {
16745
16897
  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.",
16898
+ 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
16899
  inputSchema: {
16748
16900
  type: "object",
16749
16901
  properties: {
@@ -17117,46 +17269,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
17117
17269
  const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
17118
17270
  noteThreadActivity(channel, thread_ts);
17119
17271
  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");
17272
+ const result = await fetchThreadTranscript(channel, thread_ts, limit, {
17273
+ botToken: BOT_TOKEN,
17274
+ resolveUserName
17275
+ });
17276
+ if (!result.ok) {
17277
+ return {
17278
+ content: [{ type: "text", text: `Slack error: ${result.error}` }],
17279
+ isError: true
17280
+ };
17281
+ }
17154
17282
  return {
17155
17283
  content: [{
17156
17284
  type: "text",
17157
- text: allMessages.length > 0 ? `Thread (${allMessages.length} messages):
17285
+ text: result.count > 0 ? `Thread (${result.count} messages):
17158
17286
 
17159
- ${formatted}` : "Thread is empty or not found."
17287
+ ${result.formatted}` : "Thread is empty or not found."
17160
17288
  }]
17161
17289
  };
17162
17290
  } catch (err) {
@@ -17206,7 +17334,7 @@ ${formatted}` : "Thread is empty or not found."
17206
17334
  };
17207
17335
  }
17208
17336
  size = stat.size;
17209
- bytes = readFileSync3(resolvedPath);
17337
+ bytes = readFileSync4(resolvedPath);
17210
17338
  } catch (err) {
17211
17339
  return {
17212
17340
  content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
@@ -18226,14 +18354,24 @@ async function connectSocketMode() {
18226
18354
  isAutoFollowed,
18227
18355
  botUserId
18228
18356
  });
18229
- if (decideSlackAckReaction({ channel, ts })) {
18357
+ const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
18358
+ const ackDecision = decideAckReaction({
18359
+ hasTarget: decideSlackAckReaction({ channel, ts }),
18360
+ integrationReady: Boolean(BOT_TOKEN),
18361
+ tmux: ackProbe.tmux,
18362
+ claude: ackProbe.claude,
18363
+ withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
18364
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR)
18365
+ });
18366
+ if (ackDecision !== "none") {
18367
+ const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
18230
18368
  fetch("https://slack.com/api/reactions.add", {
18231
18369
  method: "POST",
18232
18370
  headers: {
18233
18371
  "Content-Type": "application/json",
18234
18372
  Authorization: `Bearer ${BOT_TOKEN}`
18235
18373
  },
18236
- body: JSON.stringify({ channel, timestamp: ts, name: "eyes" })
18374
+ body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
18237
18375
  }).catch(() => {
18238
18376
  });
18239
18377
  }
@@ -18246,6 +18384,29 @@ async function connectSocketMode() {
18246
18384
  (f) => f.kind === "image" && typeof f.path === "string"
18247
18385
  );
18248
18386
  const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
18387
+ let threadContext;
18388
+ if (isThreadReply && channel && threadTs) {
18389
+ try {
18390
+ const transcript = await fetchThreadTranscript(
18391
+ channel,
18392
+ threadTs,
18393
+ SLACK_AUTOLOAD_THREAD_LIMIT,
18394
+ { botToken: BOT_TOKEN, resolveUserName }
18395
+ );
18396
+ if (transcript.ok && transcript.count >= 2) {
18397
+ threadContext = capThreadContext(
18398
+ transcript.formatted,
18399
+ SLACK_AUTOLOAD_THREAD_MAX_CHARS
18400
+ );
18401
+ }
18402
+ } catch (err) {
18403
+ const msg2 = err instanceof Error ? err.message : String(err);
18404
+ process.stderr.write(
18405
+ `slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
18406
+ `
18407
+ );
18408
+ }
18409
+ }
18249
18410
  await mcp.notification({
18250
18411
  method: "notifications/claude/channel",
18251
18412
  params: {
@@ -18260,7 +18421,9 @@ async function connectSocketMode() {
18260
18421
  // Only set these when we actually have attachments to avoid
18261
18422
  // bloating every notification with empty metadata.
18262
18423
  ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
18263
- ...imagePath ? { image_path: imagePath } : {}
18424
+ ...imagePath ? { image_path: imagePath } : {},
18425
+ // ENG-5830: the pre-loaded surrounding thread (thread replies only).
18426
+ ...threadContext ? { thread_context: threadContext } : {}
18264
18427
  }
18265
18428
  }
18266
18429
  });