@integrity-labs/agt-cli 0.28.582 → 0.28.583

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.
package/dist/bin/agt.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-XYWEC7PX.js";
43
+ } from "../chunk-I4ZI4W7S.js";
44
44
  import {
45
45
  AnchorSessionClient,
46
46
  CHANNEL_REGISTRY,
@@ -4908,7 +4908,7 @@ import { execFileSync, execSync } from "child_process";
4908
4908
  import { existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4909
4909
  import chalk18 from "chalk";
4910
4910
  import ora16 from "ora";
4911
- var cliVersion = true ? "0.28.582" : "dev";
4911
+ var cliVersion = true ? "0.28.583" : "dev";
4912
4912
  async function fetchLatestVersion() {
4913
4913
  const host2 = getHost();
4914
4914
  if (!host2) return null;
@@ -6086,7 +6086,7 @@ function handleError(err) {
6086
6086
  }
6087
6087
 
6088
6088
  // src/bin/agt.ts
6089
- var cliVersion2 = true ? "0.28.582" : "dev";
6089
+ var cliVersion2 = true ? "0.28.583" : "dev";
6090
6090
  var program = new Command();
6091
6091
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6092
6092
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -5971,7 +5971,7 @@ function exchangeFailureKind(err) {
5971
5971
  }
5972
5972
 
5973
5973
  // src/lib/api-client.ts
5974
- var agtCliVersion = true ? "0.28.582" : "dev";
5974
+ var agtCliVersion = true ? "0.28.583" : "dev";
5975
5975
  var lastConfigHash = null;
5976
5976
  function setConfigHash(hash) {
5977
5977
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -9347,4 +9347,4 @@ export {
9347
9347
  managerInstallSystemUnitCommand,
9348
9348
  managerUninstallSystemUnitCommand
9349
9349
  };
9350
- //# sourceMappingURL=chunk-XYWEC7PX.js.map
9350
+ //# sourceMappingURL=chunk-I4ZI4W7S.js.map
@@ -53,7 +53,7 @@ import {
53
53
  safeWriteJsonAtomic,
54
54
  setConfigHash,
55
55
  tripClass
56
- } from "../chunk-XYWEC7PX.js";
56
+ } from "../chunk-I4ZI4W7S.js";
57
57
  import {
58
58
  getProjectDir as getProjectDir2,
59
59
  getReadyTasks,
@@ -11624,7 +11624,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
11624
11624
  var lastVersionCheckAt = 0;
11625
11625
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
11626
11626
  var lastResponsivenessProbeAt = 0;
11627
- var agtCliVersion = true ? "0.28.582" : "dev";
11627
+ var agtCliVersion = true ? "0.28.583" : "dev";
11628
11628
  function resolveBrewPath(execFileSync2) {
11629
11629
  try {
11630
11630
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -36636,6 +36636,145 @@ function shouldPostUndeliverableNotice(lastNoticeAtMs, nowMs, throttleMs = UNDEL
36636
36636
  function undeliverableNoticeText(replayEnabled = channelReplayEnabled()) {
36637
36637
  return replayEnabled ? "\u23F3 I can't get to this right now \u2014 no need to resend; I'll pick it up automatically as soon as I'm free." : "\u23F3 I can't get to this right now. Please resend in a few minutes \u2014 I may not see this one.";
36638
36638
  }
36639
+ var UNDELIVERABLE_NOTICE_DEBOUNCE_MS = 3e4;
36640
+ function undeliverableNoticeDebounceMs() {
36641
+ const raw = (process.env.AGT_CHANNEL_UNDELIVERABLE_NOTICE_DEBOUNCE_MS ?? "").trim();
36642
+ if (!/^\d+$/.test(raw)) return UNDELIVERABLE_NOTICE_DEBOUNCE_MS;
36643
+ const parsed = Number(raw);
36644
+ return Number.isSafeInteger(parsed) ? parsed : UNDELIVERABLE_NOTICE_DEBOUNCE_MS;
36645
+ }
36646
+ function undeliverableNoticeDelayMs(cause, debounceMs = undeliverableNoticeDebounceMs()) {
36647
+ if (isConfirmedUndeliverableCause(cause)) return 0;
36648
+ return debounceMs;
36649
+ }
36650
+ function isConfirmedUndeliverableCause(cause) {
36651
+ return cause === "integration_down" || cause === "session_dead";
36652
+ }
36653
+ function decideUndeliverableNoticeDispatch(i) {
36654
+ if (i.confirmed && i.hasPendingNotice) return "send-now";
36655
+ if (!shouldPostUndeliverableNotice(i.lastNoticeAtMs, i.nowMs, i.throttleMs)) return "suppress";
36656
+ if (i.confirmed) return "send-now";
36657
+ if (i.hasPendingNotice) return "suppress";
36658
+ return i.delayMs > 0 ? "schedule" : "send-now";
36659
+ }
36660
+ var UndeliverableNoticeScheduler = class {
36661
+ constructor(throttleMs = UNDELIVERABLE_NOTICE_THROTTLE_MS) {
36662
+ this.throttleMs = throttleMs;
36663
+ }
36664
+ /** decision-time stamps, keyed by conversation. Shared with other one-per-window system notices. */
36665
+ stamps = /* @__PURE__ */ new Map();
36666
+ /** scheduled-but-unsent notices, keyed by conversation. */
36667
+ timers = /* @__PURE__ */ new Map();
36668
+ /** is a notice scheduled but not yet sent for this conversation? */
36669
+ hasPending(key2) {
36670
+ return this.timers.has(key2);
36671
+ }
36672
+ /** when this conversation was last stamped, or undefined if never. */
36673
+ stampedAt(key2) {
36674
+ return this.stamps.get(key2);
36675
+ }
36676
+ /**
36677
+ * ENG-6025: claim this conversation's one-notice-per-window slot for a
36678
+ * DIFFERENT system notice (the watchdog give-up line). Returns false when the
36679
+ * window is still closed. Sharing the stamp is the point: the user gets one
36680
+ * system message per conversation per window, whichever fires first.
36681
+ */
36682
+ claimThrottleSlot(key2, nowMs = Date.now()) {
36683
+ if (!shouldPostUndeliverableNotice(this.stamps.get(key2), nowMs, this.throttleMs)) return false;
36684
+ this.stamps.set(key2, nowMs);
36685
+ return true;
36686
+ }
36687
+ /**
36688
+ * Run the dispatch decision and carry it out. `send` is invoked synchronously
36689
+ * for 'send-now' and after `delayMs` for 'schedule'; it is never invoked for
36690
+ * 'suppress'. Returns the decision so callers can log it.
36691
+ */
36692
+ dispatch(key2, i) {
36693
+ const nowMs = i.nowMs ?? Date.now();
36694
+ const decision = decideUndeliverableNoticeDispatch({
36695
+ confirmed: i.confirmed,
36696
+ delayMs: i.delayMs,
36697
+ hasPendingNotice: this.timers.has(key2),
36698
+ lastNoticeAtMs: this.stamps.get(key2),
36699
+ nowMs,
36700
+ throttleMs: this.throttleMs
36701
+ });
36702
+ if (decision === "suppress") return "suppress";
36703
+ if (decision === "send-now") {
36704
+ this.clearTimer(key2);
36705
+ this.stamps.set(key2, nowMs);
36706
+ this.safeSend(i.send);
36707
+ return "send-now";
36708
+ }
36709
+ this.stamps.set(key2, nowMs);
36710
+ const timer = setTimeout(() => {
36711
+ this.timers.delete(key2);
36712
+ this.safeSend(i.send);
36713
+ }, i.delayMs);
36714
+ timer.unref?.();
36715
+ this.timers.set(key2, timer);
36716
+ return "schedule";
36717
+ }
36718
+ /**
36719
+ * The agent engaged: drop a scheduled-but-unsent notice, and its stamp with
36720
+ * it. Returns whether anything was actually cancelled.
36721
+ *
36722
+ * Cancelling something that was never sent cannot be wrong, so this needs no
36723
+ * "was it undeliverable?" gate - which is what Slack's version had, and why
36724
+ * it missed the case where the marker had never been flagged.
36725
+ */
36726
+ cancel(key2) {
36727
+ if (!this.timers.has(key2)) return false;
36728
+ this.clearTimer(key2);
36729
+ this.stamps.delete(key2);
36730
+ return true;
36731
+ }
36732
+ /**
36733
+ * Forget this conversation entirely - pending timer AND stamp - after a
36734
+ * POSTED notice has been retracted (ENG-7705). Distinct from cancel(): that
36735
+ * one is a no-op when nothing is pending, this one always clears the stamp so
36736
+ * a later busy period earns a fresh notice instead of staying muted.
36737
+ */
36738
+ forget(key2) {
36739
+ this.clearTimer(key2);
36740
+ this.stamps.delete(key2);
36741
+ }
36742
+ /** Test seam / shutdown: drop all timers and stamps. */
36743
+ reset() {
36744
+ for (const timer of this.timers.values()) clearTimeout(timer);
36745
+ this.timers.clear();
36746
+ this.stamps.clear();
36747
+ }
36748
+ clearTimer(key2) {
36749
+ const timer = this.timers.get(key2);
36750
+ if (timer) clearTimeout(timer);
36751
+ this.timers.delete(key2);
36752
+ }
36753
+ /**
36754
+ * ENG-6247 round 3 (CodeRabbit #4446): the notice is fire-and-forget by design
36755
+ * everywhere else - "a failed notice never blocks inbound processing" - and
36756
+ * that has to hold here too.
36757
+ *
36758
+ * The delayed case is the dangerous one: a setTimeout callback runs outside
36759
+ * every request context, so a synchronous throw from `send` is an unhandled
36760
+ * exception and takes the channel server down. Both call sites close over
36761
+ * network-calling code. A notice the user was never promised must not be able
36762
+ * to cost them their agent.
36763
+ *
36764
+ * The immediate case is wrapped for the same reason one step earlier: it runs
36765
+ * inside the inbound handler, so an escaping throw would abort processing of
36766
+ * the very message the notice was about.
36767
+ *
36768
+ * Swallowed rather than logged: this module is pure and has no channel logger,
36769
+ * and both send closures already report their own failures.
36770
+ */
36771
+ safeSend(send) {
36772
+ try {
36773
+ send();
36774
+ } catch {
36775
+ }
36776
+ }
36777
+ };
36639
36778
  function undeliverableNoticeConversationKey(channel, threadTs, isThreadReply) {
36640
36779
  return isThreadReply && threadTs ? `${channel}:${threadTs}` : channel;
36641
36780
  }
@@ -41481,16 +41620,19 @@ function healSlackUndeliverable(channel, messageTs) {
41481
41620
  });
41482
41621
  });
41483
41622
  }
41484
- var lastSlackUndeliverableNoticeAt = /* @__PURE__ */ new Map();
41623
+ var slackUndeliverableNotices = new UndeliverableNoticeScheduler();
41485
41624
  var lastSlackUndeliverableNoticeInfo = /* @__PURE__ */ new Map();
41486
- function postUndeliverableNotice(channel, threadTs, isThreadReply) {
41625
+ function postUndeliverableNotice(channel, threadTs, isThreadReply, cause = "unknown") {
41487
41626
  if (!BOT_TOKEN || !channel) return;
41488
- const now = Date.now();
41489
41627
  const conversationKey = undeliverableNoticeConversationKey(channel, threadTs, isThreadReply);
41490
- if (!shouldPostUndeliverableNotice(lastSlackUndeliverableNoticeAt.get(conversationKey), now)) {
41491
- return;
41492
- }
41493
- lastSlackUndeliverableNoticeAt.set(conversationKey, now);
41628
+ slackUndeliverableNotices.dispatch(conversationKey, {
41629
+ confirmed: isConfirmedUndeliverableCause(cause),
41630
+ delayMs: undeliverableNoticeDelayMs(cause),
41631
+ send: () => sendUndeliverableNotice(channel, threadTs, conversationKey)
41632
+ });
41633
+ }
41634
+ function sendUndeliverableNotice(channel, threadTs, conversationKey) {
41635
+ if (!BOT_TOKEN || !channel) return;
41494
41636
  fetch("https://slack.com/api/chat.postMessage", {
41495
41637
  method: "POST",
41496
41638
  headers: {
@@ -41510,13 +41652,19 @@ function postUndeliverableNotice(channel, threadTs, isThreadReply) {
41510
41652
  });
41511
41653
  }
41512
41654
  function __resetSlackUndeliverableNoticeThrottle() {
41513
- lastSlackUndeliverableNoticeAt.clear();
41655
+ slackUndeliverableNotices.reset();
41514
41656
  lastSlackUndeliverableNoticeInfo.clear();
41515
41657
  }
41658
+ function __slackUndeliverableNoticeScheduled(conversationKey) {
41659
+ return slackUndeliverableNotices.hasPending(conversationKey);
41660
+ }
41661
+ function cancelScheduledSlackUndeliverableNotice(conversationKey) {
41662
+ slackUndeliverableNotices.cancel(conversationKey);
41663
+ }
41516
41664
  function deleteSlackUndeliverableNotice(conversationKey) {
41665
+ slackUndeliverableNotices.forget(conversationKey);
41517
41666
  const info = lastSlackUndeliverableNoticeInfo.get(conversationKey);
41518
41667
  lastSlackUndeliverableNoticeInfo.delete(conversationKey);
41519
- lastSlackUndeliverableNoticeAt.delete(conversationKey);
41520
41668
  if (!BOT_TOKEN || !info) return;
41521
41669
  fetch("https://slack.com/api/chat.delete", {
41522
41670
  method: "POST",
@@ -41604,6 +41752,11 @@ function clearSlackMarkerFileWithHeal(fullPath) {
41604
41752
  marker = JSON.parse(readFileSync18(fullPath, "utf-8"));
41605
41753
  } catch {
41606
41754
  }
41755
+ if (marker?.channel) {
41756
+ cancelScheduledSlackUndeliverableNotice(
41757
+ undeliverableNoticeConversationKeyFromMarker(marker)
41758
+ );
41759
+ }
41607
41760
  if (marker && decideRecoveryHeal({
41608
41761
  wasUndeliverable: marker.undeliverable === true,
41609
41762
  hasTarget: Boolean(marker.channel && marker.message_ts)
@@ -41632,6 +41785,11 @@ function healThenEngageSlackMarker(fullPath, opts) {
41632
41785
  } catch {
41633
41786
  return false;
41634
41787
  }
41788
+ if (marker.channel) {
41789
+ cancelScheduledSlackUndeliverableNotice(
41790
+ undeliverableNoticeConversationKeyFromMarker(marker)
41791
+ );
41792
+ }
41635
41793
  if (decideRecoveryHeal({
41636
41794
  wasUndeliverable: marker.undeliverable === true,
41637
41795
  hasTarget: Boolean(marker.channel && marker.message_ts)
@@ -42415,10 +42573,7 @@ function postSlackWatchdogGiveUpNotice(channel, threadTs, isThreadReply, reason)
42415
42573
  if (!BOT_TOKEN || !channel) return;
42416
42574
  const now = Date.now();
42417
42575
  const conversationKey = isThreadReply ? `${channel}:${threadTs}` : channel;
42418
- if (!shouldPostUndeliverableNotice(lastSlackUndeliverableNoticeAt.get(conversationKey), now)) {
42419
- return;
42420
- }
42421
- lastSlackUndeliverableNoticeAt.set(conversationKey, now);
42576
+ if (!slackUndeliverableNotices.claimThrottleSlot(conversationKey, now)) return;
42422
42577
  process.stderr.write(
42423
42578
  `slack-channel(${AGENT_CODE_NAME}): give-up notice posted channel=${redactSlackId(channel)}
42424
42579
  `
@@ -45988,8 +46143,8 @@ async function connectSocketMode() {
45988
46143
  };
45989
46144
  const ackDecision = decideAckReaction(ackInputs);
45990
46145
  const arrivedWhileBusy = hasInFlightInbound(SLACK_PENDING_INBOUND_DIR);
46146
+ const cause = classifyUndeliverableCause(ackInputs) ?? "unknown";
45991
46147
  if (ackDecision === "undeliverable") {
45992
- const cause = classifyUndeliverableCause(ackInputs) ?? "unknown";
45993
46148
  recordChannelDeflection(SLACK_AGENT_DIR, "slack", cause);
45994
46149
  process.stderr.write(
45995
46150
  `slack-channel(${AGENT_CODE_NAME}): [channel-deflection] cause=${cause} channel=slack ts=${redactSlackId(ts)}
@@ -46008,7 +46163,7 @@ async function connectSocketMode() {
46008
46163
  }).catch(() => {
46009
46164
  });
46010
46165
  if (ackDecision === "undeliverable" && channel && undeliverableNoticeEligible(shouldEngage)) {
46011
- postUndeliverableNotice(channel, threadTs, isThreadReply);
46166
+ postUndeliverableNotice(channel, threadTs, isThreadReply, cause);
46012
46167
  }
46013
46168
  }
46014
46169
  if (channel && ts && armMarker) {
@@ -46300,7 +46455,8 @@ connectSocketModeSafely();
46300
46455
  export {
46301
46456
  __resetSlackBusyAckNoticeThrottle,
46302
46457
  __resetSlackGiveUpNoticeStateForTests,
46303
- __resetSlackUndeliverableNoticeThrottle
46458
+ __resetSlackUndeliverableNoticeThrottle,
46459
+ __slackUndeliverableNoticeScheduled
46304
46460
  };
46305
46461
  /*! Bundled license information:
46306
46462
 
@@ -39247,6 +39247,145 @@ function shouldPostUndeliverableNotice(lastNoticeAtMs, nowMs, throttleMs = UNDEL
39247
39247
  function undeliverableNoticeText(replayEnabled = channelReplayEnabled()) {
39248
39248
  return replayEnabled ? "\u23F3 I can't get to this right now \u2014 no need to resend; I'll pick it up automatically as soon as I'm free." : "\u23F3 I can't get to this right now. Please resend in a few minutes \u2014 I may not see this one.";
39249
39249
  }
39250
+ var UNDELIVERABLE_NOTICE_DEBOUNCE_MS = 3e4;
39251
+ function undeliverableNoticeDebounceMs() {
39252
+ const raw = (process.env.AGT_CHANNEL_UNDELIVERABLE_NOTICE_DEBOUNCE_MS ?? "").trim();
39253
+ if (!/^\d+$/.test(raw)) return UNDELIVERABLE_NOTICE_DEBOUNCE_MS;
39254
+ const parsed = Number(raw);
39255
+ return Number.isSafeInteger(parsed) ? parsed : UNDELIVERABLE_NOTICE_DEBOUNCE_MS;
39256
+ }
39257
+ function undeliverableNoticeDelayMs(cause, debounceMs = undeliverableNoticeDebounceMs()) {
39258
+ if (isConfirmedUndeliverableCause(cause)) return 0;
39259
+ return debounceMs;
39260
+ }
39261
+ function isConfirmedUndeliverableCause(cause) {
39262
+ return cause === "integration_down" || cause === "session_dead";
39263
+ }
39264
+ function decideUndeliverableNoticeDispatch(i) {
39265
+ if (i.confirmed && i.hasPendingNotice) return "send-now";
39266
+ if (!shouldPostUndeliverableNotice(i.lastNoticeAtMs, i.nowMs, i.throttleMs)) return "suppress";
39267
+ if (i.confirmed) return "send-now";
39268
+ if (i.hasPendingNotice) return "suppress";
39269
+ return i.delayMs > 0 ? "schedule" : "send-now";
39270
+ }
39271
+ var UndeliverableNoticeScheduler = class {
39272
+ constructor(throttleMs = UNDELIVERABLE_NOTICE_THROTTLE_MS) {
39273
+ this.throttleMs = throttleMs;
39274
+ }
39275
+ /** decision-time stamps, keyed by conversation. Shared with other one-per-window system notices. */
39276
+ stamps = /* @__PURE__ */ new Map();
39277
+ /** scheduled-but-unsent notices, keyed by conversation. */
39278
+ timers = /* @__PURE__ */ new Map();
39279
+ /** is a notice scheduled but not yet sent for this conversation? */
39280
+ hasPending(key2) {
39281
+ return this.timers.has(key2);
39282
+ }
39283
+ /** when this conversation was last stamped, or undefined if never. */
39284
+ stampedAt(key2) {
39285
+ return this.stamps.get(key2);
39286
+ }
39287
+ /**
39288
+ * ENG-6025: claim this conversation's one-notice-per-window slot for a
39289
+ * DIFFERENT system notice (the watchdog give-up line). Returns false when the
39290
+ * window is still closed. Sharing the stamp is the point: the user gets one
39291
+ * system message per conversation per window, whichever fires first.
39292
+ */
39293
+ claimThrottleSlot(key2, nowMs = Date.now()) {
39294
+ if (!shouldPostUndeliverableNotice(this.stamps.get(key2), nowMs, this.throttleMs)) return false;
39295
+ this.stamps.set(key2, nowMs);
39296
+ return true;
39297
+ }
39298
+ /**
39299
+ * Run the dispatch decision and carry it out. `send` is invoked synchronously
39300
+ * for 'send-now' and after `delayMs` for 'schedule'; it is never invoked for
39301
+ * 'suppress'. Returns the decision so callers can log it.
39302
+ */
39303
+ dispatch(key2, i) {
39304
+ const nowMs = i.nowMs ?? Date.now();
39305
+ const decision = decideUndeliverableNoticeDispatch({
39306
+ confirmed: i.confirmed,
39307
+ delayMs: i.delayMs,
39308
+ hasPendingNotice: this.timers.has(key2),
39309
+ lastNoticeAtMs: this.stamps.get(key2),
39310
+ nowMs,
39311
+ throttleMs: this.throttleMs
39312
+ });
39313
+ if (decision === "suppress") return "suppress";
39314
+ if (decision === "send-now") {
39315
+ this.clearTimer(key2);
39316
+ this.stamps.set(key2, nowMs);
39317
+ this.safeSend(i.send);
39318
+ return "send-now";
39319
+ }
39320
+ this.stamps.set(key2, nowMs);
39321
+ const timer = setTimeout(() => {
39322
+ this.timers.delete(key2);
39323
+ this.safeSend(i.send);
39324
+ }, i.delayMs);
39325
+ timer.unref?.();
39326
+ this.timers.set(key2, timer);
39327
+ return "schedule";
39328
+ }
39329
+ /**
39330
+ * The agent engaged: drop a scheduled-but-unsent notice, and its stamp with
39331
+ * it. Returns whether anything was actually cancelled.
39332
+ *
39333
+ * Cancelling something that was never sent cannot be wrong, so this needs no
39334
+ * "was it undeliverable?" gate - which is what Slack's version had, and why
39335
+ * it missed the case where the marker had never been flagged.
39336
+ */
39337
+ cancel(key2) {
39338
+ if (!this.timers.has(key2)) return false;
39339
+ this.clearTimer(key2);
39340
+ this.stamps.delete(key2);
39341
+ return true;
39342
+ }
39343
+ /**
39344
+ * Forget this conversation entirely - pending timer AND stamp - after a
39345
+ * POSTED notice has been retracted (ENG-7705). Distinct from cancel(): that
39346
+ * one is a no-op when nothing is pending, this one always clears the stamp so
39347
+ * a later busy period earns a fresh notice instead of staying muted.
39348
+ */
39349
+ forget(key2) {
39350
+ this.clearTimer(key2);
39351
+ this.stamps.delete(key2);
39352
+ }
39353
+ /** Test seam / shutdown: drop all timers and stamps. */
39354
+ reset() {
39355
+ for (const timer of this.timers.values()) clearTimeout(timer);
39356
+ this.timers.clear();
39357
+ this.stamps.clear();
39358
+ }
39359
+ clearTimer(key2) {
39360
+ const timer = this.timers.get(key2);
39361
+ if (timer) clearTimeout(timer);
39362
+ this.timers.delete(key2);
39363
+ }
39364
+ /**
39365
+ * ENG-6247 round 3 (CodeRabbit #4446): the notice is fire-and-forget by design
39366
+ * everywhere else - "a failed notice never blocks inbound processing" - and
39367
+ * that has to hold here too.
39368
+ *
39369
+ * The delayed case is the dangerous one: a setTimeout callback runs outside
39370
+ * every request context, so a synchronous throw from `send` is an unhandled
39371
+ * exception and takes the channel server down. Both call sites close over
39372
+ * network-calling code. A notice the user was never promised must not be able
39373
+ * to cost them their agent.
39374
+ *
39375
+ * The immediate case is wrapped for the same reason one step earlier: it runs
39376
+ * inside the inbound handler, so an escaping throw would abort processing of
39377
+ * the very message the notice was about.
39378
+ *
39379
+ * Swallowed rather than logged: this module is pure and has no channel logger,
39380
+ * and both send closures already report their own failures.
39381
+ */
39382
+ safeSend(send) {
39383
+ try {
39384
+ send();
39385
+ } catch {
39386
+ }
39387
+ }
39388
+ };
39250
39389
  var BUSY_ACK_THRESHOLD_MS = 12e4;
39251
39390
  var BUSY_ACK_NOTICE_THROTTLE_MS = 20 * 60 * 1e3;
39252
39391
  function channelBusyAckEnabled() {
@@ -39959,12 +40098,18 @@ async function setMessageReaction(chatId, messageId, emoji2) {
39959
40098
  );
39960
40099
  }
39961
40100
  }
39962
- var lastUndeliverableNoticeAt = /* @__PURE__ */ new Map();
39963
- async function notifyUndeliverable(chatId, messageId) {
39964
- const key2 = String(chatId);
39965
- const now = Date.now();
39966
- if (!shouldPostUndeliverableNotice(lastUndeliverableNoticeAt.get(key2), now)) return;
39967
- lastUndeliverableNoticeAt.set(key2, now);
40101
+ var telegramUndeliverableNotices = new UndeliverableNoticeScheduler();
40102
+ function cancelScheduledUndeliverableNotice(chatId) {
40103
+ telegramUndeliverableNotices.cancel(String(chatId));
40104
+ }
40105
+ function notifyUndeliverable(chatId, messageId, cause = "unknown") {
40106
+ telegramUndeliverableNotices.dispatch(String(chatId), {
40107
+ confirmed: isConfirmedUndeliverableCause(cause),
40108
+ delayMs: undeliverableNoticeDelayMs(cause),
40109
+ send: () => void sendUndeliverableNotice(chatId, messageId)
40110
+ });
40111
+ }
40112
+ async function sendUndeliverableNotice(chatId, messageId) {
39968
40113
  try {
39969
40114
  const resp = await telegramApiCall(
39970
40115
  "sendMessage",
@@ -39991,7 +40136,10 @@ async function notifyUndeliverable(chatId, messageId) {
39991
40136
  }
39992
40137
  }
39993
40138
  function __resetUndeliverableNoticeThrottle() {
39994
- lastUndeliverableNoticeAt.clear();
40139
+ telegramUndeliverableNotices.reset();
40140
+ }
40141
+ function __telegramUndeliverableNoticeScheduled(chatId) {
40142
+ return telegramUndeliverableNotices.hasPending(String(chatId));
39995
40143
  }
39996
40144
  var lastBackOnlineNoticeAt = /* @__PURE__ */ new Map();
39997
40145
  function notifyBackOnline(chatId) {
@@ -41005,6 +41153,9 @@ function clearTelegramMarkerFileWithHeal(fullPath) {
41005
41153
  marker = JSON.parse(readFileSync14(fullPath, "utf-8"));
41006
41154
  } catch {
41007
41155
  }
41156
+ if (marker?.chat_id) {
41157
+ cancelScheduledUndeliverableNotice(marker.chat_id);
41158
+ }
41008
41159
  if (marker && decideRecoveryHeal({
41009
41160
  wasUndeliverable: marker.undeliverable === true,
41010
41161
  hasTarget: Boolean(marker.chat_id)
@@ -41035,6 +41186,7 @@ function markTelegramMarkerEngagedInPlace(fullPath, opts) {
41035
41186
  (/* @__PURE__ */ new Date()).toISOString()
41036
41187
  );
41037
41188
  if (!decision.stamp) return opts.promise ? decision.promised : false;
41189
+ if (marker.chat_id) cancelScheduledUndeliverableNotice(marker.chat_id);
41038
41190
  Object.assign(marker, decision.stamp);
41039
41191
  if (marker.undeliverable) delete marker.undeliverable;
41040
41192
  rewriteTelegramMarkerInPlace(fullPath, marker);
@@ -41053,6 +41205,7 @@ function markTelegramMarkerSeenWithHeal(fullPath) {
41053
41205
  } catch {
41054
41206
  return;
41055
41207
  }
41208
+ if (marker.chat_id) cancelScheduledUndeliverableNotice(marker.chat_id);
41056
41209
  if (decideRecoveryHeal({
41057
41210
  wasUndeliverable: marker.undeliverable === true,
41058
41211
  hasTarget: Boolean(marker.chat_id)
@@ -41733,8 +41886,7 @@ function armTelegramTurnFailureWatch(args) {
41733
41886
  }
41734
41887
  async function notifyWatchdogGiveUp(chatId, reason) {
41735
41888
  const now = Date.now();
41736
- if (!shouldPostUndeliverableNotice(lastUndeliverableNoticeAt.get(chatId), now)) return;
41737
- lastUndeliverableNoticeAt.set(chatId, now);
41889
+ if (!telegramUndeliverableNotices.claimThrottleSlot(chatId, now)) return;
41738
41890
  try {
41739
41891
  const resp = await telegramApiCall(
41740
41892
  "sendMessage",
@@ -43277,7 +43429,7 @@ async function pollLoop() {
43277
43429
  if (ackDecision === "ack") {
43278
43430
  void setMessageReaction(chatId, messageId, ACK_EMOJI);
43279
43431
  } else if (ackDecision === "undeliverable") {
43280
- void notifyUndeliverable(chatId, messageId);
43432
+ notifyUndeliverable(chatId, messageId, classifyUndeliverableCause(ackInputs) ?? "unknown");
43281
43433
  }
43282
43434
  const fileMeta = [];
43283
43435
  for (const attachment of classifiedAttachments) {
@@ -43508,6 +43660,7 @@ export {
43508
43660
  __resetBusyAckNoticeThrottle,
43509
43661
  __resetGiveUpNoticeStateForTests,
43510
43662
  __resetUndeliverableNoticeThrottle,
43663
+ __telegramUndeliverableNoticeScheduled,
43511
43664
  _resetQueuedReplyCountsForTests
43512
43665
  };
43513
43666
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.582",
3
+ "version": "0.28.583",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {