@integrity-labs/agt-cli 0.28.237 → 0.28.238

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.
@@ -16256,18 +16256,18 @@ import {
16256
16256
  ftruncateSync,
16257
16257
  mkdirSync as mkdirSync8,
16258
16258
  openSync,
16259
- readFileSync as readFileSync10,
16260
- readdirSync as readdirSync3,
16259
+ readFileSync as readFileSync14,
16260
+ readdirSync as readdirSync4,
16261
16261
  renameSync as renameSync4,
16262
16262
  statSync as statSync2,
16263
16263
  unlinkSync as unlinkSync5,
16264
16264
  watch,
16265
- writeFileSync as writeFileSync10,
16265
+ writeFileSync as writeFileSync12,
16266
16266
  writeSync
16267
16267
  } from "fs";
16268
- import { basename, join as join9, resolve as resolve2 } from "path";
16268
+ import { basename, join as join13, resolve as resolve2 } from "path";
16269
16269
  import { homedir as homedir4 } from "os";
16270
- import { createHash, randomUUID as randomUUID2 } from "crypto";
16270
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
16271
16271
 
16272
16272
  // src/slack-thread-store.ts
16273
16273
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
@@ -16549,8 +16549,46 @@ async function runOrRetry(fn, opts) {
16549
16549
  }
16550
16550
 
16551
16551
  // src/turn-initiator-marker.ts
16552
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
16553
- import { dirname as dirname4 } from "path";
16552
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync7, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
16553
+ import { dirname as dirname4, join as join7 } from "path";
16554
+ var TURN_INITIATOR_MAX_AGE_MS = 5 * 60 * 1e3;
16555
+ var TURN_INITIATOR_LEDGER_MAX_ENTRIES = 20;
16556
+ var TURN_INITIATOR_LEDGER_FILENAME = ".turn-initiator-ledger.json";
16557
+ function turnInitiatorLedgerPath(singleSlotFile) {
16558
+ return join7(dirname4(singleSlotFile), TURN_INITIATOR_LEDGER_FILENAME);
16559
+ }
16560
+ function foldTurnInitiatorLedger(existing, marker, maxAgeMs = TURN_INITIATOR_MAX_AGE_MS, maxEntries = TURN_INITIATOR_LEDGER_MAX_ENTRIES) {
16561
+ const now = marker.ts;
16562
+ const key2 = (e) => `${e.channel}\0${e.channel_ref ?? ""}`;
16563
+ const markerKey = key2(marker);
16564
+ const prior = existing && Array.isArray(existing.entries) ? existing.entries : [];
16565
+ const kept = prior.filter(
16566
+ (e) => e && typeof e.ts === "number" && Number.isFinite(e.ts) && now - e.ts <= maxAgeMs && now - e.ts >= 0 && key2(e) !== markerKey
16567
+ // the same thread is replaced by the new entry below
16568
+ );
16569
+ kept.push({
16570
+ channel: marker.channel,
16571
+ sender_id: marker.sender_id,
16572
+ ...marker.sender_name ? { sender_name: marker.sender_name } : {},
16573
+ ...marker.channel_ref ? { channel_ref: marker.channel_ref } : {},
16574
+ ts: marker.ts
16575
+ });
16576
+ kept.sort((a, b) => b.ts - a.ts);
16577
+ return { v: 1, entries: kept.slice(0, maxEntries) };
16578
+ }
16579
+ function updateTurnInitiatorLedger(singleSlotFile, marker) {
16580
+ const ledgerFile = turnInitiatorLedgerPath(singleSlotFile);
16581
+ let existing = null;
16582
+ try {
16583
+ const parsed = JSON.parse(readFileSync7(ledgerFile, "utf8"));
16584
+ if (parsed && parsed.v === 1 && Array.isArray(parsed.entries)) existing = parsed;
16585
+ } catch {
16586
+ }
16587
+ const next = foldTurnInitiatorLedger(existing, marker);
16588
+ const tmp = `${ledgerFile}.tmp`;
16589
+ writeFileSync6(tmp, JSON.stringify(next), "utf8");
16590
+ renameSync2(tmp, ledgerFile);
16591
+ }
16554
16592
  function writeTurnInitiatorMarker(input) {
16555
16593
  const file = process.env["AGT_TURN_INITIATOR_FILE"];
16556
16594
  if (!file || !input.sender_id) return;
@@ -16560,12 +16598,16 @@ function writeTurnInitiatorMarker(input) {
16560
16598
  const tmp = `${file}.tmp`;
16561
16599
  writeFileSync6(tmp, JSON.stringify(marker), "utf8");
16562
16600
  renameSync2(tmp, file);
16601
+ try {
16602
+ updateTurnInitiatorLedger(file, marker);
16603
+ } catch {
16604
+ }
16563
16605
  } catch {
16564
16606
  }
16565
16607
  }
16566
16608
 
16567
16609
  // src/slack-bot-photo.ts
16568
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
16610
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
16569
16611
  import { dirname as dirname5 } from "path";
16570
16612
  async function applyBotPhoto(opts) {
16571
16613
  const fetchImpl = opts.fetchImpl ?? fetch;
@@ -16575,7 +16617,7 @@ async function applyBotPhoto(opts) {
16575
16617
  const { token, avatarUrl, markerPath } = opts;
16576
16618
  if (markerPath && existsSync6(markerPath)) {
16577
16619
  try {
16578
- if (readFileSync7(markerPath, "utf-8").trim() === avatarUrl) {
16620
+ if (readFileSync8(markerPath, "utf-8").trim() === avatarUrl) {
16579
16621
  return { status: "skipped-unchanged" };
16580
16622
  }
16581
16623
  } catch {
@@ -16700,8 +16742,8 @@ function conversationalLaneMeta(expectsReply = true) {
16700
16742
  }
16701
16743
 
16702
16744
  // src/inbound-lane-telemetry.ts
16703
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
16704
- import { join as join7 } from "path";
16745
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
16746
+ import { join as join8 } from "path";
16705
16747
  var LANE_CLASSIFICATION_COUNTER_SUFFIX = "-lane-classifications.json";
16706
16748
  var SUSPECTED_MISCLASSIFICATION_KEY = "suspected_misclassification";
16707
16749
  var HUMAN_CHANNEL_SOURCES = /* @__PURE__ */ new Set([
@@ -16718,10 +16760,10 @@ function isSuspectedMisclassification(lane, source) {
16718
16760
  }
16719
16761
  function recordLaneClassification(agentDir, channel, classification) {
16720
16762
  if (!agentDir) return;
16721
- const path = join7(agentDir, `${channel}${LANE_CLASSIFICATION_COUNTER_SUFFIX}`);
16763
+ const path = join8(agentDir, `${channel}${LANE_CLASSIFICATION_COUNTER_SUFFIX}`);
16722
16764
  let counts = {};
16723
16765
  try {
16724
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
16766
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
16725
16767
  if (parsed && typeof parsed === "object") counts = parsed;
16726
16768
  } catch {
16727
16769
  }
@@ -16736,6 +16778,333 @@ function recordLaneClassification(agentDir, channel, classification) {
16736
16778
  }
16737
16779
  }
16738
16780
 
16781
+ // src/slack-inbound-registry.ts
16782
+ import { readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
16783
+ import { join as join9 } from "path";
16784
+ var DEFAULT_MAX_ENTRIES = 500;
16785
+ var DEFAULT_CLEARED_TTL_MS = 6 * 60 * 60 * 1e3;
16786
+ function entryKey(channel, messageTs) {
16787
+ return `${channel}\0${messageTs}`;
16788
+ }
16789
+ function createInboundRegistry(opts = {}) {
16790
+ const maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
16791
+ const clearedTtlMs = opts.clearedTtlMs ?? DEFAULT_CLEARED_TTL_MS;
16792
+ const now = opts.now ?? Date.now;
16793
+ const entries = /* @__PURE__ */ new Map();
16794
+ function prune() {
16795
+ const cutoff = now() - clearedTtlMs;
16796
+ for (const [key2, e] of entries) {
16797
+ if (e.clearedAt !== void 0 && e.clearedAt < cutoff) entries.delete(key2);
16798
+ }
16799
+ if (entries.size <= maxEntries) return;
16800
+ for (const [key2, e] of entries) {
16801
+ if (entries.size <= maxEntries) break;
16802
+ if (e.clearedAt !== void 0) entries.delete(key2);
16803
+ }
16804
+ for (const key2 of entries.keys()) {
16805
+ if (entries.size <= maxEntries) break;
16806
+ entries.delete(key2);
16807
+ }
16808
+ }
16809
+ function record2(i) {
16810
+ const key2 = entryKey(i.channel, i.messageTs);
16811
+ entries.delete(key2);
16812
+ entries.set(key2, { ...i });
16813
+ prune();
16814
+ }
16815
+ function markCleared(channel, messageTs) {
16816
+ const e = entries.get(entryKey(channel, messageTs));
16817
+ if (e && e.clearedAt === void 0) e.clearedAt = now();
16818
+ }
16819
+ function markClearedThread(channel, threadTs) {
16820
+ const t = now();
16821
+ for (const e of entries.values()) {
16822
+ if (e.channel === channel && e.threadTs === threadTs && e.clearedAt === void 0) {
16823
+ e.clearedAt = t;
16824
+ }
16825
+ }
16826
+ }
16827
+ function markClearedOldestInChannel(channel) {
16828
+ let oldest = null;
16829
+ for (const e of entries.values()) {
16830
+ if (e.channel !== channel || e.clearedAt !== void 0) continue;
16831
+ if (oldest === null || e.receivedAt < oldest.receivedAt) oldest = e;
16832
+ }
16833
+ if (oldest) oldest.clearedAt = now();
16834
+ }
16835
+ function byInboundId(id) {
16836
+ if (!id) return null;
16837
+ for (const e of entries.values()) {
16838
+ if (e.inboundId === id) return e;
16839
+ }
16840
+ return null;
16841
+ }
16842
+ function byCoords(channel, threadOrMessageTs) {
16843
+ let best = null;
16844
+ let bestExact = false;
16845
+ for (const e of entries.values()) {
16846
+ if (e.channel !== channel) continue;
16847
+ const exact = e.messageTs === threadOrMessageTs;
16848
+ const thread = e.threadTs === threadOrMessageTs;
16849
+ if (!exact && !thread) continue;
16850
+ if (best === null) {
16851
+ best = e;
16852
+ bestExact = exact;
16853
+ continue;
16854
+ }
16855
+ if (exact && !bestExact) {
16856
+ best = e;
16857
+ bestExact = true;
16858
+ } else if (exact === bestExact && e.receivedAt >= best.receivedAt) {
16859
+ best = e;
16860
+ }
16861
+ }
16862
+ return best;
16863
+ }
16864
+ function livePending() {
16865
+ const out = [];
16866
+ for (const e of entries.values()) {
16867
+ if (e.clearedAt === void 0) out.push(e);
16868
+ }
16869
+ return out;
16870
+ }
16871
+ function seedFromMarkerDir(dir) {
16872
+ let names;
16873
+ try {
16874
+ names = readdirSync3(dir);
16875
+ } catch {
16876
+ return;
16877
+ }
16878
+ for (const name of names) {
16879
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
16880
+ if (name.includes(".retry-") || name.includes(".poison")) continue;
16881
+ let marker;
16882
+ try {
16883
+ marker = JSON.parse(readFileSync10(join9(dir, name), "utf-8"));
16884
+ } catch {
16885
+ continue;
16886
+ }
16887
+ const channel = typeof marker.channel === "string" ? marker.channel : "";
16888
+ const threadTs = typeof marker.thread_ts === "string" ? marker.thread_ts : "";
16889
+ const messageTs = typeof marker.message_ts === "string" ? marker.message_ts : "";
16890
+ if (!channel || !messageTs) continue;
16891
+ const receivedRaw = typeof marker.received_at === "string" ? Date.parse(marker.received_at) : NaN;
16892
+ record2({
16893
+ inboundId: typeof marker.inbound_id === "string" ? marker.inbound_id : "",
16894
+ channel,
16895
+ threadTs: threadTs || messageTs,
16896
+ // top-level markers may omit thread_ts
16897
+ messageTs,
16898
+ user: "",
16899
+ // markers don't persist the sender - unknown after restart
16900
+ userName: "",
16901
+ receivedAt: Number.isFinite(receivedRaw) ? receivedRaw : now()
16902
+ });
16903
+ }
16904
+ }
16905
+ return {
16906
+ record: record2,
16907
+ markCleared,
16908
+ markClearedThread,
16909
+ markClearedOldestInChannel,
16910
+ byInboundId,
16911
+ byCoords,
16912
+ livePending,
16913
+ seedFromMarkerDir,
16914
+ size: () => entries.size
16915
+ };
16916
+ }
16917
+
16918
+ // src/slack-inbound-id.ts
16919
+ import { createHash } from "crypto";
16920
+ var INBOUND_ID_HEX_LEN = 12;
16921
+ function slackInboundId(channel, threadTs, messageTs) {
16922
+ const digest = createHash("sha256").update(`${channel}|${threadTs}|${messageTs}`).digest("hex").slice(0, INBOUND_ID_HEX_LEN);
16923
+ return `ib_${digest}`;
16924
+ }
16925
+
16926
+ // src/slack-reply-binding.ts
16927
+ function matchByCoords(registry2, channel, messageTs, threadTs) {
16928
+ if (!channel) return null;
16929
+ return (messageTs ? registry2.byCoords(channel, messageTs) : null) ?? (threadTs ? registry2.byCoords(channel, threadTs) : null);
16930
+ }
16931
+ function describeLivePending(registry2, now) {
16932
+ const live = registry2.livePending();
16933
+ if (live.length === 0) return "none";
16934
+ return live.slice(0, 8).map((i) => {
16935
+ const ageS = Math.max(0, Math.floor((now - i.receivedAt) / 1e3));
16936
+ const who = i.userName || i.user || "unknown";
16937
+ const id = i.inboundId || "(no id)";
16938
+ return `${id} (${who}, ${ageS}s ago)`;
16939
+ }).join("; ");
16940
+ }
16941
+ function resolveReplyBinding(input) {
16942
+ const { mode, args, registry: registry2 } = input;
16943
+ const now = input.now ?? Date.now();
16944
+ const modelChannel = args.channel;
16945
+ const modelThread = args.thread_ts;
16946
+ if (args.inbound_id) {
16947
+ const known = registry2.byInboundId(args.inbound_id);
16948
+ if (known) {
16949
+ const conflict = !!modelChannel && modelChannel !== known.channel || !!modelThread && modelThread !== known.threadTs;
16950
+ return {
16951
+ channel: known.channel,
16952
+ threadTs: known.threadTs,
16953
+ classification: conflict ? "coords_corrected" : "bound",
16954
+ unknownInboundId: false
16955
+ };
16956
+ }
16957
+ if (args.proactive === true) {
16958
+ return {
16959
+ channel: modelChannel,
16960
+ threadTs: modelThread,
16961
+ classification: "proactive",
16962
+ unknownInboundId: true
16963
+ };
16964
+ }
16965
+ return classifyByCoords(mode, args, registry2, now, true);
16966
+ }
16967
+ if (args.proactive === true) {
16968
+ return {
16969
+ channel: modelChannel,
16970
+ threadTs: modelThread,
16971
+ classification: "proactive",
16972
+ unknownInboundId: false
16973
+ };
16974
+ }
16975
+ return classifyByCoords(mode, args, registry2, now, false);
16976
+ }
16977
+ function classifyByCoords(mode, args, registry2, now, unknownInboundId) {
16978
+ const modelChannel = args.channel;
16979
+ const modelThread = args.thread_ts;
16980
+ const matched = matchByCoords(registry2, modelChannel, args.message_ts, modelThread);
16981
+ if (matched) {
16982
+ return {
16983
+ channel: modelChannel ?? matched.channel,
16984
+ threadTs: modelThread ?? matched.threadTs,
16985
+ classification: "coords_match",
16986
+ unknownInboundId
16987
+ };
16988
+ }
16989
+ if (registry2.size() === 0) {
16990
+ return {
16991
+ channel: modelChannel,
16992
+ threadTs: modelThread,
16993
+ classification: "legacy",
16994
+ unknownInboundId
16995
+ };
16996
+ }
16997
+ const decision = {
16998
+ channel: modelChannel,
16999
+ threadTs: modelThread,
17000
+ classification: "unknown_target",
17001
+ unknownInboundId
17002
+ };
17003
+ if (mode === "warn") {
17004
+ decision.agentFeedback = `Note: this reply does not match any message you received. If it is a self-initiated message set proactive:true; otherwise reply with the inbound_id of the message you are answering. Live inbounds awaiting a reply: ${describeLivePending(registry2, now)}.`;
17005
+ } else if (mode === "enforce") {
17006
+ decision.reject = `This reply was blocked: its target does not match any message you received, so it may be going to the wrong thread. Reply with the inbound_id of the message you are answering, or set proactive:true for a self-initiated message. Live inbounds: ${describeLivePending(registry2, now)}.`;
17007
+ }
17008
+ return decision;
17009
+ }
17010
+
17011
+ // src/slack-mcp-instructions.ts
17012
+ function buildSlackMcpInstructions(opts) {
17013
+ const { ackReaction, skipReactionOn, skipReaction } = opts;
17014
+ return [
17015
+ "CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply - typed text never reaches the user. Slow work: interim ack (slack.reply interim:true), then your FINAL answer as a separate slack.reply (interim omitted). An ack is not the answer.",
17016
+ `Inbound: <channel ... inbound_id="ib_..." message_ts="..." [thread_context="..."]>. Reply with slack.reply passing inbound_id (copy it verbatim from the tag you are answering) + text; the server routes it to the right thread. Multiple <channel> tags = separate conversations: NEVER answer one user in another's inbound_id/thread. thread_context = pre-loaded thread; ground replies ONLY in it. For a self-initiated message (not a reply) set proactive:true.`,
17017
+ "Attachments: <channel> `files` is a JSON string - JSON.parse it. Entry with `path` (or a top-level `image_path`) is already downloaded - Read it; do NOT call slack.download_attachment. Only entries with `file_id` and no `path` (PDF/docx/csv) need slack.download_attachment (pass file_id + channel), then Read the returned path. Don't surface file-handling errors that don't affect the answer.",
17018
+ "Address users by user_name, never raw user ID. In multi-participant threads the CURRENT speaker is on the latest <channel> tag.",
17019
+ `Reactions (slack.react sparingly - prefer a reply): :${ackReaction}: = ack (auto-added on inbound, don't duplicate); \u2705 = success.` + (skipReactionOn ? ` "${skipReaction}" = seen, not replying.` : "") + " NEVER react to signal failure of your work - on failure slack.reply one sentence explaining it (no stack traces/secrets). \u23F3 and \u274C are system-only; never add them.",
17020
+ skipReactionOn ? `Decided NOT to reply to a DM or thread message (different @-mention, side chat, auto_followed catch-up, nothing useful)? slack.react with "${skipReaction}" so the sender knows you saw it - never post "this wasn't for me". Top-level channel messages you may skip silently.` : "A thread message NOT addressed to you (different @-mention, side chat, auto_followed catch-up): SILENTLY SKIP - no reaction, no reply.",
17021
+ 'auto_followed="true" \u2192 reply only if useful OR your bot user is @-mentioned. Deliver a file: save under your project dir, call slack.upload_file with path + channel + inbound_id.'
17022
+ ].join(" ");
17023
+ }
17024
+
17025
+ // src/slack-reply-binding-telemetry.ts
17026
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
17027
+ import { join as join10 } from "path";
17028
+ var REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX = "-reply-binding-classifications.json";
17029
+ var UNKNOWN_INBOUND_ID_KEY = "unknown_inbound_id";
17030
+ var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "warn", "enforce"]);
17031
+ function slackReplyBindingMode() {
17032
+ const raw = process.env["AGT_SLACK_REPLY_BINDING"];
17033
+ if (raw && VALID_MODES.has(raw)) return raw;
17034
+ return "shadow";
17035
+ }
17036
+ function recordReplyBindingClassification(agentDir, channel, input) {
17037
+ if (!agentDir) return;
17038
+ const path = join10(agentDir, `${channel}${REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX}`);
17039
+ let counts = {};
17040
+ try {
17041
+ const parsed = JSON.parse(readFileSync11(path, "utf-8"));
17042
+ if (parsed && typeof parsed === "object") counts = parsed;
17043
+ } catch {
17044
+ }
17045
+ counts[input.classification] = (counts[input.classification] ?? 0) + 1;
17046
+ if (input.unknownInboundId) {
17047
+ counts[UNKNOWN_INBOUND_ID_KEY] = (counts[UNKNOWN_INBOUND_ID_KEY] ?? 0) + 1;
17048
+ }
17049
+ try {
17050
+ writeFileSync9(path, JSON.stringify(counts), { mode: 384 });
17051
+ } catch {
17052
+ }
17053
+ }
17054
+
17055
+ // src/slack-reply-target-telemetry.ts
17056
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
17057
+ import { join as join11 } from "path";
17058
+ var REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX = "-reply-target-classifications.json";
17059
+ function pendingThreadsBucket(n) {
17060
+ if (n <= 0) return "0";
17061
+ if (n === 1) return "1";
17062
+ if (n === 2) return "2";
17063
+ return "3plus";
17064
+ }
17065
+ function replyTargetKey(result, bucket) {
17066
+ return `${result}|${bucket}`;
17067
+ }
17068
+ function classifyReplyTarget(input) {
17069
+ const { channel, threadTs, messageTs, registry: registry2 } = input;
17070
+ const pending = registry2.livePending();
17071
+ const pendingThreadKeys = new Set(pending.map((p) => `${p.channel} ${p.threadTs}`));
17072
+ const pendingThreads = pendingThreadKeys.size;
17073
+ const targetThread = threadTs ?? messageTs;
17074
+ const targetThreadPending = targetThread !== void 0 && pendingThreadKeys.has(`${channel} ${targetThread}`);
17075
+ const matched = (messageTs ? registry2.byCoords(channel, messageTs) : null) ?? (threadTs ? registry2.byCoords(channel, threadTs) : null);
17076
+ let result;
17077
+ if (!matched) {
17078
+ result = "unknown_target";
17079
+ } else if (matched.clearedAt === void 0 || targetThreadPending) {
17080
+ result = "matched_pending";
17081
+ } else if (pendingThreads > 0) {
17082
+ result = "crossed_candidate";
17083
+ } else {
17084
+ result = "matched_recent";
17085
+ }
17086
+ return { result, pendingThreads };
17087
+ }
17088
+ function recordReplyTargetClassification(agentDir, channel, classification) {
17089
+ if (!agentDir) return;
17090
+ const path = join11(agentDir, `${channel}${REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX}`);
17091
+ let counts = {};
17092
+ try {
17093
+ const parsed = JSON.parse(readFileSync12(path, "utf-8"));
17094
+ if (parsed && typeof parsed === "object") counts = parsed;
17095
+ } catch {
17096
+ }
17097
+ const key2 = replyTargetKey(
17098
+ classification.result,
17099
+ pendingThreadsBucket(classification.pendingThreads)
17100
+ );
17101
+ counts[key2] = (counts[key2] ?? 0) + 1;
17102
+ try {
17103
+ writeFileSync10(path, JSON.stringify(counts), { mode: 384 });
17104
+ } catch {
17105
+ }
17106
+ }
17107
+
16739
17108
  // src/slack-allowlist-source.ts
16740
17109
  function parseAllowedUsersCsv(raw) {
16741
17110
  return new Set(
@@ -17402,12 +17771,12 @@ async function actuateHostRestart(opts) {
17402
17771
  import {
17403
17772
  existsSync as existsSync7,
17404
17773
  mkdirSync as mkdirSync7,
17405
- readFileSync as readFileSync9,
17774
+ readFileSync as readFileSync13,
17406
17775
  renameSync as renameSync3,
17407
17776
  unlinkSync as unlinkSync4,
17408
- writeFileSync as writeFileSync9
17777
+ writeFileSync as writeFileSync11
17409
17778
  } from "fs";
17410
- import { join as join8 } from "path";
17779
+ import { join as join12 } from "path";
17411
17780
  function defaultIsPidAlive(pid) {
17412
17781
  if (!Number.isFinite(pid) || pid <= 0) return false;
17413
17782
  try {
@@ -17425,7 +17794,7 @@ function acquireMcpSpawnLock(args) {
17425
17794
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
17426
17795
  const selfPid = options.selfPid ?? process.pid;
17427
17796
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
17428
- const path = join8(agentDir, basename2);
17797
+ const path = join12(agentDir, basename2);
17429
17798
  const existing = readLockHolder(path);
17430
17799
  if (existing) {
17431
17800
  if (existing.pid === selfPid) {
@@ -17438,7 +17807,7 @@ function acquireMcpSpawnLock(args) {
17438
17807
  mkdirSync7(agentDir, { recursive: true, mode: 448 });
17439
17808
  const tmpPath = `${path}.${selfPid}.tmp`;
17440
17809
  const payload = { pid: selfPid, started_at: now() };
17441
- writeFileSync9(tmpPath, JSON.stringify(payload), { mode: 384 });
17810
+ writeFileSync11(tmpPath, JSON.stringify(payload), { mode: 384 });
17442
17811
  renameSync3(tmpPath, path);
17443
17812
  return { kind: "acquired", path };
17444
17813
  }
@@ -17456,7 +17825,7 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
17456
17825
  function readLockHolder(path) {
17457
17826
  if (!existsSync7(path)) return null;
17458
17827
  try {
17459
- const raw = readFileSync9(path, "utf8");
17828
+ const raw = readFileSync13(path, "utf8");
17460
17829
  const parsed = JSON.parse(raw);
17461
17830
  const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
17462
17831
  if (!Number.isFinite(pid) || pid <= 0) return null;
@@ -17690,8 +18059,8 @@ var SLACK_TEAM_PEER_USER_IDS = parseTeamPeerUserIdsEnv(
17690
18059
  process.env.SLACK_TEAM_PEER_USER_IDS
17691
18060
  );
17692
18061
  var PEER_HINT_SEEN = /* @__PURE__ */ new Set();
17693
- var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join9(homedir4(), ".augmented", AGENT_CODE_NAME) : null;
17694
- var SLACK_MCP_CONFIG_PATH = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "project", ".mcp.json") : null;
18062
+ var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join13(homedir4(), ".augmented", AGENT_CODE_NAME) : null;
18063
+ var SLACK_MCP_CONFIG_PATH = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "project", ".mcp.json") : null;
17695
18064
  var liveAllowedUsersCache = null;
17696
18065
  function readLiveAllowedUsers() {
17697
18066
  if (!SLACK_MCP_CONFIG_PATH) return null;
@@ -17701,7 +18070,7 @@ function readLiveAllowedUsers() {
17701
18070
  return liveAllowedUsersCache.value;
17702
18071
  }
17703
18072
  const value = extractAllowedUsersFromMcpJson(
17704
- readFileSync10(SLACK_MCP_CONFIG_PATH, "utf-8")
18073
+ readFileSync14(SLACK_MCP_CONFIG_PATH, "utf-8")
17705
18074
  );
17706
18075
  if (value === null) return null;
17707
18076
  liveAllowedUsersCache = { mtimeMs, value };
@@ -17722,7 +18091,7 @@ function readLivePingAllowedUsers() {
17722
18091
  return livePingAllowedUsersCache.value;
17723
18092
  }
17724
18093
  const value = extractPingAllowedUsersFromMcpJson(
17725
- readFileSync10(SLACK_MCP_CONFIG_PATH, "utf-8")
18094
+ readFileSync14(SLACK_MCP_CONFIG_PATH, "utf-8")
17726
18095
  );
17727
18096
  if (value === null) return null;
17728
18097
  livePingAllowedUsersCache = { mtimeMs, value };
@@ -17734,16 +18103,17 @@ function readLivePingAllowedUsers() {
17734
18103
  function getEffectivePingAllowedUsers() {
17735
18104
  return readLivePingAllowedUsers() ?? PING_ALLOWED_USERS;
17736
18105
  }
17737
- var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
17738
- var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
17739
- var SLACK_RESTART_CONFIRM_FILE = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-restart-confirm.json") : null;
17740
- var SLACK_RECENT_DMS_FILE = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-recent-dms.json") : null;
17741
- var SLACK_CHANNEL_ADD_RESTART_FILE = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-channel-add-restart.json") : null;
18106
+ var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
18107
+ var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
18108
+ var slackInboundRegistry = createInboundRegistry();
18109
+ var SLACK_RESTART_CONFIRM_FILE = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-restart-confirm.json") : null;
18110
+ var SLACK_RECENT_DMS_FILE = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-recent-dms.json") : null;
18111
+ var SLACK_CHANNEL_ADD_RESTART_FILE = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-channel-add-restart.json") : null;
17742
18112
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
17743
- var SLACK_AVATAR_MARKER_PATH = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-avatar-applied") : null;
18113
+ var SLACK_AVATAR_MARKER_PATH = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-avatar-applied") : null;
17744
18114
  function redactSlackId(id) {
17745
18115
  if (!id) return "<none>";
17746
- return createHash("sha256").update(id).digest("hex").slice(0, 8);
18116
+ return createHash2("sha256").update(id).digest("hex").slice(0, 8);
17747
18117
  }
17748
18118
  function safeSlackMarkerName(channel, threadTs, messageTs) {
17749
18119
  const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
@@ -17751,7 +18121,7 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
17751
18121
  }
17752
18122
  function slackPendingInboundPath(channel, threadTs, messageTs) {
17753
18123
  if (!SLACK_PENDING_INBOUND_DIR) return null;
17754
- return join9(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
18124
+ return join13(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
17755
18125
  }
17756
18126
  function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false, discretionary = false, payload) {
17757
18127
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
@@ -17761,6 +18131,10 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undelivera
17761
18131
  thread_ts: threadTs,
17762
18132
  message_ts: messageTs,
17763
18133
  received_at: (/* @__PURE__ */ new Date()).toISOString(),
18134
+ // WS2: the deterministic inbound id. Always written (derivable from the
18135
+ // coords, marker-semantics-neutral) so a marker recovered after a restart
18136
+ // carries the same id the tag emitted.
18137
+ inbound_id: slackInboundId(channel, threadTs, messageTs),
17764
18138
  // Only persist the flags when set — keeps healthy-path markers byte-identical.
17765
18139
  ...undeliverable ? { undeliverable: true } : {},
17766
18140
  ...discretionary ? { discretionary: true } : {},
@@ -17770,7 +18144,7 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undelivera
17770
18144
  };
17771
18145
  try {
17772
18146
  mkdirSync8(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
17773
- writeFileSync10(path, JSON.stringify(marker), { mode: 384 });
18147
+ writeFileSync12(path, JSON.stringify(marker), { mode: 384 });
17774
18148
  } catch (err) {
17775
18149
  process.stderr.write(
17776
18150
  `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
@@ -17798,7 +18172,7 @@ function rewriteSlackMarkerInPlace(path, marker) {
17798
18172
  function markSlackMarkerSeenInPlace(fullPath) {
17799
18173
  let marker;
17800
18174
  try {
17801
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
18175
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
17802
18176
  } catch {
17803
18177
  return;
17804
18178
  }
@@ -17812,7 +18186,7 @@ function attachSlackReplayPayload(channel, threadTs, messageTs, payload) {
17812
18186
  if (!path) return;
17813
18187
  let marker;
17814
18188
  try {
17815
- marker = JSON.parse(readFileSync10(path, "utf-8"));
18189
+ marker = JSON.parse(readFileSync14(path, "utf-8"));
17816
18190
  } catch {
17817
18191
  return;
17818
18192
  }
@@ -17823,7 +18197,7 @@ function readSlackPendingInboundMarker(channel, threadTs, messageTs) {
17823
18197
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
17824
18198
  if (!path || !existsSync8(path)) return null;
17825
18199
  try {
17826
- return JSON.parse(readFileSync10(path, "utf-8"));
18200
+ return JSON.parse(readFileSync14(path, "utf-8"));
17827
18201
  } catch {
17828
18202
  return null;
17829
18203
  }
@@ -17918,7 +18292,7 @@ function scheduleBusyAck(channel, threadTs, messageTs, isThreadReply, arrivedWhi
17918
18292
  let paneLogFreshAgeMs = null;
17919
18293
  if (SLACK_AGENT_DIR) {
17920
18294
  try {
17921
- const paneMtimeMs = statSync2(join9(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
18295
+ const paneMtimeMs = statSync2(join13(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
17922
18296
  paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
17923
18297
  } catch {
17924
18298
  }
@@ -17951,7 +18325,7 @@ function __resetSlackBusyAckNoticeThrottle() {
17951
18325
  function clearSlackMarkerFileWithHeal(fullPath) {
17952
18326
  let marker = null;
17953
18327
  try {
17954
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
18328
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
17955
18329
  } catch {
17956
18330
  }
17957
18331
  if (marker && decideRecoveryHeal({
@@ -17968,7 +18342,7 @@ function clearSlackMarkerFileWithHeal(fullPath) {
17968
18342
  function markSlackMarkerSeenWithHeal(fullPath) {
17969
18343
  let marker = null;
17970
18344
  try {
17971
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
18345
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
17972
18346
  } catch {
17973
18347
  return;
17974
18348
  }
@@ -17987,6 +18361,7 @@ function clearAllSlackPendingMarkersForThread2(channel, threadTs) {
17987
18361
  threadTs,
17988
18362
  clearSlackMarkerFileWithHeal
17989
18363
  );
18364
+ slackInboundRegistry.markClearedThread(channel, threadTs);
17990
18365
  }
17991
18366
  function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
17992
18367
  clearSlackPendingMarkerByMessageTs(
@@ -17995,6 +18370,7 @@ function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
17995
18370
  messageTs,
17996
18371
  clearSlackMarkerFileWithHeal
17997
18372
  );
18373
+ slackInboundRegistry.markCleared(channel, messageTs);
17998
18374
  }
17999
18375
  function clearOldestSlackPendingMarkerInChannel2(channel) {
18000
18376
  clearOldestSlackPendingMarkerInChannel(
@@ -18002,6 +18378,7 @@ function clearOldestSlackPendingMarkerInChannel2(channel) {
18002
18378
  channel,
18003
18379
  clearSlackMarkerFileWithHeal
18004
18380
  );
18381
+ slackInboundRegistry.markClearedOldestInChannel(channel);
18005
18382
  }
18006
18383
  function markSeenAllSlackPendingMarkersForThread2(channel, threadTs) {
18007
18384
  markSeenAllSlackPendingMarkersForThread(
@@ -18033,10 +18410,10 @@ function slackNextRetryName(filename) {
18033
18410
  async function processSlackRecoveryOutboxFile(filename) {
18034
18411
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
18035
18412
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
18036
- const fullPath = join9(SLACK_RECOVERY_OUTBOX_DIR, filename);
18413
+ const fullPath = join13(SLACK_RECOVERY_OUTBOX_DIR, filename);
18037
18414
  let payload;
18038
18415
  try {
18039
- payload = JSON.parse(readFileSync10(fullPath, "utf-8"));
18416
+ payload = JSON.parse(readFileSync14(fullPath, "utf-8"));
18040
18417
  } catch (err) {
18041
18418
  process.stderr.write(
18042
18419
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
@@ -18136,7 +18513,7 @@ async function processSlackRecoveryOutboxFile(filename) {
18136
18513
  const next = slackNextRetryName(filename);
18137
18514
  if (next) {
18138
18515
  try {
18139
- renameSync4(fullPath, join9(SLACK_RECOVERY_OUTBOX_DIR, next.next));
18516
+ renameSync4(fullPath, join13(SLACK_RECOVERY_OUTBOX_DIR, next.next));
18140
18517
  if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
18141
18518
  process.stderr.write(
18142
18519
  `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -18166,7 +18543,7 @@ function scanSlackRecoveryRetries() {
18166
18543
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
18167
18544
  let entries;
18168
18545
  try {
18169
- entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
18546
+ entries = readdirSync4(SLACK_RECOVERY_OUTBOX_DIR);
18170
18547
  } catch {
18171
18548
  return;
18172
18549
  }
@@ -18175,7 +18552,7 @@ function scanSlackRecoveryRetries() {
18175
18552
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
18176
18553
  let mtimeMs;
18177
18554
  try {
18178
- mtimeMs = statSync2(join9(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
18555
+ mtimeMs = statSync2(join13(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
18179
18556
  } catch {
18180
18557
  continue;
18181
18558
  }
@@ -18196,7 +18573,7 @@ function startSlackRecoveryOutboxWatcher() {
18196
18573
  return;
18197
18574
  }
18198
18575
  try {
18199
- for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
18576
+ for (const f of readdirSync4(SLACK_RECOVERY_OUTBOX_DIR)) {
18200
18577
  if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
18201
18578
  }
18202
18579
  } catch {
@@ -18205,7 +18582,7 @@ function startSlackRecoveryOutboxWatcher() {
18205
18582
  const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
18206
18583
  if (event !== "rename" || !filename) return;
18207
18584
  if (!isFirstAttemptSlackOutboxFile(filename)) return;
18208
- if (existsSync8(join9(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
18585
+ if (existsSync8(join13(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
18209
18586
  void processSlackRecoveryOutboxFile(filename);
18210
18587
  }
18211
18588
  });
@@ -18220,7 +18597,7 @@ function startSlackRecoveryOutboxWatcher() {
18220
18597
  retryTimer.unref?.();
18221
18598
  }
18222
18599
  startSlackRecoveryOutboxWatcher();
18223
- var SLACK_NOTICE_OUTBOX_DIR = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "slack-notice-outbox") : null;
18600
+ var SLACK_NOTICE_OUTBOX_DIR = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "slack-notice-outbox") : null;
18224
18601
  var SLACK_NOTICE_MAX_AGE_MS = 9e4;
18225
18602
  var SLACK_NOTICE_INFLIGHT = /* @__PURE__ */ new Set();
18226
18603
  async function processSlackNoticeOutboxFile(filename) {
@@ -18228,7 +18605,7 @@ async function processSlackNoticeOutboxFile(filename) {
18228
18605
  if (filename.startsWith(".") || filename.endsWith(".tmp") || !filename.endsWith(".json")) return;
18229
18606
  if (SLACK_NOTICE_INFLIGHT.has(filename)) return;
18230
18607
  SLACK_NOTICE_INFLIGHT.add(filename);
18231
- const fullPath = join9(SLACK_NOTICE_OUTBOX_DIR, filename);
18608
+ const fullPath = join13(SLACK_NOTICE_OUTBOX_DIR, filename);
18232
18609
  try {
18233
18610
  let mtimeMs;
18234
18611
  try {
@@ -18245,7 +18622,7 @@ async function processSlackNoticeOutboxFile(filename) {
18245
18622
  }
18246
18623
  let payload;
18247
18624
  try {
18248
- const parsed = JSON.parse(readFileSync10(fullPath, "utf-8"));
18625
+ const parsed = JSON.parse(readFileSync14(fullPath, "utf-8"));
18249
18626
  if (!parsed || typeof parsed !== "object") throw new Error("not an object");
18250
18627
  payload = parsed;
18251
18628
  } catch {
@@ -18328,13 +18705,13 @@ function startSlackNoticeOutboxWatcher() {
18328
18705
  return;
18329
18706
  }
18330
18707
  try {
18331
- for (const f of readdirSync3(SLACK_NOTICE_OUTBOX_DIR)) void processSlackNoticeOutboxFile(f);
18708
+ for (const f of readdirSync4(SLACK_NOTICE_OUTBOX_DIR)) void processSlackNoticeOutboxFile(f);
18332
18709
  } catch {
18333
18710
  }
18334
18711
  try {
18335
18712
  const watcher = watch(SLACK_NOTICE_OUTBOX_DIR, (event, filename) => {
18336
18713
  if (event !== "rename" || !filename) return;
18337
- if (existsSync8(join9(SLACK_NOTICE_OUTBOX_DIR, filename))) {
18714
+ if (existsSync8(join13(SLACK_NOTICE_OUTBOX_DIR, filename))) {
18338
18715
  void processSlackNoticeOutboxFile(filename);
18339
18716
  }
18340
18717
  });
@@ -18357,7 +18734,7 @@ function sweepSlackStaleMarkers(thresholdMs) {
18357
18734
  if (!existsSync8(SLACK_PENDING_INBOUND_DIR)) return;
18358
18735
  let filenames;
18359
18736
  try {
18360
- filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
18737
+ filenames = readdirSync4(SLACK_PENDING_INBOUND_DIR);
18361
18738
  } catch (err) {
18362
18739
  process.stderr.write(
18363
18740
  `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -18370,10 +18747,10 @@ function sweepSlackStaleMarkers(thresholdMs) {
18370
18747
  for (const filename of filenames) {
18371
18748
  if (!filename.endsWith(".json")) continue;
18372
18749
  if (filename.endsWith(".tmp")) continue;
18373
- const fullPath = join9(SLACK_PENDING_INBOUND_DIR, filename);
18750
+ const fullPath = join13(SLACK_PENDING_INBOUND_DIR, filename);
18374
18751
  let marker;
18375
18752
  try {
18376
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
18753
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
18377
18754
  } catch (err) {
18378
18755
  process.stderr.write(
18379
18756
  `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
@@ -18409,6 +18786,9 @@ function sweepSlackStaleMarkers(thresholdMs) {
18409
18786
  }
18410
18787
  }
18411
18788
  sweepSlackStaleMarkers(STALE_MARKER_MS);
18789
+ if (SLACK_PENDING_INBOUND_DIR) {
18790
+ slackInboundRegistry.seedFromMarkerDir(SLACK_PENDING_INBOUND_DIR);
18791
+ }
18412
18792
  var slackOrphanSweepTimer = setInterval(() => {
18413
18793
  const probe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
18414
18794
  const sessionAlive = probe.tmux === "alive" && probe.claude === "alive";
@@ -18416,13 +18796,13 @@ var slackOrphanSweepTimer = setInterval(() => {
18416
18796
  checkSlackWatchdogGiveUpNotice();
18417
18797
  }, orphanSweepIntervalMs());
18418
18798
  slackOrphanSweepTimer.unref?.();
18419
- var SLACK_PROGRESS_HEARTBEAT_PATH = SLACK_AGENT_DIR ? join9(SLACK_AGENT_DIR, "channel-progress-heartbeat.json") : null;
18799
+ var SLACK_PROGRESS_HEARTBEAT_PATH = SLACK_AGENT_DIR ? join13(SLACK_AGENT_DIR, "channel-progress-heartbeat.json") : null;
18420
18800
  var slackTrackedProgress = null;
18421
18801
  var slackProgressTickRunning = false;
18422
18802
  function readSlackProgressHeartbeat() {
18423
18803
  if (!SLACK_PROGRESS_HEARTBEAT_PATH || !existsSync8(SLACK_PROGRESS_HEARTBEAT_PATH)) return null;
18424
18804
  try {
18425
- return parseProgressHeartbeat(readFileSync10(SLACK_PROGRESS_HEARTBEAT_PATH, "utf-8"));
18805
+ return parseProgressHeartbeat(readFileSync14(SLACK_PROGRESS_HEARTBEAT_PATH, "utf-8"));
18426
18806
  } catch {
18427
18807
  return null;
18428
18808
  }
@@ -18431,7 +18811,7 @@ function seedSlackProgressHeartbeat() {
18431
18811
  if (!SLACK_PROGRESS_HEARTBEAT_PATH) return;
18432
18812
  const tmp = `${SLACK_PROGRESS_HEARTBEAT_PATH}.${process.pid}.tmp`;
18433
18813
  try {
18434
- writeFileSync10(tmp, serializeProgressHeartbeat(SEED_PROGRESS_STEP, Date.now()), { mode: 384 });
18814
+ writeFileSync12(tmp, serializeProgressHeartbeat(SEED_PROGRESS_STEP, Date.now()), { mode: 384 });
18435
18815
  renameSync4(tmp, SLACK_PROGRESS_HEARTBEAT_PATH);
18436
18816
  } catch {
18437
18817
  try {
@@ -18445,11 +18825,11 @@ function findSlackProgressTarget() {
18445
18825
  let best = null;
18446
18826
  let bestMs = Infinity;
18447
18827
  try {
18448
- for (const name of readdirSync3(SLACK_PENDING_INBOUND_DIR)) {
18828
+ for (const name of readdirSync4(SLACK_PENDING_INBOUND_DIR)) {
18449
18829
  if (!name.endsWith(".json")) continue;
18450
18830
  let m;
18451
18831
  try {
18452
- m = JSON.parse(readFileSync10(join9(SLACK_PENDING_INBOUND_DIR, name), "utf-8"));
18832
+ m = JSON.parse(readFileSync14(join13(SLACK_PENDING_INBOUND_DIR, name), "utf-8"));
18453
18833
  } catch {
18454
18834
  continue;
18455
18835
  }
@@ -18596,11 +18976,11 @@ function listPendingSlackConversations() {
18596
18976
  if (!SLACK_PENDING_INBOUND_DIR || !existsSync8(SLACK_PENDING_INBOUND_DIR)) return [];
18597
18977
  const byKey = /* @__PURE__ */ new Map();
18598
18978
  try {
18599
- for (const name of readdirSync3(SLACK_PENDING_INBOUND_DIR)) {
18979
+ for (const name of readdirSync4(SLACK_PENDING_INBOUND_DIR)) {
18600
18980
  if (!name.endsWith(".json")) continue;
18601
18981
  try {
18602
18982
  const marker = JSON.parse(
18603
- readFileSync10(join9(SLACK_PENDING_INBOUND_DIR, name), "utf8")
18983
+ readFileSync14(join13(SLACK_PENDING_INBOUND_DIR, name), "utf8")
18604
18984
  );
18605
18985
  if (typeof marker.channel !== "string" || !marker.channel) continue;
18606
18986
  if (typeof marker.thread_ts !== "string" || !marker.thread_ts) continue;
@@ -18652,7 +19032,7 @@ function postSlackWatchdogGiveUpNotice(channel, threadTs, isThreadReply, reason)
18652
19032
  }
18653
19033
  function checkSlackWatchdogGiveUpNotice() {
18654
19034
  if (!SLACK_AGENT_DIR) return;
18655
- const signal = readGiveUpSignal(join9(SLACK_AGENT_DIR, GIVE_UP_SIGNAL_FILENAME));
19035
+ const signal = readGiveUpSignal(join13(SLACK_AGENT_DIR, GIVE_UP_SIGNAL_FILENAME));
18656
19036
  const signalAtMs = signal?.atMs ?? null;
18657
19037
  const act = decideGiveUpNotice({
18658
19038
  signalAtMs,
@@ -18689,7 +19069,7 @@ async function notifyStrandedInboundsOnFirstConnect() {
18689
19069
  if (!SLACK_PENDING_INBOUND_DIR || !existsSync8(SLACK_PENDING_INBOUND_DIR)) return;
18690
19070
  let filenames;
18691
19071
  try {
18692
- filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
19072
+ filenames = readdirSync4(SLACK_PENDING_INBOUND_DIR);
18693
19073
  } catch {
18694
19074
  hadFailure = true;
18695
19075
  return;
@@ -18699,10 +19079,10 @@ async function notifyStrandedInboundsOnFirstConnect() {
18699
19079
  let notified = 0;
18700
19080
  for (const filename of filenames) {
18701
19081
  if (!filename.endsWith(".json")) continue;
18702
- const fullPath = join9(SLACK_PENDING_INBOUND_DIR, filename);
19082
+ const fullPath = join13(SLACK_PENDING_INBOUND_DIR, filename);
18703
19083
  let marker;
18704
19084
  try {
18705
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
19085
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
18706
19086
  } catch {
18707
19087
  continue;
18708
19088
  }
@@ -18873,7 +19253,7 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
18873
19253
  markSeenAllSlackPendingMarkersForThread2(channel, messageTs);
18874
19254
  markSeenSlackPendingMarkerByMessageTs2(channel, messageTs);
18875
19255
  }
18876
- var RESTART_FLAGS_DIR = join9(homedir4(), ".augmented", "restart-flags");
19256
+ var RESTART_FLAGS_DIR = join13(homedir4(), ".augmented", "restart-flags");
18877
19257
  function actuateHostRestartSlack() {
18878
19258
  return actuateHostRestart({
18879
19259
  agtHost: AGT_HOST,
@@ -18894,10 +19274,10 @@ function buildAugmentedSlackMetadata() {
18894
19274
  };
18895
19275
  }
18896
19276
  function hashChannelId(id) {
18897
- return createHash("sha256").update(id).digest("hex").slice(0, 8);
19277
+ return createHash2("sha256").update(id).digest("hex").slice(0, 8);
18898
19278
  }
18899
19279
  function hashId(id) {
18900
- return createHash("sha256").update(id).digest("hex").slice(0, 8);
19280
+ return createHash2("sha256").update(id).digest("hex").slice(0, 8);
18901
19281
  }
18902
19282
  async function postSlackMessage(body) {
18903
19283
  const augmentedMeta = buildAugmentedSlackMetadata();
@@ -19419,7 +19799,7 @@ async function handleSlashCommandEnvelope(payload) {
19419
19799
  if (!existsSync8(RESTART_FLAGS_DIR)) {
19420
19800
  mkdirSync8(RESTART_FLAGS_DIR, { recursive: true });
19421
19801
  }
19422
- const flagPath = join9(RESTART_FLAGS_DIR, `${codeName}.flag`);
19802
+ const flagPath = join13(RESTART_FLAGS_DIR, `${codeName}.flag`);
19423
19803
  const flag = {
19424
19804
  codeName,
19425
19805
  source: "slack",
@@ -19427,7 +19807,7 @@ async function handleSlashCommandEnvelope(payload) {
19427
19807
  reply: confirmReply
19428
19808
  };
19429
19809
  const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
19430
- writeFileSync10(tmpPath, JSON.stringify(flag) + "\n", "utf8");
19810
+ writeFileSync12(tmpPath, JSON.stringify(flag) + "\n", "utf8");
19431
19811
  renameSync4(tmpPath, flagPath);
19432
19812
  }
19433
19813
  process.stderr.write(
@@ -19562,7 +19942,7 @@ async function handleRestartCommand(opts) {
19562
19942
  if (!existsSync8(RESTART_FLAGS_DIR)) {
19563
19943
  mkdirSync8(RESTART_FLAGS_DIR, { recursive: true });
19564
19944
  }
19565
- const flagPath = join9(RESTART_FLAGS_DIR, `${codeName}.flag`);
19945
+ const flagPath = join13(RESTART_FLAGS_DIR, `${codeName}.flag`);
19566
19946
  const flag = {
19567
19947
  codeName,
19568
19948
  source: "slack",
@@ -19570,7 +19950,7 @@ async function handleRestartCommand(opts) {
19570
19950
  reply: { ...confirmReply, message_ts: opts.ts }
19571
19951
  };
19572
19952
  const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
19573
- writeFileSync10(tmpPath, JSON.stringify(flag) + "\n", "utf8");
19953
+ writeFileSync12(tmpPath, JSON.stringify(flag) + "\n", "utf8");
19574
19954
  renameSync4(tmpPath, flagPath);
19575
19955
  }
19576
19956
  process.stderr.write(
@@ -19675,7 +20055,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
19675
20055
  var threadPersister = null;
19676
20056
  function resolveThreadStorePath() {
19677
20057
  if (!AGENT_CODE_NAME) return null;
19678
- return join9(homedir4(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
20058
+ return join13(homedir4(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
19679
20059
  }
19680
20060
  function parseTtlDays(raw) {
19681
20061
  if (!raw) return void 0;
@@ -19714,9 +20094,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
19714
20094
  var slackStderrLogStream = null;
19715
20095
  if (AGENT_CODE_NAME) {
19716
20096
  try {
19717
- const logDir = join9(homedir4(), ".augmented", AGENT_CODE_NAME);
20097
+ const logDir = join13(homedir4(), ".augmented", AGENT_CODE_NAME);
19718
20098
  mkdirSync8(logDir, { recursive: true });
19719
- slackStderrLogStream = createWriteStream(join9(logDir, "slack-channel-stderr.log"), {
20099
+ slackStderrLogStream = createWriteStream(join13(logDir, "slack-channel-stderr.log"), {
19720
20100
  flags: "a",
19721
20101
  mode: 384
19722
20102
  });
@@ -19795,20 +20175,13 @@ var mcp = new Server(
19795
20175
  tools: {}
19796
20176
  },
19797
20177
  // NOTE: Claude Code truncates MCP server instructions at 2048 chars.
19798
- // Attachments rules live near the top because they get chopped otherwise
19799
- // and the agent silently loses attachment-handling guidance.
19800
- instructions: [
19801
- // Highest-priority lines first — Claude Code truncates this string at
19802
- // 2048 chars, so anything appended late silently disappears.
19803
- "CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply \u2014 typed text never reaches the user. Slow work: interim ack (slack.reply interim:true), then your FINAL answer as a separate slack.reply (interim omitted). An ack is not the answer.",
19804
- `Inbound: <channel ... thread_ts="..." message_ts="..." [thread_context="..."]>. Pass channel + message_ts to slack.reply (and thread_ts on threads). thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
19805
- "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.",
19806
- "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.",
19807
- `Always reply in a thread \u2014 pass message_ts and the server threads your reply off it, for channels AND DMs (it creates the thread if one doesn't exist yet). auto_followed="true" \u2192 only reply if useful, OR if your own bot user is @-mentioned (counts even in auto_followed).`,
19808
- `Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): :${SLACK_ACK_REACTION}: = ack (already auto-added on inbound, do not duplicate); \u2705 = success.` + (SKIP_REACTION_ON ? ` "${SLACK_SKIP_REACTION}" = seen but not replying (see below).` : "") + " NEVER react to signal failure of YOUR work \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets). (The \u23F3 you may see on an inbound is applied by the system, not you \u2014 it marks a message that arrived while the agent couldn't reply; never add \u23F3 or \u274C yourself.)",
19809
- SKIP_REACTION_ON ? `When you read a DM or thread message and decide NOT to reply (different @-mention, side conversation, auto_followed catch-up, or simply nothing useful to add): call slack.react with emoji "${SLACK_SKIP_REACTION}" so the sender knows you saw it \u2014 never post a "this wasn't for me" message. Top-level channel messages (not a DM, not a thread reply) you may still skip silently.` : `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.`,
19810
- "To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
19811
- ].join(" ")
20178
+ // Built by a pure, length-tested builder (slack-mcp-instructions.ts) so the
20179
+ // routing guidance can't silently fall off the end (ENG-6296 regression).
20180
+ instructions: buildSlackMcpInstructions({
20181
+ ackReaction: SLACK_ACK_REACTION,
20182
+ skipReactionOn: SKIP_REACTION_ON,
20183
+ skipReaction: SLACK_SKIP_REACTION
20184
+ })
19812
20185
  }
19813
20186
  );
19814
20187
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -19844,6 +20217,15 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
19844
20217
  interim: {
19845
20218
  type: "boolean",
19846
20219
  description: 'Set true ONLY when this is an interim acknowledgement (e.g. "On it \u2014 checking now\u2026") and the real answer is still coming. The system keeps tracking this conversation as awaiting your final reply. Your eventual substantive answer MUST be a separate slack.reply with interim omitted/false \u2014 that is what marks the request complete. Omit (default false) for any reply that fully answers the user.'
20220
+ },
20221
+ // WS2 (cross-thread reply routing): the preferred way to target a reply.
20222
+ inbound_id: {
20223
+ type: "string",
20224
+ description: `The inbound_id attribute of the <channel> tag you are answering (e.g. "ib_a1b2c3d4e5f6"). PREFERRED over channel/thread_ts/message_ts: the server resolves the exact destination thread from it, so your reply can never land in another user's thread. Copy it verbatim from the tag of the message you are replying to.`
20225
+ },
20226
+ proactive: {
20227
+ type: "boolean",
20228
+ description: "Set true ONLY when this message is NOT a reply to any inbound <channel> tag - a self-initiated outreach or scheduled-task output. Leave it off (default) for every reply to a user message."
19847
20229
  }
19848
20230
  },
19849
20231
  required: ["channel", "text"]
@@ -20075,12 +20457,58 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20075
20457
  return buildImpersonationRefusal(name);
20076
20458
  }
20077
20459
  if (name === "slack.reply") {
20078
- const { channel, text, thread_ts, message_ts, interim } = args;
20079
- const effectiveThreadTs = resolveReplyThreadTs({
20460
+ const { channel, text, thread_ts, message_ts, interim, inbound_id, proactive } = args;
20461
+ const bindingMode = slackReplyBindingMode();
20462
+ const binding = resolveReplyBinding({
20463
+ mode: bindingMode,
20464
+ args: { inbound_id, channel, thread_ts, message_ts, proactive },
20465
+ registry: slackInboundRegistry
20466
+ });
20467
+ recordReplyBindingClassification(SLACK_AGENT_DIR, "slack", {
20468
+ classification: binding.classification,
20469
+ unknownInboundId: binding.unknownInboundId
20470
+ });
20471
+ if (bindingMode === "enforce" && binding.reject) {
20472
+ process.stderr.write(
20473
+ `slack-channel(${AGENT_CODE_NAME}): reply_binding_rejected class=${binding.classification} channel=${redactSlackId(channel)}
20474
+ `
20475
+ );
20476
+ return { content: [{ type: "text", text: binding.reject }], isError: true };
20477
+ }
20478
+ const enforceBound = bindingMode === "enforce" && (binding.classification === "bound" || binding.classification === "coords_corrected");
20479
+ if (enforceBound && binding.channel && binding.channel !== channel) {
20480
+ process.stderr.write(
20481
+ `slack-channel(${AGENT_CODE_NAME}): reply_binding_channel_mismatch_rejected class=${binding.classification} model=${redactSlackId(channel)} registry=${redactSlackId(binding.channel)}
20482
+ `
20483
+ );
20484
+ return {
20485
+ content: [
20486
+ {
20487
+ type: "text",
20488
+ text: "This reply was blocked: the channel does not match the inbound_id you named, so it may go to the wrong place. Reply again using the channel from the <channel> tag whose inbound_id you are answering."
20489
+ }
20490
+ ],
20491
+ isError: true
20492
+ };
20493
+ }
20494
+ const bindingFeedback = bindingMode === "warn" ? binding.agentFeedback : void 0;
20495
+ const effectiveThreadTs = enforceBound ? binding.threadTs : resolveReplyThreadTs({
20080
20496
  channel,
20081
20497
  threadTs: thread_ts,
20082
20498
  messageTs: message_ts
20083
20499
  });
20500
+ if (channel) {
20501
+ recordReplyTargetClassification(
20502
+ SLACK_AGENT_DIR,
20503
+ "slack",
20504
+ classifyReplyTarget({
20505
+ channel,
20506
+ threadTs: effectiveThreadTs,
20507
+ messageTs: message_ts,
20508
+ registry: slackInboundRegistry
20509
+ })
20510
+ );
20511
+ }
20084
20512
  const throttleCfg = configFromEnv();
20085
20513
  const throttleKey = effectiveThreadTs ?? channel;
20086
20514
  const throttleNow = Date.now();
@@ -20192,7 +20620,9 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20192
20620
  }
20193
20621
  })();
20194
20622
  }
20195
- return { content: [{ type: "text", text: "sent" }] };
20623
+ return {
20624
+ content: [{ type: "text", text: bindingFeedback ? `sent. ${bindingFeedback}` : "sent" }]
20625
+ };
20196
20626
  } catch (err) {
20197
20627
  return {
20198
20628
  content: [{ type: "text", text: `Failed: ${err.message}` }],
@@ -20308,7 +20738,7 @@ ${result.formatted}` : "Thread is empty or not found."
20308
20738
  };
20309
20739
  }
20310
20740
  size = stat2.size;
20311
- bytes = readFileSync10(resolvedPath);
20741
+ bytes = readFileSync14(resolvedPath);
20312
20742
  } catch (err) {
20313
20743
  return {
20314
20744
  content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
@@ -20668,6 +21098,16 @@ async function handleSendStructured(args) {
20668
21098
  threadTs: thread_ts,
20669
21099
  messageTs: message_ts
20670
21100
  });
21101
+ recordReplyTargetClassification(
21102
+ SLACK_AGENT_DIR,
21103
+ "slack",
21104
+ classifyReplyTarget({
21105
+ channel,
21106
+ threadTs: effectiveThreadTs,
21107
+ messageTs: message_ts,
21108
+ registry: slackInboundRegistry
21109
+ })
21110
+ );
20671
21111
  const validation = validateSlackBlocks2(blocks);
20672
21112
  if (!validation.ok) {
20673
21113
  return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
@@ -21010,7 +21450,7 @@ async function downloadSlackFile(fileId, codeName) {
21010
21450
  throw new Error(`refusing to write ${savedPath} outside ${dir}`);
21011
21451
  }
21012
21452
  mkdirSync8(dir, { recursive: true });
21013
- writeFileSync10(savedPath, bytes, { mode: 384 });
21453
+ writeFileSync12(savedPath, bytes, { mode: 384 });
21014
21454
  try {
21015
21455
  chmodSync(savedPath, 384);
21016
21456
  } catch {
@@ -21071,7 +21511,7 @@ async function replayPendingSlackMarkers() {
21071
21511
  if (!sessionAlive) return;
21072
21512
  let filenames;
21073
21513
  try {
21074
- filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
21514
+ filenames = readdirSync4(SLACK_PENDING_INBOUND_DIR);
21075
21515
  } catch {
21076
21516
  return;
21077
21517
  }
@@ -21079,17 +21519,17 @@ async function replayPendingSlackMarkers() {
21079
21519
  let paneFreshAgeMs = null;
21080
21520
  if (SLACK_AGENT_DIR) {
21081
21521
  try {
21082
- paneFreshAgeMs = Math.max(0, now - statSync2(join9(SLACK_AGENT_DIR, "pane.log")).mtimeMs);
21522
+ paneFreshAgeMs = Math.max(0, now - statSync2(join13(SLACK_AGENT_DIR, "pane.log")).mtimeMs);
21083
21523
  } catch {
21084
21524
  }
21085
21525
  }
21086
21526
  const entries = [];
21087
21527
  for (const name of filenames) {
21088
21528
  if (!name.endsWith(".json") || name.endsWith(".tmp")) continue;
21089
- const fullPath = join9(SLACK_PENDING_INBOUND_DIR, name);
21529
+ const fullPath = join13(SLACK_PENDING_INBOUND_DIR, name);
21090
21530
  let marker;
21091
21531
  try {
21092
- marker = JSON.parse(readFileSync10(fullPath, "utf-8"));
21532
+ marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
21093
21533
  } catch {
21094
21534
  continue;
21095
21535
  }
@@ -21392,7 +21832,7 @@ async function connectSocketMode() {
21392
21832
  const passedIdentity = access.kind !== "drop" || access.reason.startsWith("peer:");
21393
21833
  if (passedIdentity) recordActivity("inbound");
21394
21834
  if (access.kind === "drop") {
21395
- const channelHash = createHash("sha256").update(evt.channel ?? "").digest("hex").slice(0, 8);
21835
+ const channelHash = createHash2("sha256").update(evt.channel ?? "").digest("hex").slice(0, 8);
21396
21836
  const subtypeSuffix = access.reason === "content:subtype" ? ` subtype=${evt.subtype ?? "none"}` : "";
21397
21837
  process.stderr.write(
21398
21838
  `slack-channel: inbound drop reason=${access.reason}${subtypeSuffix} channel=${channelHash} ts=${redactSlackId(evt.ts)}
@@ -21540,7 +21980,7 @@ async function connectSocketMode() {
21540
21980
  if (evt.type === "message" && evt.channel && !isDirectMessage) {
21541
21981
  if (isThreadReply) {
21542
21982
  const logThreadDrop = (reason) => {
21543
- const channelHash = createHash("sha256").update(evt.channel ?? "").digest("hex").slice(0, 8);
21983
+ const channelHash = createHash2("sha256").update(evt.channel ?? "").digest("hex").slice(0, 8);
21544
21984
  process.stderr.write(
21545
21985
  `slack-channel(${AGENT_CODE_NAME}): thread-reply drop reason=${reason} auto_follow=${THREAD_AUTO_FOLLOW} channel=${channelHash} ts=${redactSlackId(evt.ts)}
21546
21986
  `
@@ -21586,7 +22026,7 @@ async function connectSocketMode() {
21586
22026
  let paneLogFreshAgeMs = null;
21587
22027
  if (SLACK_AGENT_DIR) {
21588
22028
  try {
21589
- const paneMtimeMs = statSync2(join9(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
22029
+ const paneMtimeMs = statSync2(join13(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
21590
22030
  paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
21591
22031
  } catch {
21592
22032
  }
@@ -21688,6 +22128,7 @@ ${forwarded.text}` : forwarded.text;
21688
22128
  `
21689
22129
  );
21690
22130
  }
22131
+ const inboundId = channel && ts ? slackInboundId(channel, threadTs, ts) : "";
21691
22132
  const replayPayload = {
21692
22133
  content: inboundContent,
21693
22134
  meta: {
@@ -21695,6 +22136,7 @@ ${forwarded.text}` : forwarded.text;
21695
22136
  user_name: userName,
21696
22137
  channel,
21697
22138
  thread_ts: threadTs,
22139
+ ...inboundId ? { inbound_id: inboundId } : {},
21698
22140
  // ENG-5861: explicit message_ts so the agent can pass it back
21699
22141
  // to slack.reply for reliable pending-inbound cleanup. For
21700
22142
  // top-level messages threadTs === ts; for threaded replies
@@ -21725,6 +22167,15 @@ ${forwarded.text}` : forwarded.text;
21725
22167
  });
21726
22168
  if (channel && ts && armMarker) {
21727
22169
  attachSlackReplayPayload(channel, threadTs, ts, replayPayload);
22170
+ slackInboundRegistry.record({
22171
+ inboundId,
22172
+ channel,
22173
+ threadTs,
22174
+ messageTs: ts,
22175
+ user,
22176
+ userName,
22177
+ receivedAt: Date.now()
22178
+ });
21728
22179
  }
21729
22180
  if (!isFromBot && evt.user && channel) {
21730
22181
  writeTurnInitiatorMarker({