switchroom 0.20.8 → 0.20.9

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.
Files changed (36) hide show
  1. package/dist/agent-scheduler/index.js +16 -13
  2. package/dist/auth-broker/index.js +51 -29
  3. package/dist/cli/autoaccept-poll.js +5 -3
  4. package/dist/cli/drive-write-pretool.mjs +5 -3
  5. package/dist/cli/ms-365-write-pretool.mjs +5 -3
  6. package/dist/cli/notion-write-pretool.mjs +6 -6
  7. package/dist/cli/switchroom.js +12 -10
  8. package/dist/host-control/main.js +7 -7
  9. package/dist/vault/approvals/kernel-server.js +6 -6
  10. package/dist/vault/broker/server.js +6 -6
  11. package/package.json +1 -1
  12. package/profiles/default/CLAUDE.md.hbs +12 -13
  13. package/telegram-plugin/ask-user.ts +6 -7
  14. package/telegram-plugin/dist/gateway/gateway.js +183 -66
  15. package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
  16. package/telegram-plugin/gateway/auth-command.ts +4 -2
  17. package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
  18. package/telegram-plugin/gateway/gateway.ts +8 -4
  19. package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
  20. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
  21. package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
  22. package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
  23. package/telegram-plugin/render/emphasis-guard.ts +92 -12
  24. package/telegram-plugin/render/line-start-guard.ts +27 -2
  25. package/telegram-plugin/sticker-aliases.ts +12 -14
  26. package/telegram-plugin/tests/ask-user.test.ts +15 -0
  27. package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
  28. package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
  29. package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
  30. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
  31. package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
  32. package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
  33. package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
  34. package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
  35. package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
  36. package/telegram-plugin/throttle-tier.ts +59 -0
@@ -11329,7 +11329,8 @@ var init_protocol = __esm(() => {
11329
11329
  v: exports_external.literal(PROTOCOL_VERSION),
11330
11330
  op: exports_external.literal("mark-throttled"),
11331
11331
  id: exports_external.string().min(1),
11332
- until: exports_external.number().int().positive()
11332
+ until: exports_external.number().int().positive(),
11333
+ probeOnly: exports_external.boolean().optional()
11333
11334
  });
