@integrity-labs/agt-cli 0.28.266 → 0.28.268

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.
@@ -16869,7 +16869,7 @@ import { join as join9 } from "path";
16869
16869
  var DEFAULT_MAX_ENTRIES = 500;
16870
16870
  var DEFAULT_CLEARED_TTL_MS = 6 * 60 * 60 * 1e3;
16871
16871
  function entryKey(channel, messageTs) {
16872
- return `${channel}\0${messageTs}`;
16872
+ return `${channel} ${messageTs}`;
16873
16873
  }
16874
16874
  function createInboundRegistry(opts = {}) {
16875
16875
  const maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
@@ -16946,6 +16946,27 @@ function createInboundRegistry(opts = {}) {
16946
16946
  }
16947
16947
  return best;
16948
16948
  }
16949
+ function byCoordsAnyChannel(threadOrMessageTs) {
16950
+ let best = null;
16951
+ let bestExact = false;
16952
+ for (const e of entries.values()) {
16953
+ const exact = e.messageTs === threadOrMessageTs;
16954
+ const thread = e.threadTs === threadOrMessageTs;
16955
+ if (!exact && !thread) continue;
16956
+ if (best === null) {
16957
+ best = e;
16958
+ bestExact = exact;
16959
+ continue;
16960
+ }
16961
+ if (exact && !bestExact) {
16962
+ best = e;
16963
+ bestExact = true;
16964
+ } else if (exact === bestExact && e.receivedAt >= best.receivedAt) {
16965
+ best = e;
16966
+ }
16967
+ }
16968
+ return best;
16969
+ }
16949
16970
  function livePending() {
16950
16971
  const out = [];
16951
16972
  for (const e of entries.values()) {
@@ -16994,6 +17015,7 @@ function createInboundRegistry(opts = {}) {
16994
17015
  markClearedOldestInChannel,
16995
17016
  byInboundId,
16996
17017
  byCoords,
17018
+ byCoordsAnyChannel,
16997
17019
  livePending,
16998
17020
  seedFromMarkerDir,
16999
17021
  size: () => entries.size
@@ -17059,6 +17081,33 @@ function resolveReplyBinding(input) {
17059
17081
  }
17060
17082
  return classifyByCoords(mode, args, registry2, now, false, knownThread);
17061
17083
  }
17084
+ function isDmChannel(channel) {
17085
+ return !!channel && channel.startsWith("D");
17086
+ }
17087
+ function guardReplyChannel(input) {
17088
+ const { args, registry: registry2 } = input;
17089
+ const noop = {
17090
+ channel: args.channel,
17091
+ threadTs: args.thread_ts,
17092
+ messageTs: args.message_ts,
17093
+ corrected: false,
17094
+ dm: false
17095
+ };
17096
+ if (args.inbound_id) return noop;
17097
+ if (args.proactive === true) return noop;
17098
+ if (!args.channel) return noop;
17099
+ const matched = (args.message_ts ? registry2.byCoordsAnyChannel(args.message_ts) : null) ?? (args.thread_ts ? registry2.byCoordsAnyChannel(args.thread_ts) : null);
17100
+ if (!matched) return noop;
17101
+ if (matched.channel === args.channel) return noop;
17102
+ return {
17103
+ channel: matched.channel,
17104
+ threadTs: matched.threadTs,
17105
+ messageTs: matched.messageTs,
17106
+ corrected: true,
17107
+ dm: isDmChannel(matched.channel),
17108
+ requestedChannel: args.channel
17109
+ };
17110
+ }
17062
17111
  function classifyByCoords(mode, args, registry2, now, unknownInboundId, knownThread) {
17063
17112
  const modelChannel = args.channel;
17064
17113
  const modelThread = args.thread_ts;
@@ -17121,6 +17170,8 @@ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from
17121
17170
  import { join as join10 } from "path";
17122
17171
  var REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX = "-reply-binding-classifications.json";
17123
17172
  var UNKNOWN_INBOUND_ID_KEY = "unknown_inbound_id";
17173
+ var CHANNEL_MISTARGET_CORRECTED_KEY = "channel_mistarget_corrected";
17174
+ var DM_CHANNEL_MISTARGET_KEY = "dm_channel_mistarget";
17124
17175
  var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "warn", "enforce"]);
17125
17176
  function slackReplyBindingMode() {
17126
17177
  const raw = process.env["AGT_SLACK_REPLY_BINDING"];
@@ -17145,6 +17196,24 @@ function recordReplyBindingClassification(agentDir, channel, input) {
17145
17196
  } catch {
17146
17197
  }
17147
17198
  }
17199
+ function recordChannelMistarget(agentDir, channel, input) {
17200
+ if (!agentDir) return;
17201
+ const path = join10(agentDir, `${channel}${REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX}`);
17202
+ let counts = {};
17203
+ try {
17204
+ const parsed = JSON.parse(readFileSync11(path, "utf-8"));
17205
+ if (parsed && typeof parsed === "object") counts = parsed;
17206
+ } catch {
17207
+ }
17208
+ counts[CHANNEL_MISTARGET_CORRECTED_KEY] = (counts[CHANNEL_MISTARGET_CORRECTED_KEY] ?? 0) + 1;
17209
+ if (input.dm) {
17210
+ counts[DM_CHANNEL_MISTARGET_KEY] = (counts[DM_CHANNEL_MISTARGET_KEY] ?? 0) + 1;
17211
+ }
17212
+ try {
17213
+ writeFileSync9(path, JSON.stringify(counts), { mode: 384 });
17214
+ } catch {
17215
+ }
17216
+ }
17148
17217
 
17149
17218
  // src/slack-reply-target-telemetry.ts
17150
17219
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
@@ -20297,7 +20366,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
20297
20366
  properties: {
20298
20367
  channel: {
20299
20368
  type: "string",
20300
- description: "Slack channel ID (from the channel attribute in the <channel> tag)"
20369
+ description: 'Slack channel ID. Copy it from the channel attribute of the SAME <channel> tag you are answering - never reuse a channel from a different message. A DM channel starts with "D": a DM answer MUST go to that "D\u2026" id, not a shared "C\u2026"/"G\u2026" channel. Prefer passing inbound_id, which resolves the exact destination for you.'
20301
20370
  },
20302
20371
  text: { type: "string", description: "The message to send" },
20303
20372
  thread_ts: {
@@ -20324,7 +20393,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
20324
20393
  // WS2 (cross-thread reply routing): the preferred way to target a reply.
20325
20394
  inbound_id: {
20326
20395
  type: "string",
20327
- 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.`
20396
+ description: `The inbound_id attribute of the <channel> tag you are answering (e.g. "ib_a1b2c3d4e5f6"). ALWAYS include it when replying to a message - it is STRONGLY PREFERRED over channel/thread_ts/message_ts because the server resolves the exact destination (channel AND thread) from it, so your reply can never land in another user's channel or thread. Copy it verbatim from the tag of the message you are replying to. Omit it only for a proactive (self-initiated) send.`
20328
20397
  },
20329
20398
  proactive: {
20330
20399
  type: "boolean",
@@ -20580,7 +20649,20 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20580
20649
  classification: binding.classification,
20581
20650
  unknownInboundId: binding.unknownInboundId
20582
20651
  });
20583
- if (bindingMode === "enforce" && binding.reject) {
20652
+ const channelGuard = guardReplyChannel({
20653
+ args: { inbound_id, channel, thread_ts, message_ts, proactive },
20654
+ registry: slackInboundRegistry
20655
+ });
20656
+ if (channelGuard.corrected) {
20657
+ recordChannelMistarget(SLACK_AGENT_DIR, "slack", { dm: channelGuard.dm });
20658
+ process.stderr.write(
20659
+ `slack-channel(${AGENT_CODE_NAME}): reply_channel_mistarget_corrected dm=${channelGuard.dm} requested=${redactSlackId(channelGuard.requestedChannel)} registry=${redactSlackId(channelGuard.channel)}
20660
+ `
20661
+ );
20662
+ }
20663
+ const effectiveChannel = channelGuard.corrected && channelGuard.channel ? channelGuard.channel : channel;
20664
+ const effectiveMessageTs = channelGuard.corrected ? channelGuard.messageTs : message_ts;
20665
+ if (bindingMode === "enforce" && binding.reject && !channelGuard.corrected) {
20584
20666
  process.stderr.write(
20585
20667
  `slack-channel(${AGENT_CODE_NAME}): reply_binding_rejected class=${binding.classification} channel=${redactSlackId(channel)}
20586
20668
  `
@@ -20604,28 +20686,28 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20604
20686
  };
20605
20687
  }
20606
20688
  const bindingFeedback = bindingMode === "warn" ? binding.agentFeedback : void 0;
20607
- const effectiveThreadTs = enforceBound ? binding.threadTs : resolveReplyThreadTs({
20689
+ const effectiveThreadTs = enforceBound ? binding.threadTs : channelGuard.corrected ? channelGuard.threadTs : resolveReplyThreadTs({
20608
20690
  channel,
20609
20691
  threadTs: thread_ts,
20610
20692
  messageTs: message_ts
20611
20693
  });
20612
- if (channel) {
20694
+ if (effectiveChannel) {
20613
20695
  recordReplyTargetClassification(
20614
20696
  SLACK_AGENT_DIR,
20615
20697
  "slack",
20616
20698
  classifyReplyTarget({
20617
- channel,
20699
+ channel: effectiveChannel,
20618
20700
  threadTs: effectiveThreadTs,
20619
- messageTs: message_ts,
20701
+ messageTs: effectiveMessageTs,
20620
20702
  registry: slackInboundRegistry
20621
20703
  })
20622
20704
  );
20623
20705
  }
20624
20706
  const throttleCfg = configFromEnv();
20625
- const throttleKey = effectiveThreadTs ?? channel;
20707
+ const throttleKey = effectiveThreadTs ?? effectiveChannel;
20626
20708
  const throttleNow = Date.now();
20627
20709
  const throttleDecision = decideReplyThrottle({
20628
- recentReplyTimestamps: getRecentReplies(channel, throttleKey, throttleNow, throttleCfg),
20710
+ recentReplyTimestamps: getRecentReplies(effectiveChannel, throttleKey, throttleNow, throttleCfg),
20629
20711
  now: throttleNow,
20630
20712
  config: throttleCfg
20631
20713
  });
@@ -20634,14 +20716,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20634
20716
  `slack-channel(${AGENT_CODE_NAME}): reply_throttled channel=${redactSlackId(channel)} thread=${redactSlackId(throttleKey)} count=${throttleDecision.recentCount} window_ms=${throttleCfg.windowMs} threshold=${throttleCfg.threshold}
20635
20717
  `
20636
20718
  );
20637
- if (channel && effectiveThreadTs) {
20719
+ if (effectiveChannel && effectiveThreadTs) {
20638
20720
  fetch("https://slack.com/api/reactions.add", {
20639
20721
  method: "POST",
20640
20722
  headers: {
20641
20723
  "Content-Type": "application/json",
20642
20724
  Authorization: `Bearer ${BOT_TOKEN}`
20643
20725
  },
20644
- body: JSON.stringify({ channel, timestamp: effectiveThreadTs, name: "lock" })
20726
+ body: JSON.stringify({ channel: effectiveChannel, timestamp: effectiveThreadTs, name: "lock" })
20645
20727
  }).catch(() => {
20646
20728
  });
20647
20729
  }
@@ -20655,21 +20737,24 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20655
20737
  isError: true
20656
20738
  };
20657
20739
  }
20740
+ const settleChannel = effectiveChannel;
20741
+ const settleMessageTs = effectiveMessageTs;
20742
+ const settleThreadTs = enforceBound ? binding.threadTs : channelGuard.corrected ? channelGuard.threadTs : thread_ts;
20658
20743
  const settlePendingMarker = () => {
20659
- if (!channel) return;
20660
- if (message_ts) {
20744
+ if (!settleChannel) return;
20745
+ if (settleMessageTs) {
20661
20746
  if (interim) {
20662
- markSeenSlackPendingMarkerByMessageTs2(channel, message_ts);
20663
- if (thread_ts) markSeenAllSlackPendingMarkersForThread2(channel, thread_ts);
20747
+ markSeenSlackPendingMarkerByMessageTs2(settleChannel, settleMessageTs);
20748
+ if (settleThreadTs) markSeenAllSlackPendingMarkersForThread2(settleChannel, settleThreadTs);
20664
20749
  } else {
20665
- clearSlackPendingMarkerByMessageTs2(channel, message_ts);
20666
- if (thread_ts) clearPendingMessage(channel, thread_ts);
20750
+ clearSlackPendingMarkerByMessageTs2(settleChannel, settleMessageTs);
20751
+ if (settleThreadTs) clearPendingMessage(settleChannel, settleThreadTs);
20667
20752
  }
20668
- } else if (thread_ts) {
20669
- if (interim) markSeenAllSlackPendingMarkersForThread2(channel, thread_ts);
20670
- else clearPendingMessage(channel, thread_ts);
20753
+ } else if (settleThreadTs) {
20754
+ if (interim) markSeenAllSlackPendingMarkersForThread2(settleChannel, settleThreadTs);
20755
+ else clearPendingMessage(settleChannel, settleThreadTs);
20671
20756
  } else if (!interim) {
20672
- clearOldestSlackPendingMarkerInChannel2(channel);
20757
+ clearOldestSlackPendingMarkerInChannel2(settleChannel);
20673
20758
  }
20674
20759
  };
20675
20760
  const sanitized = stripRepostPreambleWithMeta(text);
@@ -20686,7 +20771,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20686
20771
  Authorization: `Bearer ${BOT_TOKEN}`
20687
20772
  },
20688
20773
  body: JSON.stringify({
20689
- channel,
20774
+ channel: effectiveChannel,
20690
20775
  text: apiErr.text,
20691
20776
  ...effectiveThreadTs ? { thread_ts: effectiveThreadTs } : {},
20692
20777
  ...buildAugmentedSlackMetadata() ? { metadata: buildAugmentedSlackMetadata() } : {}
@@ -20702,11 +20787,13 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20702
20787
  };
20703
20788
  }
20704
20789
  settlePendingMarker();
20705
- recordReply(channel, throttleKey, throttleNow, throttleCfg);
20790
+ recordReply(effectiveChannel, throttleKey, throttleNow, throttleCfg);
20706
20791
  recordActivity("reply");
20707
20792
  if (THREAD_AUTO_FOLLOW !== "off") {
20708
20793
  const trackTs = effectiveThreadTs ?? data.ts ?? void 0;
20709
- rememberThread(channel, trackTs, thread_ts ? "mentioned" : "started", { botPosted: true });
20794
+ rememberThread(effectiveChannel, trackTs, thread_ts ? "mentioned" : "started", {
20795
+ botPosted: true
20796
+ });
20710
20797
  }
20711
20798
  if (interactiveHostAvailable() && data.ts) {
20712
20799
  const cfg = {
@@ -20720,7 +20807,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
20720
20807
  try {
20721
20808
  const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
20722
20809
  await runtime.recordSlackDelivery(cfg, {
20723
- channel,
20810
+ channel: effectiveChannel,
20724
20811
  messageTs: tsToRecord,
20725
20812
  threadTs: threadTsToRecord
20726
20813
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.266",
3
+ "version": "0.28.268",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {