@integrity-labs/agt-cli 0.27.7-test.6 → 0.27.8-test.8

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.
@@ -14249,6 +14249,105 @@ 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 decideRecoveryHeal(i) {
14269
+ return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
14270
+ }
14271
+ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
14272
+ if (!dir) return null;
14273
+ let names;
14274
+ try {
14275
+ names = readdirSync(dir);
14276
+ } catch {
14277
+ return null;
14278
+ }
14279
+ let oldest = null;
14280
+ for (const name of names) {
14281
+ if (!name.endsWith(".json")) continue;
14282
+ let receivedAt;
14283
+ try {
14284
+ const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
14285
+ receivedAt = raw.received_at;
14286
+ } catch {
14287
+ continue;
14288
+ }
14289
+ if (!receivedAt) continue;
14290
+ const t = Date.parse(receivedAt);
14291
+ if (Number.isNaN(t)) continue;
14292
+ const age = now - t;
14293
+ if (age < 0) continue;
14294
+ if (oldest == null || age > oldest) oldest = age;
14295
+ }
14296
+ return oldest;
14297
+ }
14298
+
14299
+ // src/session-probe-runtime.ts
14300
+ import { execFileSync } from "child_process";
14301
+ function agentTmuxSessionName(codeName) {
14302
+ return `agt-${codeName}`;
14303
+ }
14304
+ function escapePgrepRegex(value) {
14305
+ return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
14306
+ }
14307
+ function probeClaudeProcessInTmux(tmuxSession) {
14308
+ const escapedSession = escapePgrepRegex(tmuxSession);
14309
+ const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
14310
+ try {
14311
+ const out = execFileSync("pgrep", ["-f", "--", pattern], {
14312
+ encoding: "utf-8",
14313
+ timeout: 3e3
14314
+ }).trim();
14315
+ return out.length > 0 ? "alive" : "dead";
14316
+ } catch (err) {
14317
+ const e = err;
14318
+ if (e?.code === "ENOENT") return "unknown";
14319
+ return e?.status === 1 ? "dead" : "unknown";
14320
+ }
14321
+ }
14322
+ function probeTmuxSession(tmuxSession) {
14323
+ try {
14324
+ execFileSync("tmux", ["has-session", "-t", tmuxSession], {
14325
+ stdio: "ignore",
14326
+ timeout: 3e3
14327
+ });
14328
+ return "alive";
14329
+ } catch (err) {
14330
+ const e = err;
14331
+ if (e?.code === "ENOENT") return "unknown";
14332
+ return "dead";
14333
+ }
14334
+ }
14335
+ function probeAgentSession(codeName) {
14336
+ const session = agentTmuxSessionName(codeName);
14337
+ const tmux = probeTmuxSession(session);
14338
+ const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
14339
+ return { tmux, claude };
14340
+ }
14341
+ var probeCache = /* @__PURE__ */ new Map();
14342
+ var SESSION_PROBE_TTL_MS = 15e3;
14343
+ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
14344
+ const cached2 = probeCache.get(codeName);
14345
+ if (cached2 && now - cached2.at < ttlMs) return cached2.value;
14346
+ const value = probeAgentSession(codeName);
14347
+ probeCache.set(codeName, { at: now, value });
14348
+ return value;
14349
+ }
14350
+
14252
14351
  // src/slack-loop-throttle.ts
14253
14352
  var DEFAULT_THROTTLE = {
14254
14353
  threshold: 3,
@@ -14351,466 +14450,62 @@ async function isThreadKilled(opts) {
14351
14450
  }
14352
14451
  }
14353
14452
 
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
- }
14453
+ // src/slack-thread-context.ts
14454
+ var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
14455
+ var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
14456
+ function formatThreadMessages(messages, nameById) {
14457
+ return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
14458
+ }
14459
+ function capThreadContext(formatted, maxChars) {
14460
+ if (formatted.length <= maxChars) return formatted;
14461
+ const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
14462
+ if (maxChars <= marker.length) return marker.slice(0, maxChars);
14463
+ const tailBudget = maxChars - marker.length;
14464
+ const tail = formatted.slice(formatted.length - tailBudget);
14465
+ const firstNewline = tail.indexOf("\n");
14466
+ const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
14467
+ return `${marker}${clean}`;
14468
+ }
14469
+ async function fetchThreadTranscript(channel, threadTs, limit, deps) {
14470
+ const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
14471
+ const cappedLimit = Math.min(Math.max(limit, 1), 200);
14472
+ const allMessages = [];
14473
+ let cursor;
14474
+ do {
14475
+ const remaining = cappedLimit - allMessages.length;
14476
+ if (remaining <= 0) break;
14477
+ const params = new URLSearchParams({
14478
+ channel,
14479
+ ts: threadTs,
14480
+ limit: String(Math.min(remaining, 100)),
14481
+ ...cursor ? { cursor } : {}
14482
+ });
14483
+ let data;
14482
14484
  try {
14483
- await slackCall("chat.update", {
14484
- channel: opts.channel,
14485
- ts: anchorTs,
14486
- text: payload.text,
14487
- blocks: payload.blocks
14485
+ const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
14486
+ headers: { Authorization: `Bearer ${botToken}` }
14488
14487
  });
14488
+ if (!res.ok) return { ok: false, error: `http_${res.status}` };
14489
+ data = await res.json();
14489
14490
  } 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(() => {
14491
+ return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
14492
+ }
14493
+ if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
14494
+ for (const msg of data.messages ?? []) {
14495
+ allMessages.push({
14496
+ user: msg.user ?? msg.bot_id ?? "unknown",
14497
+ text: msg.text ?? "",
14498
+ ts: msg.ts ?? ""
14566
14499
  });
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
14500
  }
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";
14501
+ cursor = data.response_metadata?.next_cursor || void 0;
14502
+ } while (cursor && allMessages.length < cappedLimit);
14503
+ const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
14504
+ const resolved = await Promise.all(
14505
+ uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
14506
+ );
14507
+ const nameById = new Map(resolved);
14508
+ return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
14814
14509
  }
14815
14510
 
14816
14511
  // src/impersonation.ts
@@ -14958,20 +14653,20 @@ import {
14958
14653
  createWriteStream,
14959
14654
  existsSync as existsSync2,
14960
14655
  mkdirSync as mkdirSync3,
14961
- readFileSync as readFileSync3,
14962
- readdirSync,
14656
+ readFileSync as readFileSync4,
14657
+ readdirSync as readdirSync2,
14963
14658
  renameSync as renameSync2,
14964
14659
  statSync,
14965
14660
  unlinkSync as unlinkSync2,
14966
14661
  watch,
14967
14662
  writeFileSync as writeFileSync3
14968
14663
  } from "fs";
14969
- import { basename, join as join3, resolve as resolve2 } from "path";
14664
+ import { basename, join as join4, resolve as resolve2 } from "path";
14970
14665
  import { homedir as homedir2 } from "os";
14971
- import { createHash, randomUUID as randomUUID2 } from "crypto";
14666
+ import { createHash, randomUUID } from "crypto";
14972
14667
 
14973
14668
  // src/slack-thread-store.ts
14974
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
14669
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
14975
14670
  import { dirname } from "path";
14976
14671
  var FILE_VERSION = 1;
14977
14672
  var DEFAULT_TTL_DAYS = 30;
@@ -14982,7 +14677,7 @@ function loadThreadStore(filePath, opts = {}) {
14982
14677
  const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
14983
14678
  let raw;
14984
14679
  try {
14985
- raw = readFileSync(filePath, "utf-8");
14680
+ raw = readFileSync2(filePath, "utf-8");
14986
14681
  } catch {
14987
14682
  return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
14988
14683
  }
@@ -15097,9 +14792,9 @@ async function runOrRetry(fn, opts) {
15097
14792
 
15098
14793
  // src/channel-attachments.ts
15099
14794
  import { homedir } from "os";
15100
- import { join, resolve, sep } from "path";
14795
+ import { join as join2, resolve, sep } from "path";
15101
14796
  function resolveChannelInboundDir(codeName, channelSlug) {
15102
- const base = join(homedir(), ".augmented");
14797
+ const base = join2(homedir(), ".augmented");
15103
14798
  const allowedSegment = /^[A-Za-z0-9_-]+$/;
15104
14799
  if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
15105
14800
  throw new Error(
@@ -15745,12 +15440,12 @@ function createSlackBotUserIdClient(args) {
15745
15440
  import {
15746
15441
  existsSync,
15747
15442
  mkdirSync as mkdirSync2,
15748
- readFileSync as readFileSync2,
15443
+ readFileSync as readFileSync3,
15749
15444
  renameSync,
15750
15445
  unlinkSync,
15751
15446
  writeFileSync as writeFileSync2
15752
15447
  } from "fs";
15753
- import { join as join2 } from "path";
15448
+ import { join as join3 } from "path";
15754
15449
  function defaultIsPidAlive(pid) {
15755
15450
  if (!Number.isFinite(pid) || pid <= 0) return false;
15756
15451
  try {
@@ -15768,7 +15463,7 @@ function acquireMcpSpawnLock(args) {
15768
15463
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15769
15464
  const selfPid = options.selfPid ?? process.pid;
15770
15465
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15771
- const path = join2(agentDir, basename2);
15466
+ const path = join3(agentDir, basename2);
15772
15467
  const existing = readLockHolder(path);
15773
15468
  if (existing) {
15774
15469
  if (existing.pid === selfPid) {
@@ -15799,7 +15494,7 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
15799
15494
  function readLockHolder(path) {
15800
15495
  if (!existsSync(path)) return null;
15801
15496
  try {
15802
- const raw = readFileSync2(path, "utf8");
15497
+ const raw = readFileSync3(path, "utf8");
15803
15498
  const parsed = JSON.parse(raw);
15804
15499
  const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
15805
15500
  if (!Number.isFinite(pid) || pid <= 0) return null;
@@ -15881,9 +15576,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15881
15576
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15882
15577
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15883
15578
  };
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;
15579
+ var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15580
+ var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15581
+ var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15887
15582
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
15888
15583
  function redactSlackId(id) {
15889
15584
  if (!id) return "<none>";
@@ -15895,16 +15590,18 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
15895
15590
  }
15896
15591
  function slackPendingInboundPath(channel, threadTs, messageTs) {
15897
15592
  if (!SLACK_PENDING_INBOUND_DIR) return null;
15898
- return join3(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15593
+ return join4(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15899
15594
  }
15900
- function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15595
+ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
15901
15596
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
15902
15597
  if (!path || !SLACK_PENDING_INBOUND_DIR) return;
15903
15598
  const marker = {
15904
15599
  channel,
15905
15600
  thread_ts: threadTs,
15906
15601
  message_ts: messageTs,
15907
- received_at: (/* @__PURE__ */ new Date()).toISOString()
15602
+ received_at: (/* @__PURE__ */ new Date()).toISOString(),
15603
+ // Only persist the flag when set — keeps healthy-path markers byte-identical.
15604
+ ...undeliverable ? { undeliverable: true } : {}
15908
15605
  };
15909
15606
  try {
15910
15607
  mkdirSync3(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
@@ -15916,18 +15613,52 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15916
15613
  );
15917
15614
  }
15918
15615
  }
15616
+ function healSlackUndeliverable(channel, messageTs) {
15617
+ if (!BOT_TOKEN || !channel || !messageTs) return;
15618
+ const headers = {
15619
+ "Content-Type": "application/json",
15620
+ Authorization: `Bearer ${BOT_TOKEN}`
15621
+ };
15622
+ fetch("https://slack.com/api/reactions.remove", {
15623
+ method: "POST",
15624
+ headers,
15625
+ body: JSON.stringify({ channel, timestamp: messageTs, name: "x" })
15626
+ }).catch(() => {
15627
+ }).finally(() => {
15628
+ fetch("https://slack.com/api/reactions.add", {
15629
+ method: "POST",
15630
+ headers,
15631
+ body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
15632
+ }).catch(() => {
15633
+ });
15634
+ });
15635
+ }
15636
+ function clearSlackMarkerFileWithHeal(fullPath) {
15637
+ let marker = null;
15638
+ try {
15639
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
15640
+ } catch {
15641
+ }
15642
+ if (marker && decideRecoveryHeal({
15643
+ wasUndeliverable: marker.undeliverable === true,
15644
+ hasTarget: Boolean(marker.channel && marker.message_ts)
15645
+ }) === "heal") {
15646
+ healSlackUndeliverable(marker.channel, marker.message_ts);
15647
+ }
15648
+ try {
15649
+ if (existsSync2(fullPath)) unlinkSync2(fullPath);
15650
+ } catch {
15651
+ }
15652
+ }
15919
15653
  function clearAllSlackPendingMarkersForThread(channel, threadTs) {
15920
15654
  if (!SLACK_PENDING_INBOUND_DIR) return;
15921
15655
  const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
15922
15656
  const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
15923
15657
  const prefix = `${safeChan}__${safeThread}__`;
15924
15658
  try {
15925
- for (const f of readdirSync(SLACK_PENDING_INBOUND_DIR)) {
15659
+ for (const f of readdirSync2(SLACK_PENDING_INBOUND_DIR)) {
15926
15660
  if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
15927
- try {
15928
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
15929
- } catch {
15930
- }
15661
+ clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, f));
15931
15662
  }
15932
15663
  } catch {
15933
15664
  }
@@ -15946,10 +15677,10 @@ function slackNextRetryName(filename) {
15946
15677
  async function processSlackRecoveryOutboxFile(filename) {
15947
15678
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
15948
15679
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
15949
- const fullPath = join3(SLACK_RECOVERY_OUTBOX_DIR, filename);
15680
+ const fullPath = join4(SLACK_RECOVERY_OUTBOX_DIR, filename);
15950
15681
  let payload;
15951
15682
  try {
15952
- payload = JSON.parse(readFileSync3(fullPath, "utf-8"));
15683
+ payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
15953
15684
  } catch (err) {
15954
15685
  process.stderr.write(
15955
15686
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
@@ -16023,7 +15754,7 @@ async function processSlackRecoveryOutboxFile(filename) {
16023
15754
  const next = slackNextRetryName(filename);
16024
15755
  if (next) {
16025
15756
  try {
16026
- renameSync2(fullPath, join3(SLACK_RECOVERY_OUTBOX_DIR, next.next));
15757
+ renameSync2(fullPath, join4(SLACK_RECOVERY_OUTBOX_DIR, next.next));
16027
15758
  if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
16028
15759
  process.stderr.write(
16029
15760
  `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -16053,7 +15784,7 @@ function scanSlackRecoveryRetries() {
16053
15784
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
16054
15785
  let entries;
16055
15786
  try {
16056
- entries = readdirSync(SLACK_RECOVERY_OUTBOX_DIR);
15787
+ entries = readdirSync2(SLACK_RECOVERY_OUTBOX_DIR);
16057
15788
  } catch {
16058
15789
  return;
16059
15790
  }
@@ -16062,7 +15793,7 @@ function scanSlackRecoveryRetries() {
16062
15793
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
16063
15794
  let mtimeMs;
16064
15795
  try {
16065
- mtimeMs = statSync(join3(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
15796
+ mtimeMs = statSync(join4(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16066
15797
  } catch {
16067
15798
  continue;
16068
15799
  }
@@ -16083,7 +15814,7 @@ function startSlackRecoveryOutboxWatcher() {
16083
15814
  return;
16084
15815
  }
16085
15816
  try {
16086
- for (const f of readdirSync(SLACK_RECOVERY_OUTBOX_DIR)) {
15817
+ for (const f of readdirSync2(SLACK_RECOVERY_OUTBOX_DIR)) {
16087
15818
  if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
16088
15819
  }
16089
15820
  } catch {
@@ -16092,7 +15823,7 @@ function startSlackRecoveryOutboxWatcher() {
16092
15823
  const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
16093
15824
  if (event !== "rename" || !filename) return;
16094
15825
  if (!isFirstAttemptSlackOutboxFile(filename)) return;
16095
- if (existsSync2(join3(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
15826
+ if (existsSync2(join4(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16096
15827
  void processSlackRecoveryOutboxFile(filename);
16097
15828
  }
16098
15829
  });
@@ -16108,15 +15839,15 @@ function startSlackRecoveryOutboxWatcher() {
16108
15839
  }
16109
15840
  startSlackRecoveryOutboxWatcher();
16110
15841
  var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
16111
- function trackPendingMessage(channel, threadTs, messageTs) {
16112
- writeSlackPendingInboundMarker(channel, threadTs, messageTs);
15842
+ function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
15843
+ writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
16113
15844
  }
16114
15845
  function sweepSlackStaleMarkersOnBoot() {
16115
15846
  if (!SLACK_PENDING_INBOUND_DIR) return;
16116
15847
  if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16117
15848
  let filenames;
16118
15849
  try {
16119
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
15850
+ filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16120
15851
  } catch (err) {
16121
15852
  process.stderr.write(
16122
15853
  `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -16129,10 +15860,10 @@ function sweepSlackStaleMarkersOnBoot() {
16129
15860
  for (const filename of filenames) {
16130
15861
  if (!filename.endsWith(".json")) continue;
16131
15862
  if (filename.endsWith(".tmp")) continue;
16132
- const fullPath = join3(SLACK_PENDING_INBOUND_DIR, filename);
15863
+ const fullPath = join4(SLACK_PENDING_INBOUND_DIR, filename);
16133
15864
  let marker;
16134
15865
  try {
16135
- marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
15866
+ marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
16136
15867
  } catch (err) {
16137
15868
  process.stderr.write(
16138
15869
  `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
@@ -16185,7 +15916,7 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16185
15916
  if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16186
15917
  let filenames;
16187
15918
  try {
16188
- filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
15919
+ filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16189
15920
  } catch {
16190
15921
  return;
16191
15922
  }
@@ -16196,13 +15927,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
16196
15927
  for (const filename of filenames) {
16197
15928
  if (!filename.startsWith(channelPrefix)) continue;
16198
15929
  if (!filename.endsWith(messageSuffix)) continue;
16199
- try {
16200
- unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
16201
- } catch {
16202
- }
15930
+ clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, filename));
16203
15931
  }
16204
15932
  }
16205
- var RESTART_FLAGS_DIR = join3(homedir2(), ".augmented", "restart-flags");
15933
+ var RESTART_FLAGS_DIR = join4(homedir2(), ".augmented", "restart-flags");
16206
15934
  function buildAugmentedSlackMetadata() {
16207
15935
  if (!AGT_TEAM_ID) return void 0;
16208
15936
  return {
@@ -16384,7 +16112,7 @@ async function handleSlashCommandEnvelope(payload) {
16384
16112
  if (!existsSync2(RESTART_FLAGS_DIR)) {
16385
16113
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16386
16114
  }
16387
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16115
+ const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16388
16116
  const flag = {
16389
16117
  codeName,
16390
16118
  source: "slack",
@@ -16394,7 +16122,7 @@ async function handleSlashCommandEnvelope(payload) {
16394
16122
  ...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
16395
16123
  }
16396
16124
  };
16397
- const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
16125
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
16398
16126
  writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
16399
16127
  renameSync2(tmpPath, flagPath);
16400
16128
  process.stderr.write(
@@ -16491,7 +16219,7 @@ async function handleRestartCommand(opts) {
16491
16219
  if (!existsSync2(RESTART_FLAGS_DIR)) {
16492
16220
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16493
16221
  }
16494
- const flagPath = join3(RESTART_FLAGS_DIR, `${codeName}.flag`);
16222
+ const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16495
16223
  const flag = {
16496
16224
  codeName,
16497
16225
  source: "slack",
@@ -16502,7 +16230,7 @@ async function handleRestartCommand(opts) {
16502
16230
  message_ts: opts.ts
16503
16231
  }
16504
16232
  };
16505
- const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
16233
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
16506
16234
  writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
16507
16235
  renameSync2(tmpPath, flagPath);
16508
16236
  process.stderr.write(
@@ -16550,7 +16278,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
16550
16278
  var threadPersister = null;
16551
16279
  function resolveThreadStorePath() {
16552
16280
  if (!AGENT_CODE_NAME) return null;
16553
- return join3(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16281
+ return join4(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16554
16282
  }
16555
16283
  function parseTtlDays(raw) {
16556
16284
  if (!raw) return void 0;
@@ -16585,9 +16313,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
16585
16313
  var slackStderrLogStream = null;
16586
16314
  if (AGENT_CODE_NAME) {
16587
16315
  try {
16588
- const logDir = join3(homedir2(), ".augmented", AGENT_CODE_NAME);
16316
+ const logDir = join4(homedir2(), ".augmented", AGENT_CODE_NAME);
16589
16317
  mkdirSync3(logDir, { recursive: true });
16590
- slackStderrLogStream = createWriteStream(join3(logDir, "slack-channel-stderr.log"), {
16318
+ slackStderrLogStream = createWriteStream(join4(logDir, "slack-channel-stderr.log"), {
16591
16319
  flags: "a",
16592
16320
  mode: 384
16593
16321
  });
@@ -16657,7 +16385,6 @@ void resolveBotUserIdOrThrow().then((id) => {
16657
16385
  );
16658
16386
  if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
16659
16387
  });
16660
- var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
16661
16388
  var mcp = new Server(
16662
16389
  { name: "slack", version: "0.1.0" },
16663
16390
  {
@@ -16672,43 +16399,18 @@ var mcp = new Server(
16672
16399
  // Highest-priority lines first — Claude Code truncates this string at
16673
16400
  // 2048 chars, so anything appended late silently disappears.
16674
16401
  "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,
16402
+ `Inbound: <channel ... thread_ts="..." [thread_context="..."]>. Pass channel + thread_ts to slack.reply on threads. thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
16678
16403
  "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
16404
  "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).",
16405
+ 'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if useful, OR if your own bot user is @-mentioned (counts even in auto_followed).',
16406
+ "Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure of YOUR work \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets). (The \u274C you may see on an inbound is applied by the system, not you \u2014 it marks a message that arrived while the agent couldn't reply; never add \u274C yourself.)",
16682
16407
  `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
16408
  "To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
16684
16409
  ].join(" ")
16685
16410
  }
16686
16411
  );
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
16412
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16710
16413
  tools: [
16711
- ...progressRegistry.getDefinitions(),
16712
16414
  {
16713
16415
  name: "slack.reply",
16714
16416
  description: "Send a message back to a Slack channel or thread",
@@ -16730,7 +16432,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16730
16432
  },
16731
16433
  {
16732
16434
  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.",
16435
+ description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure of your work \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate. \u274C (:x:) is reserved for the system to mark an inbound that arrived while the agent couldn't reply \u2014 never add \u274C yourself.",
16734
16436
  inputSchema: {
16735
16437
  type: "object",
16736
16438
  properties: {
@@ -16743,7 +16445,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16743
16445
  },
16744
16446
  {
16745
16447
  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.",
16448
+ 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
16449
  inputSchema: {
16748
16450
  type: "object",
16749
16451
  properties: {
@@ -16959,8 +16661,6 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16959
16661
  if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
16960
16662
  return buildImpersonationRefusal(name);
16961
16663
  }
16962
- const progressResult = await progressRegistry.handle(name, args ?? {});
16963
- if (progressResult !== void 0) return progressResult;
16964
16664
  if (name === "slack.reply") {
16965
16665
  const { channel, text, thread_ts } = args;
16966
16666
  if (channel && thread_ts) {
@@ -17117,46 +16817,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
17117
16817
  const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
17118
16818
  noteThreadActivity(channel, thread_ts);
17119
16819
  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");
16820
+ const result = await fetchThreadTranscript(channel, thread_ts, limit, {
16821
+ botToken: BOT_TOKEN,
16822
+ resolveUserName
16823
+ });
16824
+ if (!result.ok) {
16825
+ return {
16826
+ content: [{ type: "text", text: `Slack error: ${result.error}` }],
16827
+ isError: true
16828
+ };
16829
+ }
17154
16830
  return {
17155
16831
  content: [{
17156
16832
  type: "text",
17157
- text: allMessages.length > 0 ? `Thread (${allMessages.length} messages):
16833
+ text: result.count > 0 ? `Thread (${result.count} messages):
17158
16834
 
17159
- ${formatted}` : "Thread is empty or not found."
16835
+ ${result.formatted}` : "Thread is empty or not found."
17160
16836
  }]
17161
16837
  };
17162
16838
  } catch (err) {
@@ -17206,7 +16882,7 @@ ${formatted}` : "Thread is empty or not found."
17206
16882
  };
17207
16883
  }
17208
16884
  size = stat.size;
17209
- bytes = readFileSync3(resolvedPath);
16885
+ bytes = readFileSync4(resolvedPath);
17210
16886
  } catch (err) {
17211
16887
  return {
17212
16888
  content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
@@ -17505,15 +17181,15 @@ async function handleSendStructured(args) {
17505
17181
  const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
17506
17182
  const { channel, blocks, text, thread_ts, interactive } = args;
17507
17183
  if (typeof channel !== "string" || !channel) {
17508
- return errResult2("channel is required");
17184
+ return errResult("channel is required");
17509
17185
  }
17510
17186
  if (typeof text !== "string" || !text) {
17511
- return errResult2("text is required (used as fallback for push notifications and unfurls)");
17187
+ return errResult("text is required (used as fallback for push notifications and unfurls)");
17512
17188
  }
17513
17189
  noteThreadActivity(channel, thread_ts);
17514
17190
  const validation = validateSlackBlocks2(blocks);
17515
17191
  if (!validation.ok) {
17516
- return errResult2(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17192
+ return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17517
17193
  }
17518
17194
  let effectiveBlocks = blocks;
17519
17195
  let ratingCallbackId = null;
@@ -17521,13 +17197,13 @@ async function handleSendStructured(args) {
17521
17197
  let ratingTokenised = [];
17522
17198
  if (interactive) {
17523
17199
  if (interactive.type !== "rate_run") {
17524
- return errResult2(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
17200
+ return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
17525
17201
  }
17526
17202
  if (typeof interactive.run_id !== "string" || !interactive.run_id) {
17527
- return errResult2("interactive.run_id is required when interactive is set");
17203
+ return errResult("interactive.run_id is required when interactive is set");
17528
17204
  }
17529
17205
  if (!interactiveHostAvailable()) {
17530
- return errResult2(
17206
+ return errResult(
17531
17207
  "interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
17532
17208
  );
17533
17209
  }
@@ -17558,7 +17234,7 @@ async function handleSendStructured(args) {
17558
17234
  effectiveBlocks = [...blocks, ratingBlock];
17559
17235
  const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
17560
17236
  if (!finalValidation.ok) {
17561
- return errResult2(
17237
+ return errResult(
17562
17238
  `Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
17563
17239
  );
17564
17240
  }
@@ -17579,7 +17255,7 @@ async function handleSendStructured(args) {
17579
17255
  kindData: { run_id: interactive.run_id, scale }
17580
17256
  });
17581
17257
  } catch (err) {
17582
- return errResult2(`Failed to register rating row: ${err.message}`);
17258
+ return errResult(`Failed to register rating row: ${err.message}`);
17583
17259
  }
17584
17260
  }
17585
17261
  const result = await postSlackMessageWithTs({
@@ -17589,7 +17265,7 @@ async function handleSendStructured(args) {
17589
17265
  ...thread_ts ? { thread_ts } : {}
17590
17266
  });
17591
17267
  if (!result.ok || !result.ts) {
17592
- return errResult2(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
17268
+ return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
17593
17269
  }
17594
17270
  if (ratingCallbackId) {
17595
17271
  try {
@@ -17644,17 +17320,17 @@ async function handleSendStructured(args) {
17644
17320
  }
17645
17321
  async function handleAskUser(args) {
17646
17322
  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.");
17323
+ return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
17648
17324
  }
17649
17325
  const { randomUUID: rndUUID } = await import("crypto");
17650
17326
  const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
17651
17327
  const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
17652
17328
  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");
17329
+ if (typeof channel !== "string" || !channel) return errResult("channel is required");
17330
+ if (typeof question !== "string" || !question) return errResult("question is required");
17655
17331
  const optsValidation = validateAskUserOptions2(options);
17656
17332
  if (!optsValidation.ok) {
17657
- return errResult2(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17333
+ return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
17658
17334
  }
17659
17335
  const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
17660
17336
  const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
@@ -17687,7 +17363,7 @@ async function handleAskUser(args) {
17687
17363
  expiresAt
17688
17364
  });
17689
17365
  } catch (err) {
17690
- return errResult2(`Failed to register pending interaction: ${err.message}`);
17366
+ return errResult(`Failed to register pending interaction: ${err.message}`);
17691
17367
  }
17692
17368
  const slackResult = await postSlackMessageWithTs({
17693
17369
  channel,
@@ -17697,7 +17373,7 @@ async function handleAskUser(args) {
17697
17373
  ...thread_ts ? { thread_ts } : {}
17698
17374
  });
17699
17375
  if (!slackResult.ok || !slackResult.ts) {
17700
- return errResult2(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
17376
+ return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
17701
17377
  }
17702
17378
  try {
17703
17379
  await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
@@ -17744,7 +17420,7 @@ async function handleAskUser(args) {
17744
17420
  }
17745
17421
  async function handleChannelRequestInput(args) {
17746
17422
  if (!askUserToolAvailable()) {
17747
- return errResult2(
17423
+ return errResult(
17748
17424
  "channel_request_input is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring."
17749
17425
  );
17750
17426
  }
@@ -17752,10 +17428,10 @@ async function handleChannelRequestInput(args) {
17752
17428
  const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
17753
17429
  const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
17754
17430
  if (typeof thread_id !== "string" || !thread_id) {
17755
- return errResult2("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
17431
+ return errResult("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
17756
17432
  }
17757
17433
  if (typeof prompt_text !== "string" || !prompt_text) {
17758
- return errResult2("prompt_text is required");
17434
+ return errResult("prompt_text is required");
17759
17435
  }
17760
17436
  const optsValidation = validateAskUserOptions2(options, {
17761
17437
  maxOptions: 4,
@@ -17763,25 +17439,25 @@ async function handleChannelRequestInput(args) {
17763
17439
  maxValueChars: 2e3
17764
17440
  });
17765
17441
  if (!optsValidation.ok) {
17766
- return errResult2(
17442
+ return errResult(
17767
17443
  `Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
17768
17444
  );
17769
17445
  }
17770
17446
  const optionsArr = options;
17771
17447
  if (optionsArr.length < 2) {
17772
- return errResult2("options must contain at least 2 entries (this is a picker, not a notification)");
17448
+ return errResult("options must contain at least 2 entries (this is a picker, not a notification)");
17773
17449
  }
17774
17450
  if (!schema || typeof schema.type !== "string") {
17775
- return errResult2("schema is required with shape { type: 'yes_no' | 'choice' }");
17451
+ return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
17776
17452
  }
17777
17453
  if (schema.type !== "yes_no" && schema.type !== "choice") {
17778
- return errResult2("schema.type must be 'yes_no' or 'choice'");
17454
+ return errResult("schema.type must be 'yes_no' or 'choice'");
17779
17455
  }
17780
17456
  if (schema.type === "yes_no" && optionsArr.length !== 2) {
17781
- return errResult2("schema.type='yes_no' requires exactly 2 options");
17457
+ return errResult("schema.type='yes_no' requires exactly 2 options");
17782
17458
  }
17783
17459
  if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
17784
- return errResult2("request_id must be a non-empty string when provided");
17460
+ return errResult("request_id must be a non-empty string when provided");
17785
17461
  }
17786
17462
  const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
17787
17463
  const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
@@ -17803,10 +17479,10 @@ async function handleChannelRequestInput(args) {
17803
17479
  dispatchPayload = await res.json().catch(() => ({}));
17804
17480
  if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
17805
17481
  const reason = dispatchPayload.reason ?? `http_${res.status}`;
17806
- return errResult2(`channel_request_input dispatch failed: ${reason}`);
17482
+ return errResult(`channel_request_input dispatch failed: ${reason}`);
17807
17483
  }
17808
17484
  } catch (err) {
17809
- return errResult2(
17485
+ return errResult(
17810
17486
  `channel_request_input dispatch failed: ${err.message}`
17811
17487
  );
17812
17488
  }
@@ -17844,7 +17520,7 @@ async function handleChannelRequestInput(args) {
17844
17520
  ]
17845
17521
  };
17846
17522
  }
17847
- function errResult2(text) {
17523
+ function errResult(text) {
17848
17524
  return { content: [{ type: "text", text }], isError: true };
17849
17525
  }
17850
17526
  var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
@@ -18226,19 +17902,29 @@ async function connectSocketMode() {
18226
17902
  isAutoFollowed,
18227
17903
  botUserId
18228
17904
  });
18229
- if (decideSlackAckReaction({ channel, ts })) {
17905
+ const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
17906
+ const ackDecision = decideAckReaction({
17907
+ hasTarget: decideSlackAckReaction({ channel, ts }),
17908
+ integrationReady: Boolean(BOT_TOKEN),
17909
+ tmux: ackProbe.tmux,
17910
+ claude: ackProbe.claude,
17911
+ withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
17912
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR)
17913
+ });
17914
+ if (ackDecision !== "none") {
17915
+ const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
18230
17916
  fetch("https://slack.com/api/reactions.add", {
18231
17917
  method: "POST",
18232
17918
  headers: {
18233
17919
  "Content-Type": "application/json",
18234
17920
  Authorization: `Bearer ${BOT_TOKEN}`
18235
17921
  },
18236
- body: JSON.stringify({ channel, timestamp: ts, name: "eyes" })
17922
+ body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
18237
17923
  }).catch(() => {
18238
17924
  });
18239
17925
  }
18240
17926
  if (channel && ts && shouldEngage) {
18241
- trackPendingMessage(channel, threadTs, ts);
17927
+ trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
18242
17928
  }
18243
17929
  const userName = await resolveUserName(user);
18244
17930
  const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
@@ -18246,6 +17932,29 @@ async function connectSocketMode() {
18246
17932
  (f) => f.kind === "image" && typeof f.path === "string"
18247
17933
  );
18248
17934
  const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
17935
+ let threadContext;
17936
+ if (isThreadReply && channel && threadTs) {
17937
+ try {
17938
+ const transcript = await fetchThreadTranscript(
17939
+ channel,
17940
+ threadTs,
17941
+ SLACK_AUTOLOAD_THREAD_LIMIT,
17942
+ { botToken: BOT_TOKEN, resolveUserName }
17943
+ );
17944
+ if (transcript.ok && transcript.count >= 2) {
17945
+ threadContext = capThreadContext(
17946
+ transcript.formatted,
17947
+ SLACK_AUTOLOAD_THREAD_MAX_CHARS
17948
+ );
17949
+ }
17950
+ } catch (err) {
17951
+ const msg2 = err instanceof Error ? err.message : String(err);
17952
+ process.stderr.write(
17953
+ `slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
17954
+ `
17955
+ );
17956
+ }
17957
+ }
18249
17958
  await mcp.notification({
18250
17959
  method: "notifications/claude/channel",
18251
17960
  params: {
@@ -18260,7 +17969,9 @@ async function connectSocketMode() {
18260
17969
  // Only set these when we actually have attachments to avoid
18261
17970
  // bloating every notification with empty metadata.
18262
17971
  ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
18263
- ...imagePath ? { image_path: imagePath } : {}
17972
+ ...imagePath ? { image_path: imagePath } : {},
17973
+ // ENG-5830: the pre-loaded surrounding thread (thread replies only).
17974
+ ...threadContext ? { thread_context: threadContext } : {}
18264
17975
  }
18265
17976
  }
18266
17977
  });