11334
11335
  RefreshAccountRequestSchema = exports_external.object({
11335
11336
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -11725,12 +11726,13 @@ class AuthBrokerClient {
11725
11726
  const data = await this.send(req);
11726
11727
  return data;
11727
11728
  }
11728
- async markThrottled(until) {
11729
+ async markThrottled(until, probeOnly = false) {
11729
11730
  const data = await this.send({
11730
11731
  v: PROTOCOL_VERSION,
11731
11732
  id: randomUUID2(),
11732
11733
  op: "mark-throttled",
11733
- until
11734
+ until,
11735
+ ...probeOnly ? { probeOnly: true } : {}
11734
11736
  });
11735
11737
  return data;
11736
11738
  }
@@ -13480,12 +13482,27 @@ var init_dollar_math_guard = __esm(() => {
13480
13482
  });
13481
13483
 
13482
13484
  // render/emphasis-guard.ts
13485
+ function isLineLeadingBullet(text, starIndex) {
13486
+ const next = text[starIndex + 1];
13487
+ if (next !== " " && next !== "\t")
13488
+ return false;
13489
+ let i = starIndex - 1;
13490
+ let indent = 0;
13491
+ while (i >= 0 && (text[i] === " " || text[i] === "\t")) {
13492
+ if (++indent > 3)
13493
+ return false;
13494
+ i--;
13495
+ }
13496
+ return i < 0 || text[i] === `
13497
+ `;
13498
+ }
13483
13499
  function guardAccidentalEmphasis(text) {
13484
13500
  if (!text.includes("_") && !text.includes("*"))
13485
13501
  return text;
13486
13502
  const segments = splitProtectedSegments(text);
13487
13503
  let hasIntraUnderscore = false;
13488
13504
  let hasIntraAsterisk = false;
13505
+ let hasBoundaryAsterisk = false;
13489
13506
  let underscoreCount = 0;
13490
13507
  let asteriskCount = 0;
13491
13508
  for (const seg of segments) {
@@ -13495,14 +13512,24 @@ function guardAccidentalEmphasis(text) {
13495
13512
  hasIntraUnderscore = true;
13496
13513
  if (INTRA_WORD_ASTERISK.test(seg.text))
13497
13514
  hasIntraAsterisk = true;
13515
+ if (!hasBoundaryAsterisk) {
13516
+ for (const m of seg.text.matchAll(BOUNDARY_FLANKED_ASTERISK)) {
13517
+ if (!isLineLeadingBullet(seg.text, m.index ?? 0)) {
13518
+ hasBoundaryAsterisk = true;
13519
+ break;
13520
+ }
13521
+ }
13522
+ }
13498
13523
  underscoreCount += seg.text.match(ANY_UNDERSCORE)?.length ?? 0;
13499
13524
  asteriskCount += seg.text.match(ANY_ASTERISK)?.length ?? 0;
13500
13525
  }
13501
13526
  INTRA_WORD_UNDERSCORE.lastIndex = 0;
13502
13527
  INTRA_WORD_ASTERISK.lastIndex = 0;
13528
+ BOUNDARY_FLANKED_ASTERISK.lastIndex = 0;
13503
13529
  const armUnderscore = hasIntraUnderscore && underscoreCount >= 2;
13504
13530
  const armAsterisk = hasIntraAsterisk && asteriskCount >= 2;
13505
- if (!armUnderscore && !armAsterisk)
13531
+ const armBoundaryAsterisk = hasBoundaryAsterisk;
13532
+ if (!armUnderscore && !armAsterisk && !armBoundaryAsterisk)
13506
13533
  return text;
13507
13534
  return segments.map((seg) => {
13508
13535
  if (seg.code)
@@ -13512,13 +13539,17 @@ function guardAccidentalEmphasis(text) {
13512
13539
  out = out.replace(INTRA_WORD_UNDERSCORE, "\\_");
13513
13540
  if (armAsterisk)
13514
13541
  out = out.replace(INTRA_WORD_ASTERISK, "\\*");
13542
+ if (armBoundaryAsterisk) {
13543
+ out = out.replace(BOUNDARY_FLANKED_ASTERISK, (m, offset, str) => isLineLeadingBullet(str, offset) ? m : "\\*");
13544
+ }
13515
13545
  return out;
13516
13546
  }).join("");
13517
13547
  }
13518
- var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
13548
+ var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, BOUNDARY_FLANKED_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
13519
13549
  var init_emphasis_guard = __esm(() => {
13520
13550
  INTRA_WORD_UNDERSCORE = /(?<=[A-Za-z0-9])_(?=[A-Za-z0-9])/g;
13521
13551
  INTRA_WORD_ASTERISK = /(?<=[A-Za-z0-9])\*(?=[A-Za-z0-9])/g;
13552
+ BOUNDARY_FLANKED_ASTERISK = /(?<=^|\s)\*(?=\s|$)/g;
13522
13553
  ANY_UNDERSCORE = /(?<!\\)_/g;
13523
13554
  ANY_ASTERISK = /(?<!\\)\*/g;
13524
13555
  });
@@ -13569,7 +13600,7 @@ function guardAccidentalBlockConstructs(text) {
13569
13600
  return out;
13570
13601
  }
13571
13602
  function escapeAccidentalHeadingLine(line) {
13572
- return line.replace(ACCIDENTAL_HEADING, "$1\\$2");
13603
+ return line.replace(ACCIDENTAL_HEADING, "$1\\$2").replace(ACCIDENTAL_HEADING_AFTER_MARKER, "$1\\$2");
13573
13604
  }
13574
13605
  function guardAccidentalHeading(text) {
13575
13606
  if (!text.includes("#"))
@@ -13599,11 +13630,12 @@ function guardAccidentalHeading(text) {
13599
13630
  }
13600
13631
  return out;
13601
13632
  }
13602
- var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING;
13633
+ var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING, ACCIDENTAL_HEADING_AFTER_MARKER;
13603
13634
  var init_line_start_guard = __esm(() => {
13604
13635
  ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
13605
13636
  ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
13606
13637
  ACCIDENTAL_HEADING = /^([ \t]{0,3})(#{1,6})(?=[^\s#])/;
13638
+ ACCIDENTAL_HEADING_AFTER_MARKER = /^([ \t]{0,3}(?:(?:[-*+]|\d{1,3}[.)])[ \t]+|>[ \t]*)+)(#{1,6})(?=[^\s#])/;
13607
13639
  });
13608
13640
 
13609
13641
  // render/inline-pairs-guard.ts
@@ -22616,7 +22648,7 @@ function deriveOverlayTitle(raw, fileName) {
22616
22648
  return;
22617
22649
  return base.length > 0 ? base : undefined;
22618
22650
  }
22619
- function readOverlayFile(agentName, file, agentCfg, warnings) {
22651
+ function readOverlayFile(agentName, file, agentCfg, warnings, source) {
22620
22652
  try {
22621
22653
  return readFileSync5(file, "utf-8");
22622
22654
  } catch (err) {
@@ -22629,7 +22661,7 @@ function readOverlayFile(agentName, file, agentCfg, warnings) {
22629
22661
  reason: `read error: ${err.message}`,
22630
22662
  code: code ?? "EUNKNOWN"
22631
22663
  };
22632
- recordReadFailure(agentCfg, { file, code: w.code });
22664
+ recordReadFailure(agentCfg, { file, code: w.code, source });
22633
22665
  warnings.push(w);
22634
22666
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
22635
22667
  return;
@@ -22693,14 +22725,14 @@ function applyAgentOverlays(config) {
22693
22725
  reason: `read error: cannot list overlay directory (${code})`,
22694
22726
  code
22695
22727
  };
22696
- recordReadFailure(agentCfg, { file: scheduleDir, code });
22728
+ recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
22697
22729
  warnings.push(w);
22698
22730
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
22699
22731
  });
22700
22732
  if (files.length > 0) {
22701
22733
  const merged = [...agentCfg.schedule ?? []];
22702
22734
  for (const file of files) {
22703
- const raw = readOverlayFile(agentName, file, agentCfg, warnings);
22735
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
22704
22736
  if (raw === undefined)
22705
22737
  continue;
22706
22738
  try {
@@ -22745,7 +22777,7 @@ function applyAgentOverlays(config) {
22745
22777
  reason: `read error: cannot list overlay directory (${code})`,
22746
22778
  code
22747
22779
  };
22748
- recordReadFailure(agentCfg, { file: skillsDir, code });
22780
+ recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
22749
22781
  warnings.push(w);
22750
22782
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
22751
22783
  });
@@ -22753,7 +22785,7 @@ function applyAgentOverlays(config) {
22753
22785
  const merged = [...agentCfg.skills ?? []];
22754
22786
  const seen = new Set(merged);
22755
22787
  for (const file of skillFiles) {
22756
- const raw = readOverlayFile(agentName, file, agentCfg, warnings);
22788
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
22757
22789
  if (raw === undefined)
22758
22790
  continue;
22759
22791
  try {
@@ -39617,6 +39649,22 @@ function redactAuthCodeMessage(api, chatId, messageId, log) {
39617
39649
 
39618
39650
  // ask-user.ts
39619
39651
  import { randomBytes as randomBytes2 } from "crypto";
39652
+
39653
+ // gateway/source-message-id.ts
39654
+ var MAX_TELEGRAM_MESSAGE_ID = 2 ** 31;
39655
+ function parseSourceMessageId(raw) {
39656
+ if (raw == null)
39657
+ return null;
39658
+ const s = String(raw);
39659
+ if (!/^\d+$/.test(s))
39660
+ return null;
39661
+ const n = Number(s);
39662
+ if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID)
39663
+ return null;
39664
+ return n;
39665
+ }
39666
+
39667
+ // ask-user.ts
39620
39668
  var ASK_USER_DEFAULT_TIMEOUT_MS = 300000;
39621
39669
  var ASK_USER_MAX_TIMEOUT_MS = 1800000;
39622
39670
  var ASK_USER_MIN_TIMEOUT_MS = 5000;
@@ -39653,13 +39701,7 @@ function validateAskUserArgs(args) {
39653
39701
  throw new Error("ask_user: message_thread_id must be a positive integer string");
39654
39702
  }
39655
39703
  }
39656
- let replyTo;
39657
- if (args.reply_to != null) {
39658
- replyTo = Number(args.reply_to);
39659
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
39660
- throw new Error("ask_user: reply_to must be a positive integer string");
39661
- }
39662
- }
39704
+ const replyTo = parseSourceMessageId(args.reply_to) ?? undefined;
39663
39705
  let timeoutMs = args.timeout_ms ?? ASK_USER_DEFAULT_TIMEOUT_MS;
39664
39706
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
39665
39707
  throw new Error("ask_user: timeout_ms must be a number");
@@ -39739,6 +39781,7 @@ function applyChecklistPatch(cl, patch) {
39739
39781
  return { title: patch.title ?? cl.title, tasks };
39740
39782
  }
39741
39783
  function buildNativeChecklistPayload(p) {
39784
+ const replyAnchor = parseSourceMessageId(p.replyToMessageId);
39742
39785
  return {
39743
39786
  business_connection_id: p.businessConnectionId,
39744
39787
  chat_id: p.chatId,
@@ -39746,7 +39789,7 @@ function buildNativeChecklistPayload(p) {
39746
39789
  title: p.title,
39747
39790
  tasks: p.tasks.map((t) => ({ id: t.id, text: t.text }))
39748
39791
  },
39749
- ...p.replyToMessageId != null ? { reply_parameters: { message_id: p.replyToMessageId } } : {},
39792
+ ...replyAnchor != null ? { reply_parameters: { message_id: replyAnchor } } : {},
39750
39793
  ...p.protectContent === true ? { protect_content: true } : {}
39751
39794
  };
39752
39795
  }
@@ -40030,13 +40073,7 @@ function resolveStickerSendArgs(raw, aliasMap) {
40030
40073
  throw new Error("send_sticker: message_thread_id must be a positive integer string");
40031
40074
  }
40032
40075
  }
40033
- let replyTo;
40034
- if (raw.reply_to != null) {
40035
- replyTo = Number(raw.reply_to);
40036
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
40037
- throw new Error("send_sticker: reply_to must be a positive integer string");
40038
- }
40039
- }
40076
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
40040
40077
  return {
40041
40078
  chatId: raw.chat_id,
40042
40079
  fileId,
@@ -40088,13 +40125,7 @@ function resolveGifSendArgs(raw) {
40088
40125
  throw new Error("send_gif: message_thread_id must be a positive integer string");
40089
40126
  }
40090
40127
  }
40091
- let replyTo;
40092
- if (raw.reply_to != null) {
40093
- replyTo = Number(raw.reply_to);
40094
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
40095
- throw new Error("send_gif: reply_to must be a positive integer string");
40096
- }
40097
- }
40128
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
40098
40129
  return {
40099
40130
  chatId: raw.chat_id,
40100
40131
  animationRef,
@@ -50208,6 +50239,20 @@ class PinRightsCache2 {
50208
50239
  }
50209
50240
  }
50210
50241
 
50242
+ // gateway/source-message-id.ts
50243
+ var MAX_TELEGRAM_MESSAGE_ID2 = 2 ** 31;
50244
+ function parseSourceMessageId2(raw) {
50245
+ if (raw == null)
50246
+ return null;
50247
+ const s = String(raw);
50248
+ if (!/^\d+$/.test(s))
50249
+ return null;
50250
+ const n = Number(s);
50251
+ if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID2)
50252
+ return null;
50253
+ return n;
50254
+ }
50255
+
50211
50256
  // gateway/permission-timeout.ts
50212
50257
  var SIGNATURE_SEP = String.fromCharCode(0);
50213
50258
  function permissionSignature(toolName, inputPreview) {
@@ -51221,7 +51266,7 @@ function createAuthBrokerClient() {
51221
51266
  listState: () => broker.listState(),
51222
51267
  setActive: (label) => broker.setActive(label),
51223
51268
  markExhausted: (until) => broker.markExhausted(until),
51224
- markThrottled: (until) => broker.markThrottled(until),
51269
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
51225
51270
  rmAccount: (label) => broker.rmAccount(label),
51226
51271
  refreshAccount: (label) => broker.refreshAccount(label),
51227
51272
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -73979,7 +74024,7 @@ function createAuthBrokerClient2() {
73979
74024
  listState: () => broker.listState(),
73980
74025
  setActive: (label) => broker.setActive(label),
73981
74026
  markExhausted: (until) => broker.markExhausted(until),
73982
- markThrottled: (until) => broker.markThrottled(until),
74027
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
73983
74028
  rmAccount: (label) => broker.rmAccount(label),
73984
74029
  refreshAccount: (label) => broker.refreshAccount(label),
73985
74030
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -74398,12 +74443,13 @@ class AuthBrokerClient2 {
74398
74443
  const data = await this.send(req);
74399
74444
  return data;
74400
74445
  }
74401
- async markThrottled(until) {
74446
+ async markThrottled(until, probeOnly = false) {
74402
74447
  const data = await this.send({
74403
74448
  v: PROTOCOL_VERSION,
74404
74449
  id: randomUUID5(),
74405
74450
  op: "mark-throttled",
74406
- until
74451
+ until,
74452
+ ...probeOnly ? { probeOnly: true } : {}
74407
74453
  });
74408
74454
  return data;
74409
74455
  }
@@ -77103,6 +77149,15 @@ function classify429Detail2(text4) {
77103
77149
  return "litellm-local";
77104
77150
  return "generic-transient";
77105
77151
  }
77152
+ function classification429WarrantsCorroboration(classification) {
77153
+ return classification === "generic-transient";
77154
+ }
77155
+ function routeRateLimit429(classification, runner, agent) {
77156
+ if (!classification429WarrantsCorroboration(classification))
77157
+ return false;
77158
+ runner.fireProbeOnly(agent);
77159
+ return true;
77160
+ }
77106
77161
  function build429ClassifiedMetric(opts) {
77107
77162
  const detail = typeof opts.detail === "string" ? opts.detail : "";
77108
77163
  const litellm = parseLitellmLimitDetail(detail, new Date(opts.now));
@@ -77199,6 +77254,12 @@ function createThrottleTierRunner(deps) {
77199
77254
  deps.log(`[throttle-tier] resume suppressed (${verdict}) reason=${reason}`);
77200
77255
  }
77201
77256
  }
77257
+ async function announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs) {
77258
+ deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
77259
+ await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
77260
+ if (rolledTo)
77261
+ nudgeResume("throttle-escalation-resume", armedAtMs);
77262
+ }
77202
77263
  async function fire(triggerAgent, throttledUntilMs, resetParsed) {
77203
77264
  const armedAtMs = now();
77204
77265
  let client3 = null;
@@ -77219,10 +77280,7 @@ function createThrottleTierRunner(deps) {
77219
77280
  deps.log(`[throttle-tier] markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
77220
77281
  }
77221
77282
  if (escalated) {
77222
- deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
77223
- await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
77224
- if (rolledTo)
77225
- nudgeResume("throttle-escalation-resume", armedAtMs);
77283
+ await announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs);
77226
77284
  return;
77227
77285
  }
77228
77286
  const cooldownKey = account ?? `agent:${triggerAgent}`;
@@ -77247,8 +77305,34 @@ function createThrottleTierRunner(deps) {
77247
77305
  nudgeResume("throttle-retry-resume", armedAtMs);
77248
77306
  }, delayMs);
77249
77307
  }
77308
+ async function fireProbeOnly(triggerAgent) {
77309
+ const armedAtMs = now();
77310
+ let client3 = null;
77311
+ let account = null;
77312
+ let escalated = false;
77313
+ let rolledTo = null;
77314
+ try {
77315
+ client3 = await deps.getBrokerClient();
77316
+ if (client3) {
77317
+ const r = await client3.markThrottled(now() + 1, true);
77318
+ account = r.account;
77319
+ escalated = r.escalated;
77320
+ rolledTo = r.rolledTo ?? null;
77321
+ } else {
77322
+ deps.log(`[throttle-tier] broker unreachable \u2014 probe-only skipped agent=${triggerAgent}`);
77323
+ }
77324
+ } catch (err) {
77325
+ deps.log(`[throttle-tier] probe-only markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
77326
+ }
77327
+ if (escalated) {
77328
+ await announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs);
77329
+ return;
77330
+ }
77331
+ deps.log(`[throttle-tier] generic-transient probe-only inert (no wall) agent=${triggerAgent}`);
77332
+ }
77250
77333
  return {
77251
77334
  fire,
77335
+ fireProbeOnly,
77252
77336
  inspect: () => ({ noticeState, nudgePending: pendingNudge != null })
77253
77337
  };
77254
77338
  }
@@ -79457,6 +79541,26 @@ function stampsHandbackMarker(source) {
79457
79541
  return known.decoupledCompletion;
79458
79542
  }
79459
79543
  var HANDBACK_RECENCY_WINDOW_MS = 60000;
79544
+ var TASK_NOTIFICATION_DEDUP_TTL_MS = 30000;
79545
+ var NOTIF_TERMINAL_STATUSES = new Set(["completed", "failed", "killed"]);
79546
+
79547
+ class CliTaskNotificationLedger {
79548
+ seen = new Map;
79549
+ record(taskId, status, now) {
79550
+ if (taskId.length === 0 || !NOTIF_TERMINAL_STATUSES.has(status))
79551
+ return;
79552
+ this.seen.set(taskId, now);
79553
+ for (const [id, ts] of this.seen) {
79554
+ if (now - ts > TASK_NOTIFICATION_DEDUP_TTL_MS)
79555
+ this.seen.delete(id);
79556
+ }
79557
+ }
79558
+ seenRecently(taskId, now) {
79559
+ const ts = this.seen.get(taskId);
79560
+ return ts != null && now - ts <= TASK_NOTIFICATION_DEDUP_TTL_MS;
79561
+ }
79562
+ }
79563
+ var cliTaskNotifLedger = new CliTaskNotificationLedger;
79460
79564
  var MAIN_THREAD_KEY = "<main>";
79461
79565
 
79462
79566
  class SubagentHandbackMarker {
@@ -80205,7 +80309,7 @@ async function sendReply(deps, req) {
80205
80309
  }
80206
80310
  const files = args.files ?? [];
80207
80311
  const quoteOptIn = args.quote !== false;
80208
- let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined;
80312
+ let reply_to = parseSourceMessageId(args.reply_to) ?? undefined;
80209
80313
  const protectContent = args.protect_content === true;
80210
80314
  const quoteText = args.quote_text;
80211
80315
  const access = loadAccess();
@@ -82011,20 +82115,6 @@ function formatEventDetail(event) {
82011
82115
  }
82012
82116
  }
82013
82117
 
82014
- // gateway/source-message-id.ts
82015
- var MAX_TELEGRAM_MESSAGE_ID = 2 ** 31;
82016
- function parseSourceMessageId(raw) {
82017
- if (raw == null)
82018
- return null;
82019
- const s = String(raw);
82020
- if (!/^\d+$/.test(s))
82021
- return null;
82022
- const n = Number(s);
82023
- if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID)
82024
- return null;
82025
- return n;
82026
- }
82027
-
82028
82118
  // gateway/status-surface-log.ts
82029
82119
  function formatTurnLifecycle(action, reason, t, now) {
82030
82120
  const ageMs = action === "clear" ? Math.max(0, now - t.startedAt) : 0;
@@ -84230,6 +84320,26 @@ function isTurnFlushSafetyEnabled(env = process.env) {
84230
84320
  }
84231
84321
 
84232
84322
  // gateway/subagent-handback-marker.ts
84323
+ var TASK_NOTIFICATION_DEDUP_TTL_MS2 = 30000;
84324
+ var NOTIF_TERMINAL_STATUSES2 = new Set(["completed", "failed", "killed"]);
84325
+
84326
+ class CliTaskNotificationLedger2 {
84327
+ seen = new Map;
84328
+ record(taskId, status, now) {
84329
+ if (taskId.length === 0 || !NOTIF_TERMINAL_STATUSES2.has(status))
84330
+ return;
84331
+ this.seen.set(taskId, now);
84332
+ for (const [id, ts] of this.seen) {
84333
+ if (now - ts > TASK_NOTIFICATION_DEDUP_TTL_MS2)
84334
+ this.seen.delete(id);
84335
+ }
84336
+ }
84337
+ seenRecently(taskId, now) {
84338
+ const ts = this.seen.get(taskId);
84339
+ return ts != null && now - ts <= TASK_NOTIFICATION_DEDUP_TTL_MS2;
84340
+ }
84341
+ }
84342
+ var cliTaskNotifLedger2 = new CliTaskNotificationLedger2;
84233
84343
  var MAIN_THREAD_KEY2 = "<main>";
84234
84344
 
84235
84345
  class SubagentHandbackMarker2 {
@@ -97011,6 +97121,9 @@ function decideSubagentHandback(input) {
97011
97121
  if (!input.isBackground) {
97012
97122
  return { deliver: false, reason: "foreground" };
97013
97123
  }
97124
+ if (input.cliTaskNotificationSeen === true) {
97125
+ return { deliver: false, reason: "cli-task-notification" };
97126
+ }
97014
97127
  const chatId = input.fleetChatId || input.ownerChatId;
97015
97128
  if (!chatId) {
97016
97129
  return { deliver: false, reason: "no-chat" };
@@ -102693,10 +102806,10 @@ function startOutboxSweep(deps) {
102693
102806
  }
102694
102807
 
102695
102808
  // ../src/build-info.ts
102696
- var VERSION2 = "0.20.8";
102697
- var COMMIT_SHA = "a6efc10f";
102698
- var COMMIT_DATE = "2026-08-04T21:38:16Z";
102699
- var LATEST_PR = 4374;
102809
+ var VERSION2 = "0.20.9";
102810
+ var COMMIT_SHA = "63c4c44b";
102811
+ var COMMIT_DATE = "2026-08-05T03:51:28Z";
102812
+ var LATEST_PR = 4388;
102700
102813
  var COMMITS_AHEAD_OF_TAG = 0;
102701
102814
 
102702
102815
  // gateway/boot-version.ts
@@ -107873,6 +107986,7 @@ function emitGatewayOperatorEvent(event) {
107873
107986
  agent,
107874
107987
  shouldEmitCard: (a) => shouldEmitOperatorEvent(a, "rate-limited")
107875
107988
  });
107989
+ routeRateLimit429(rateLimit429Classification, throttleTierRunner, agent);
107876
107990
  if (surface === "litellm-local-notice") {
107877
107991
  process.stderr.write(`telegram gateway: 429 classified litellm-proxy-local agent=${agent} \u2014 ` + `calm path, no account attribution, no failover
107878
107992
  `);
@@ -109150,6 +109264,8 @@ if (isGatewayMain)
109150
109264
  }
109151
109265
  }
109152
109266
  const ev = msg.event;
109267
+ if (ev.kind === "task_notification")
109268
+ cliTaskNotifLedger2.record(ev.taskId, ev.status, Date.now());
109153
109269
  handleSessionEvent2(ev);
109154
109270
  toolFlightTracker.onEvent(ev);
109155
109271
  if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
@@ -109867,7 +109983,7 @@ async function executeSendChecklist(args) {
109867
109983
  if (!Array.isArray(tasks) || tasks.length === 0)
109868
109984
  throw new Error("send_checklist: tasks must be a non-empty array");
109869
109985
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
109870
- const replyTo = args.reply_to != null ? Number(args.reply_to) : undefined;
109986
+ const replyTo = parseSourceMessageId2(args.reply_to) ?? undefined;
109871
109987
  const protectContent = args.protect_content === true;
109872
109988
  assertAllowedChat(chat_id);
109873
109989
  const { title: redactedTitle, tasks: redactedTasks } = redactChecklistFields(title, tasks, (t) => redactOutboundText(t, "send_checklist"));
@@ -116715,11 +116831,12 @@ async function startGateway() {
116715
116831
  ownerChatId: hbOwnerDm,
116716
116832
  taskDescription: description2,
116717
116833
  resultText,
116718
- jsonlAgentId: agentId
116834
+ jsonlAgentId: agentId,
116835
+ cliTaskNotificationSeen: cliTaskNotifLedger2.seenRecently(agentId, Date.now())
116719
116836
  });
116720
116837
  if (!decision.deliver) {
116721
- if (decision.reason === "no-chat") {
116722
- process.stderr.write(`telegram gateway: subagent-handback ${agentId} \u2014 no chat to deliver to; skipped
116838
+ if (decision.reason === "no-chat" || decision.reason === "cli-task-notification") {
116839
+ process.stderr.write(`telegram gateway: subagent-handback ${agentId} skipped \u2014 ${decision.reason === "no-chat" ? "no chat to deliver to" : "CLI task-notification already woke the parent for this completion (double-wake dedup)"}
116723
116840
  `);
116724
116841
  }
116725
116842
  return;
@@ -28,7 +28,7 @@ export function createAuthBrokerClient(): {
28
28
  listState: () => broker.listState(),
29
29
  setActive: (label: string) => broker.setActive(label),
30
30
  markExhausted: (until?: number) => broker.markExhausted(until),
31
- markThrottled: (until: number) => broker.markThrottled(until),
31
+ markThrottled: (until: number, probeOnly?: boolean) => broker.markThrottled(until, probeOnly),
32
32
  rmAccount: (label: string) => broker.rmAccount(label),
33
33
  refreshAccount: (label: string) => broker.refreshAccount(label),
34
34
  setOverride: (agent: string, account: string | null) =>
@@ -334,10 +334,12 @@ export interface AuthBrokerClient {
334
334
  * per-account rate limit on the CALLER's own account — `throttled_until`
335
335
  * in the quota ledger — WITHOUT rolling the fleet and WITHOUT touching
336
336
  * eligibility. `escalated` is true when the broker's escalation guard
337
- * (repeated hits corroborated by a live probe) converted it into the
337
+ * (a first-hit live probe corroborating a wall) converted it into the
338
338
  * standard mark-exhausted + roll; `rolledTo` names the roll target then.
339
+ * `probeOnly` (#failover-429-corroborate, generic-transient origin): run ONLY
340
+ * the escalation probe — a healthy probe records nothing (account stays inert).
339
341
  */
340
- markThrottled(until: number): Promise<{
342
+ markThrottled(until: number, probeOnly?: boolean): Promise<{
341
343
  account: string
342
344
  throttled_until: number
343
345
  escalated: boolean
@@ -33,6 +33,8 @@
33
33
  * (same pattern as checklist-message-handler.ts).
34
34
  */
35
35
 
36
+ import { parseSourceMessageId } from './source-message-id.js'
37
+
36
38
  export interface ChecklistTaskState {
37
39
  /** 1-based sequential id assigned at send time (mirrors the native API's
38
40
  * required per-task integer id; doubles as the patch handle in text mode). */
@@ -130,6 +132,11 @@ export function buildNativeChecklistPayload(p: {
130
132
  replyToMessageId?: number
131
133
  protectContent?: boolean
132
134
  }): Record<string, unknown> {
135
+ // #4368 — defense at the payload boundary: a fabricated / out-of-int32
136
+ // reply anchor is dropped here so the native checklist sends UNANCHORED
137
+ // rather than Telegram 400ing on `reply_parameters.message_id`. Independent
138
+ // of any caller-side guard so this builder can never emit a bad anchor.
139
+ const replyAnchor = parseSourceMessageId(p.replyToMessageId)
133
140
  return {
134
141
  business_connection_id: p.businessConnectionId,
135
142
  chat_id: p.chatId,
@@ -137,7 +144,7 @@ export function buildNativeChecklistPayload(p: {
137
144
  title: p.title,
138
145
  tasks: p.tasks.map((t) => ({ id: t.id, text: t.text })),
139
146
  },
140
- ...(p.replyToMessageId != null ? { reply_parameters: { message_id: p.replyToMessageId } } : {}),
147
+ ...(replyAnchor != null ? { reply_parameters: { message_id: replyAnchor } } : {}),
141
148
  ...(p.protectContent === true ? { protect_content: true } : {}),
142
149
  }
143
150
  }
@@ -427,6 +427,7 @@ import {
427
427
  build429ClassifiedMetric,
428
428
  classify429Detail,
429
429
  decideThrottleTier,
430
+ routeRateLimit429,
430
431
  throttleRetryInPlaceMaxMs,
431
432
  } from '../throttle-tier.js'
432
433
  import { createThrottleTierRunner } from './throttle-tier-wiring.js'
@@ -497,7 +498,7 @@ import {
497
498
  type ReplyOwnerTier, type ReplyOwnerCandidates,
498
499
  type AnswerDeliveredLatch,
499
500
  } from '../reply-owner-resolve.js'
500
- import { SubagentHandbackMarker } from './subagent-handback-marker.js'
501
+ import { SubagentHandbackMarker, cliTaskNotifLedger } from './subagent-handback-marker.js'
501
502
  // PR A — deterministic answer-ready quiescence flush (late-delivery fix).
502
503
  import { AnswerReadyFlushController, resolveAnswerReadyFlushMs, resolveAnswerStageMs } from '../answer-ready-flush.js'
503
504
  // #1667 — pure decision core for the turn_end answer-delivery gate (#1664).
@@ -7827,6 +7828,7 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
7827
7828
  agent,
7828
7829
  shouldEmitCard: (a) => shouldEmitOperatorEvent(a, 'rate-limited'),
7829
7830
  })
7831
+ routeRateLimit429(rateLimit429Classification, throttleTierRunner, agent) // #failover-429-corroborate probe-only seam; boolean return is for test observability (throttle-tier-route-429.test.ts), intentionally discarded here — see docstring
7830
7832
  if (surface === 'litellm-local-notice') {
7831
7833
  process.stderr.write(
7832
7834
  `telegram gateway: 429 classified litellm-proxy-local agent=${agent} — ` +
@@ -10690,6 +10692,7 @@ if (isGatewayMain) ipcServer = createIpcServer({
10690
10692
  }
10691
10693
  }
10692
10694
  const ev = msg.event as unknown as SessionEvent
10695
+ if (ev.kind === 'task_notification') cliTaskNotifLedger.record(ev.taskId, ev.status, Date.now()) // double-wake dedup — ledger doc in subagent-handback-marker.ts
10693
10696
  // #1122/#1126: session events used to be ingested into the pinned progress
10694
10697
  // card here (`progressDriver.ingest`). The card is retired and the driver
10695
10698
  // is permanently null, so that call was a dead no-op — removed. Session
@@ -11917,7 +11920,7 @@ async function executeSendChecklist(args: Record<string, unknown>): Promise<{ co
11917
11920
  const tasks = args.tasks as Array<{ text: string; done?: boolean }> | undefined
11918
11921
  if (!Array.isArray(tasks) || tasks.length === 0) throw new Error('send_checklist: tasks must be a non-empty array')
11919
11922
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
11920
- const replyTo = args.reply_to != null ? Number(args.reply_to) : undefined
11923
+ const replyTo = parseSourceMessageId(args.reply_to as string | number | null | undefined) ?? undefined // #4368: drop a fabricated/out-of-int32 anchor so the send lands unanchored, not 400
11921
11924
  const protectContent = args.protect_content === true
11922
11925
 
11923
11926
  assertAllowedChat(chat_id)
@@ -24370,11 +24373,12 @@ async function startGateway(): Promise<void> { // #2996 P0c: the boot IIFE, now
24370
24373
  // deterministic dedup key — closes the #1719
24371
24374
  // re-fire-on-restart class.
24372
24375
  jsonlAgentId: agentId,
24376
+ cliTaskNotificationSeen: cliTaskNotifLedger.seenRecently(agentId, Date.now()), // double-wake dedup, fail-open — ledger doc in subagent-handback-marker.ts
24373
24377
  })
24374
24378
  if (!decision.deliver) {
24375
- if (decision.reason === 'no-chat') {
24379
+ if (decision.reason === 'no-chat' || decision.reason === 'cli-task-notification') {
24376
24380
  process.stderr.write(
24377
- `telegram gateway: subagent-handback ${agentId} — no chat to deliver to; skipped\n`,
24381
+ `telegram gateway: subagent-handback ${agentId} skipped ${decision.reason === 'no-chat' ? 'no chat to deliver to' : 'CLI task-notification already woke the parent for this completion (double-wake dedup)'}\n`,
24378
24382
  )
24379
24383
  }
24380
24384
  return
@@ -73,6 +73,7 @@ import { getBuzzMirror } from './buzz-mirror.js'
73
73
  import { isFinalAnswerReply, isSubstantiveFinalReply, shouldJournalReplySiteDelivery } from '../final-answer-detect.js'
74
74
  import { decideOverPing, type OverPingDecision } from '../over-ping-safety-net.js'
75
75
  import { decideSilentReplyAnchor } from '../silent-reply-anchor.js'
76
+ import { parseSourceMessageId } from './source-message-id.js'
76
77
  import {
77
78
  decideSupersedeCorrection,
78
79
  flushedAnswerMatchesReply,
@@ -1343,7 +1344,14 @@ export async function sendReply(
1343
1344
 
1344
1345
  const files = (args.files as string[] | undefined) ?? []
1345
1346
  const quoteOptIn = args.quote !== false
1346
- let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined
1347
+ // #4368 the model's reply tool can quote a synthetic inbound (a boot-
1348
+ // resume/handback/cron fabricated id at `Date.now()` scale). Route it through
1349
+ // the canonical guard so an out-of-int32 anchor is DROPPED (send lands
1350
+ // unanchored) rather than 400ing every chunk on `reply_parameters.message_id`.
1351
+ // A later `reply_to = latest` (quote-opt-in default) is a Telegram-returned
1352
+ // id and needs no re-check. Also closes the pre-existing NaN hole: a non-
1353
+ // numeric `reply_to` used to coerce to NaN and still build the anchor.
1354
+ let reply_to = parseSourceMessageId(args.reply_to as string | number | null | undefined) ?? undefined
1347
1355
  const protectContent = args.protect_content === true
1348
1356
  const quoteText = args.quote_text as string | undefined
1349
1357
  const access = loadAccess()