switchroom 0.20.8 → 0.20.10

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 (43) hide show
  1. package/bin/handoff-briefing.sh +57 -5
  2. package/bin/working-state-reload-hook.sh +262 -0
  3. package/dist/agent-scheduler/index.js +16 -13
  4. package/dist/auth-broker/index.js +70 -30
  5. package/dist/cli/autoaccept-poll.js +5 -3
  6. package/dist/cli/drive-write-pretool.mjs +5 -3
  7. package/dist/cli/ms-365-write-pretool.mjs +5 -3
  8. package/dist/cli/notion-write-pretool.mjs +6 -6
  9. package/dist/cli/switchroom.js +42 -13
  10. package/dist/host-control/main.js +7 -7
  11. package/dist/vault/approvals/kernel-server.js +6 -6
  12. package/dist/vault/broker/server.js +6 -6
  13. package/package.json +1 -1
  14. package/profiles/_base/start.sh.hbs +49 -0
  15. package/profiles/default/CLAUDE.md.hbs +12 -13
  16. package/telegram-plugin/ask-user.ts +6 -7
  17. package/telegram-plugin/dist/gateway/gateway.js +192 -66
  18. package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
  19. package/telegram-plugin/gateway/auth-command.ts +4 -2
  20. package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
  21. package/telegram-plugin/gateway/gateway.ts +8 -4
  22. package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
  23. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
  24. package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
  25. package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
  26. package/telegram-plugin/render/emphasis-guard.ts +92 -12
  27. package/telegram-plugin/render/line-start-guard.ts +27 -2
  28. package/telegram-plugin/sticker-aliases.ts +12 -14
  29. package/telegram-plugin/tests/ask-user.test.ts +15 -0
  30. package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
  31. package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
  32. package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
  33. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
  34. package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
  35. package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
  36. package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
  37. package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
  38. package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
  39. package/telegram-plugin/throttle-tier.ts +59 -0
  40. package/vendor/hindsight-memory/CHANGELOG.md +31 -0
  41. package/vendor/hindsight-memory/hooks/hooks.json +2 -1
  42. package/vendor/hindsight-memory/scripts/session_start.py +35 -8
  43. package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
@@ -7308,6 +7308,12 @@ function refillNormalizedUtils(q, now) {
7308
7308
  sevenDayRefilled
7309
7309
  };
7310
7310
  }
7311
+ var ENTITLEMENT_BLOCKED_ERROR_CODES;
7312
+ var init_quota = __esm(() => {
7313
+ ENTITLEMENT_BLOCKED_ERROR_CODES = new Set([
7314
+ "oauth_not_allowed_for_organization"
7315
+ ]);
7316
+ });
7311
7317
 
7312
7318
  // ../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
7313
7319
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
@@ -11329,7 +11335,8 @@ var init_protocol = __esm(() => {
11329
11335
  v: exports_external.literal(PROTOCOL_VERSION),
11330
11336
  op: exports_external.literal("mark-throttled"),
11331
11337
  id: exports_external.string().min(1),
11332
- until: exports_external.number().int().positive()
11338
+ until: exports_external.number().int().positive(),
11339
+ probeOnly: exports_external.boolean().optional()
11333
11340
  });
11334
11341
  RefreshAccountRequestSchema = exports_external.object({
11335
11342
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -11725,12 +11732,13 @@ class AuthBrokerClient {
11725
11732
  const data = await this.send(req);
11726
11733
  return data;
11727
11734
  }
11728
- async markThrottled(until) {
11735
+ async markThrottled(until, probeOnly = false) {
11729
11736
  const data = await this.send({
11730
11737
  v: PROTOCOL_VERSION,
11731
11738
  id: randomUUID2(),
11732
11739
  op: "mark-throttled",
11733
- until
11740
+ until,
11741
+ ...probeOnly ? { probeOnly: true } : {}
11734
11742
  });
11735
11743
  return data;
11736
11744
  }
@@ -12693,6 +12701,7 @@ function buildSnapshotsFromCachedState(state) {
12693
12701
  }
12694
12702
  var THROTTLING_THRESHOLD_PCT = 80, OVERAGE_EXHAUSTED_REASONS, HEALTH_EMOJI, TABLE_HEALTH_RANK;
12695
12703
  var init_auth_snapshot_format = __esm(() => {
12704
+ init_quota();
12696
12705
  init_client();
12697
12706
  init_demo_mask();
12698
12707
  init_card_format();
@@ -13480,12 +13489,27 @@ var init_dollar_math_guard = __esm(() => {
13480
13489
  });
13481
13490
 
13482
13491
  // render/emphasis-guard.ts
13492
+ function isLineLeadingBullet(text, starIndex) {
13493
+ const next = text[starIndex + 1];
13494
+ if (next !== " " && next !== "\t")
13495
+ return false;
13496
+ let i = starIndex - 1;
13497
+ let indent = 0;
13498
+ while (i >= 0 && (text[i] === " " || text[i] === "\t")) {
13499
+ if (++indent > 3)
13500
+ return false;
13501
+ i--;
13502
+ }
13503
+ return i < 0 || text[i] === `
13504
+ `;
13505
+ }
13483
13506
  function guardAccidentalEmphasis(text) {
13484
13507
  if (!text.includes("_") && !text.includes("*"))
13485
13508
  return text;
13486
13509
  const segments = splitProtectedSegments(text);
13487
13510
  let hasIntraUnderscore = false;
13488
13511
  let hasIntraAsterisk = false;
13512
+ let hasBoundaryAsterisk = false;
13489
13513
  let underscoreCount = 0;
13490
13514
  let asteriskCount = 0;
13491
13515
  for (const seg of segments) {
@@ -13495,14 +13519,24 @@ function guardAccidentalEmphasis(text) {
13495
13519
  hasIntraUnderscore = true;
13496
13520
  if (INTRA_WORD_ASTERISK.test(seg.text))
13497
13521
  hasIntraAsterisk = true;
13522
+ if (!hasBoundaryAsterisk) {
13523
+ for (const m of seg.text.matchAll(BOUNDARY_FLANKED_ASTERISK)) {
13524
+ if (!isLineLeadingBullet(seg.text, m.index ?? 0)) {
13525
+ hasBoundaryAsterisk = true;
13526
+ break;
13527
+ }
13528
+ }
13529
+ }
13498
13530
  underscoreCount += seg.text.match(ANY_UNDERSCORE)?.length ?? 0;
13499
13531
  asteriskCount += seg.text.match(ANY_ASTERISK)?.length ?? 0;
13500
13532
  }
13501
13533
  INTRA_WORD_UNDERSCORE.lastIndex = 0;
13502
13534
  INTRA_WORD_ASTERISK.lastIndex = 0;
13535
+ BOUNDARY_FLANKED_ASTERISK.lastIndex = 0;
13503
13536
  const armUnderscore = hasIntraUnderscore && underscoreCount >= 2;
13504
13537
  const armAsterisk = hasIntraAsterisk && asteriskCount >= 2;
13505
- if (!armUnderscore && !armAsterisk)
13538
+ const armBoundaryAsterisk = hasBoundaryAsterisk;
13539
+ if (!armUnderscore && !armAsterisk && !armBoundaryAsterisk)
13506
13540
  return text;
13507
13541
  return segments.map((seg) => {
13508
13542
  if (seg.code)
@@ -13512,13 +13546,17 @@ function guardAccidentalEmphasis(text) {
13512
13546
  out = out.replace(INTRA_WORD_UNDERSCORE, "\\_");
13513
13547
  if (armAsterisk)
13514
13548
  out = out.replace(INTRA_WORD_ASTERISK, "\\*");
13549
+ if (armBoundaryAsterisk) {
13550
+ out = out.replace(BOUNDARY_FLANKED_ASTERISK, (m, offset, str) => isLineLeadingBullet(str, offset) ? m : "\\*");
13551
+ }
13515
13552
  return out;
13516
13553
  }).join("");
13517
13554
  }
13518
- var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
13555
+ var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, BOUNDARY_FLANKED_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
13519
13556
  var init_emphasis_guard = __esm(() => {
13520
13557
  INTRA_WORD_UNDERSCORE = /(?<=[A-Za-z0-9])_(?=[A-Za-z0-9])/g;
13521
13558
  INTRA_WORD_ASTERISK = /(?<=[A-Za-z0-9])\*(?=[A-Za-z0-9])/g;
13559
+ BOUNDARY_FLANKED_ASTERISK = /(?<=^|\s)\*(?=\s|$)/g;
13522
13560
  ANY_UNDERSCORE = /(?<!\\)_/g;
13523
13561
  ANY_ASTERISK = /(?<!\\)\*/g;
13524
13562
  });
@@ -13569,7 +13607,7 @@ function guardAccidentalBlockConstructs(text) {
13569
13607
  return out;
13570
13608
  }
13571
13609
  function escapeAccidentalHeadingLine(line) {
13572
- return line.replace(ACCIDENTAL_HEADING, "$1\\$2");
13610
+ return line.replace(ACCIDENTAL_HEADING, "$1\\$2").replace(ACCIDENTAL_HEADING_AFTER_MARKER, "$1\\$2");
13573
13611
  }
13574
13612
  function guardAccidentalHeading(text) {
13575
13613
  if (!text.includes("#"))
@@ -13599,11 +13637,12 @@ function guardAccidentalHeading(text) {
13599
13637
  }
13600
13638
  return out;
13601
13639
  }
13602
- var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING;
13640
+ var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING, ACCIDENTAL_HEADING_AFTER_MARKER;
13603
13641
  var init_line_start_guard = __esm(() => {
13604
13642
  ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
13605
13643
  ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
13606
13644
  ACCIDENTAL_HEADING = /^([ \t]{0,3})(#{1,6})(?=[^\s#])/;
13645
+ ACCIDENTAL_HEADING_AFTER_MARKER = /^([ \t]{0,3}(?:(?:[-*+]|\d{1,3}[.)])[ \t]+|>[ \t]*)+)(#{1,6})(?=[^\s#])/;
13607
13646
  });
13608
13647
 
13609
13648
  // render/inline-pairs-guard.ts
@@ -22616,7 +22655,7 @@ function deriveOverlayTitle(raw, fileName) {
22616
22655
  return;
22617
22656
  return base.length > 0 ? base : undefined;
22618
22657
  }
22619
- function readOverlayFile(agentName, file, agentCfg, warnings) {
22658
+ function readOverlayFile(agentName, file, agentCfg, warnings, source) {
22620
22659
  try {
22621
22660
  return readFileSync5(file, "utf-8");
22622
22661
  } catch (err) {
@@ -22629,7 +22668,7 @@ function readOverlayFile(agentName, file, agentCfg, warnings) {
22629
22668
  reason: `read error: ${err.message}`,
22630
22669
  code: code ?? "EUNKNOWN"
22631
22670
  };
22632
- recordReadFailure(agentCfg, { file, code: w.code });
22671
+ recordReadFailure(agentCfg, { file, code: w.code, source });
22633
22672
  warnings.push(w);
22634
22673
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
22635
22674
  return;
@@ -22693,14 +22732,14 @@ function applyAgentOverlays(config) {
22693
22732
  reason: `read error: cannot list overlay directory (${code})`,
22694
22733
  code
22695
22734
  };
22696
- recordReadFailure(agentCfg, { file: scheduleDir, code });
22735
+ recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
22697
22736
  warnings.push(w);
22698
22737
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
22699
22738
  });
22700
22739
  if (files.length > 0) {
22701
22740
  const merged = [...agentCfg.schedule ?? []];
22702
22741
  for (const file of files) {
22703
- const raw = readOverlayFile(agentName, file, agentCfg, warnings);
22742
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
22704
22743
  if (raw === undefined)
22705
22744
  continue;
22706
22745
  try {
@@ -22745,7 +22784,7 @@ function applyAgentOverlays(config) {
22745
22784
  reason: `read error: cannot list overlay directory (${code})`,
22746
22785
  code
22747
22786
  };
22748
- recordReadFailure(agentCfg, { file: skillsDir, code });
22787
+ recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
22749
22788
  warnings.push(w);
22750
22789
  console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
22751
22790
  });
@@ -22753,7 +22792,7 @@ function applyAgentOverlays(config) {
22753
22792
  const merged = [...agentCfg.skills ?? []];
22754
22793
  const seen = new Set(merged);
22755
22794
  for (const file of skillFiles) {
22756
- const raw = readOverlayFile(agentName, file, agentCfg, warnings);
22795
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
22757
22796
  if (raw === undefined)
22758
22797
  continue;
22759
22798
  try {
@@ -38273,6 +38312,7 @@ function buildSnapshotsFromCachedState2(state7) {
38273
38312
  }
38274
38313
  var THROTTLING_THRESHOLD_PCT2 = 80, OVERAGE_EXHAUSTED_REASONS2, HEALTH_EMOJI2, TABLE_HEALTH_RANK2;
38275
38314
  var init_auth_snapshot_format2 = __esm(() => {
38315
+ init_quota();
38276
38316
  init_client();
38277
38317
  init_demo_mask();
38278
38318
  init_card_format();
@@ -39001,6 +39041,7 @@ function renderUsageCard(snapshots, exhaustedByLabel, opts = {}) {
39001
39041
  }
39002
39042
  var FIVE_HOUR_MS, SEVEN_DAY_MS, BAR_WIDTH = 10, STATUS_LABEL;
39003
39043
  var init_quota_bar_format = __esm(() => {
39044
+ init_quota();
39004
39045
  init_client();
39005
39046
  init_auth_snapshot_format();
39006
39047
  init_card_format();
@@ -39617,6 +39658,22 @@ function redactAuthCodeMessage(api, chatId, messageId, log) {
39617
39658
 
39618
39659
  // ask-user.ts
39619
39660
  import { randomBytes as randomBytes2 } from "crypto";
39661
+
39662
+ // gateway/source-message-id.ts
39663
+ var MAX_TELEGRAM_MESSAGE_ID = 2 ** 31;
39664
+ function parseSourceMessageId(raw) {
39665
+ if (raw == null)
39666
+ return null;
39667
+ const s = String(raw);
39668
+ if (!/^\d+$/.test(s))
39669
+ return null;
39670
+ const n = Number(s);
39671
+ if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID)
39672
+ return null;
39673
+ return n;
39674
+ }
39675
+
39676
+ // ask-user.ts
39620
39677
  var ASK_USER_DEFAULT_TIMEOUT_MS = 300000;
39621
39678
  var ASK_USER_MAX_TIMEOUT_MS = 1800000;
39622
39679
  var ASK_USER_MIN_TIMEOUT_MS = 5000;
@@ -39653,13 +39710,7 @@ function validateAskUserArgs(args) {
39653
39710
  throw new Error("ask_user: message_thread_id must be a positive integer string");
39654
39711
  }
39655
39712
  }
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
- }
39713
+ const replyTo = parseSourceMessageId(args.reply_to) ?? undefined;
39663
39714
  let timeoutMs = args.timeout_ms ?? ASK_USER_DEFAULT_TIMEOUT_MS;
39664
39715
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
39665
39716
  throw new Error("ask_user: timeout_ms must be a number");
@@ -39739,6 +39790,7 @@ function applyChecklistPatch(cl, patch) {
39739
39790
  return { title: patch.title ?? cl.title, tasks };
39740
39791
  }
39741
39792
  function buildNativeChecklistPayload(p) {
39793
+ const replyAnchor = parseSourceMessageId(p.replyToMessageId);
39742
39794
  return {
39743
39795
  business_connection_id: p.businessConnectionId,
39744
39796
  chat_id: p.chatId,
@@ -39746,7 +39798,7 @@ function buildNativeChecklistPayload(p) {
39746
39798
  title: p.title,
39747
39799
  tasks: p.tasks.map((t) => ({ id: t.id, text: t.text }))
39748
39800
  },
39749
- ...p.replyToMessageId != null ? { reply_parameters: { message_id: p.replyToMessageId } } : {},
39801
+ ...replyAnchor != null ? { reply_parameters: { message_id: replyAnchor } } : {},
39750
39802
  ...p.protectContent === true ? { protect_content: true } : {}
39751
39803
  };
39752
39804
  }
@@ -40030,13 +40082,7 @@ function resolveStickerSendArgs(raw, aliasMap) {
40030
40082
  throw new Error("send_sticker: message_thread_id must be a positive integer string");
40031
40083
  }
40032
40084
  }
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
- }
40085
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
40040
40086
  return {
40041
40087
  chatId: raw.chat_id,
40042
40088
  fileId,
@@ -40088,13 +40134,7 @@ function resolveGifSendArgs(raw) {
40088
40134
  throw new Error("send_gif: message_thread_id must be a positive integer string");
40089
40135
  }
40090
40136
  }
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
- }
40137
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
40098
40138
  return {
40099
40139
  chatId: raw.chat_id,
40100
40140
  animationRef,
@@ -50208,6 +50248,20 @@ class PinRightsCache2 {
50208
50248
  }
50209
50249
  }
50210
50250
 
50251
+ // gateway/source-message-id.ts
50252
+ var MAX_TELEGRAM_MESSAGE_ID2 = 2 ** 31;
50253
+ function parseSourceMessageId2(raw) {
50254
+ if (raw == null)
50255
+ return null;
50256
+ const s = String(raw);
50257
+ if (!/^\d+$/.test(s))
50258
+ return null;
50259
+ const n = Number(s);
50260
+ if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID2)
50261
+ return null;
50262
+ return n;
50263
+ }
50264
+
50211
50265
  // gateway/permission-timeout.ts
50212
50266
  var SIGNATURE_SEP = String.fromCharCode(0);
50213
50267
  function permissionSignature(toolName, inputPreview) {
@@ -51221,7 +51275,7 @@ function createAuthBrokerClient() {
51221
51275
  listState: () => broker.listState(),
51222
51276
  setActive: (label) => broker.setActive(label),
51223
51277
  markExhausted: (until) => broker.markExhausted(until),
51224
- markThrottled: (until) => broker.markThrottled(until),
51278
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
51225
51279
  rmAccount: (label) => broker.rmAccount(label),
51226
51280
  refreshAccount: (label) => broker.refreshAccount(label),
51227
51281
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -73979,7 +74033,7 @@ function createAuthBrokerClient2() {
73979
74033
  listState: () => broker.listState(),
73980
74034
  setActive: (label) => broker.setActive(label),
73981
74035
  markExhausted: (until) => broker.markExhausted(until),
73982
- markThrottled: (until) => broker.markThrottled(until),
74036
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
73983
74037
  rmAccount: (label) => broker.rmAccount(label),
73984
74038
  refreshAccount: (label) => broker.refreshAccount(label),
73985
74039
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -74398,12 +74452,13 @@ class AuthBrokerClient2 {
74398
74452
  const data = await this.send(req);
74399
74453
  return data;
74400
74454
  }
74401
- async markThrottled(until) {
74455
+ async markThrottled(until, probeOnly = false) {
74402
74456
  const data = await this.send({
74403
74457
  v: PROTOCOL_VERSION,
74404
74458
  id: randomUUID5(),
74405
74459
  op: "mark-throttled",
74406
- until
74460
+ until,
74461
+ ...probeOnly ? { probeOnly: true } : {}
74407
74462
  });
74408
74463
  return data;
74409
74464
  }
@@ -77103,6 +77158,15 @@ function classify429Detail2(text4) {
77103
77158
  return "litellm-local";
77104
77159
  return "generic-transient";
77105
77160
  }
77161
+ function classification429WarrantsCorroboration(classification) {
77162
+ return classification === "generic-transient";
77163
+ }
77164
+ function routeRateLimit429(classification, runner, agent) {
77165
+ if (!classification429WarrantsCorroboration(classification))
77166
+ return false;
77167
+ runner.fireProbeOnly(agent);
77168
+ return true;
77169
+ }
77106
77170
  function build429ClassifiedMetric(opts) {
77107
77171
  const detail = typeof opts.detail === "string" ? opts.detail : "";
77108
77172
  const litellm = parseLitellmLimitDetail(detail, new Date(opts.now));
@@ -77199,6 +77263,12 @@ function createThrottleTierRunner(deps) {
77199
77263
  deps.log(`[throttle-tier] resume suppressed (${verdict}) reason=${reason}`);
77200
77264
  }
77201
77265
  }
77266
+ async function announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs) {
77267
+ deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
77268
+ await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
77269
+ if (rolledTo)
77270
+ nudgeResume("throttle-escalation-resume", armedAtMs);
77271
+ }
77202
77272
  async function fire(triggerAgent, throttledUntilMs, resetParsed) {
77203
77273
  const armedAtMs = now();
77204
77274
  let client3 = null;
@@ -77219,10 +77289,7 @@ function createThrottleTierRunner(deps) {
77219
77289
  deps.log(`[throttle-tier] markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
77220
77290
  }
77221
77291
  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);
77292
+ await announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs);
77226
77293
  return;
77227
77294
  }
77228
77295
  const cooldownKey = account ?? `agent:${triggerAgent}`;
@@ -77247,8 +77314,34 @@ function createThrottleTierRunner(deps) {
77247
77314
  nudgeResume("throttle-retry-resume", armedAtMs);
77248
77315
  }, delayMs);
77249
77316
  }
77317
+ async function fireProbeOnly(triggerAgent) {
77318
+ const armedAtMs = now();
77319
+ let client3 = null;
77320
+ let account = null;
77321
+ let escalated = false;
77322
+ let rolledTo = null;
77323
+ try {
77324
+ client3 = await deps.getBrokerClient();
77325
+ if (client3) {
77326
+ const r = await client3.markThrottled(now() + 1, true);
77327
+ account = r.account;
77328
+ escalated = r.escalated;
77329
+ rolledTo = r.rolledTo ?? null;
77330
+ } else {
77331
+ deps.log(`[throttle-tier] broker unreachable \u2014 probe-only skipped agent=${triggerAgent}`);
77332
+ }
77333
+ } catch (err) {
77334
+ deps.log(`[throttle-tier] probe-only markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
77335
+ }
77336
+ if (escalated) {
77337
+ await announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs);
77338
+ return;
77339
+ }
77340
+ deps.log(`[throttle-tier] generic-transient probe-only inert (no wall) agent=${triggerAgent}`);
77341
+ }
77250
77342
  return {
77251
77343
  fire,
77344
+ fireProbeOnly,
77252
77345
  inspect: () => ({ noticeState, nudgePending: pendingNudge != null })
77253
77346
  };
77254
77347
  }
@@ -79457,6 +79550,26 @@ function stampsHandbackMarker(source) {
79457
79550
  return known.decoupledCompletion;
79458
79551
  }
79459
79552
  var HANDBACK_RECENCY_WINDOW_MS = 60000;
79553
+ var TASK_NOTIFICATION_DEDUP_TTL_MS = 30000;
79554
+ var NOTIF_TERMINAL_STATUSES = new Set(["completed", "failed", "killed"]);
79555
+
79556
+ class CliTaskNotificationLedger {
79557
+ seen = new Map;
79558
+ record(taskId, status, now) {
79559
+ if (taskId.length === 0 || !NOTIF_TERMINAL_STATUSES.has(status))
79560
+ return;
79561
+ this.seen.set(taskId, now);
79562
+ for (const [id, ts] of this.seen) {
79563
+ if (now - ts > TASK_NOTIFICATION_DEDUP_TTL_MS)
79564
+ this.seen.delete(id);
79565
+ }
79566
+ }
79567
+ seenRecently(taskId, now) {
79568
+ const ts = this.seen.get(taskId);
79569
+ return ts != null && now - ts <= TASK_NOTIFICATION_DEDUP_TTL_MS;
79570
+ }
79571
+ }
79572
+ var cliTaskNotifLedger = new CliTaskNotificationLedger;
79460
79573
  var MAIN_THREAD_KEY = "<main>";
79461
79574
 
79462
79575
  class SubagentHandbackMarker {
@@ -80205,7 +80318,7 @@ async function sendReply(deps, req) {
80205
80318
  }
80206
80319
  const files = args.files ?? [];
80207
80320
  const quoteOptIn = args.quote !== false;
80208
- let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined;
80321
+ let reply_to = parseSourceMessageId(args.reply_to) ?? undefined;
80209
80322
  const protectContent = args.protect_content === true;
80210
80323
  const quoteText = args.quote_text;
80211
80324
  const access = loadAccess();
@@ -82011,20 +82124,6 @@ function formatEventDetail(event) {
82011
82124
  }
82012
82125
  }
82013
82126
 
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
82127
  // gateway/status-surface-log.ts
82029
82128
  function formatTurnLifecycle(action, reason, t, now) {
82030
82129
  const ageMs = action === "clear" ? Math.max(0, now - t.startedAt) : 0;
@@ -84230,6 +84329,26 @@ function isTurnFlushSafetyEnabled(env = process.env) {
84230
84329
  }
84231
84330
 
84232
84331
  // gateway/subagent-handback-marker.ts
84332
+ var TASK_NOTIFICATION_DEDUP_TTL_MS2 = 30000;
84333
+ var NOTIF_TERMINAL_STATUSES2 = new Set(["completed", "failed", "killed"]);
84334
+
84335
+ class CliTaskNotificationLedger2 {
84336
+ seen = new Map;
84337
+ record(taskId, status, now) {
84338
+ if (taskId.length === 0 || !NOTIF_TERMINAL_STATUSES2.has(status))
84339
+ return;
84340
+ this.seen.set(taskId, now);
84341
+ for (const [id, ts] of this.seen) {
84342
+ if (now - ts > TASK_NOTIFICATION_DEDUP_TTL_MS2)
84343
+ this.seen.delete(id);
84344
+ }
84345
+ }
84346
+ seenRecently(taskId, now) {
84347
+ const ts = this.seen.get(taskId);
84348
+ return ts != null && now - ts <= TASK_NOTIFICATION_DEDUP_TTL_MS2;
84349
+ }
84350
+ }
84351
+ var cliTaskNotifLedger2 = new CliTaskNotificationLedger2;
84233
84352
  var MAIN_THREAD_KEY2 = "<main>";
84234
84353
 
84235
84354
  class SubagentHandbackMarker2 {
@@ -97011,6 +97130,9 @@ function decideSubagentHandback(input) {
97011
97130
  if (!input.isBackground) {
97012
97131
  return { deliver: false, reason: "foreground" };
97013
97132
  }
97133
+ if (input.cliTaskNotificationSeen === true) {
97134
+ return { deliver: false, reason: "cli-task-notification" };
97135
+ }
97014
97136
  const chatId = input.fleetChatId || input.ownerChatId;
97015
97137
  if (!chatId) {
97016
97138
  return { deliver: false, reason: "no-chat" };
@@ -102693,10 +102815,10 @@ function startOutboxSweep(deps) {
102693
102815
  }
102694
102816
 
102695
102817
  // ../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;
102818
+ var VERSION2 = "0.20.10";
102819
+ var COMMIT_SHA = "ebb54738";
102820
+ var COMMIT_DATE = "2026-08-05T10:11:19Z";
102821
+ var LATEST_PR = 4398;
102700
102822
  var COMMITS_AHEAD_OF_TAG = 0;
102701
102823
 
102702
102824
  // gateway/boot-version.ts
@@ -107873,6 +107995,7 @@ function emitGatewayOperatorEvent(event) {
107873
107995
  agent,
107874
107996
  shouldEmitCard: (a) => shouldEmitOperatorEvent(a, "rate-limited")
107875
107997
  });
107998
+ routeRateLimit429(rateLimit429Classification, throttleTierRunner, agent);
107876
107999
  if (surface === "litellm-local-notice") {
107877
108000
  process.stderr.write(`telegram gateway: 429 classified litellm-proxy-local agent=${agent} \u2014 ` + `calm path, no account attribution, no failover
107878
108001
  `);
@@ -109150,6 +109273,8 @@ if (isGatewayMain)
109150
109273
  }
109151
109274
  }
109152
109275
  const ev = msg.event;
109276
+ if (ev.kind === "task_notification")
109277
+ cliTaskNotifLedger2.record(ev.taskId, ev.status, Date.now());
109153
109278
  handleSessionEvent2(ev);
109154
109279
  toolFlightTracker.onEvent(ev);
109155
109280
  if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
@@ -109867,7 +109992,7 @@ async function executeSendChecklist(args) {
109867
109992
  if (!Array.isArray(tasks) || tasks.length === 0)
109868
109993
  throw new Error("send_checklist: tasks must be a non-empty array");
109869
109994
  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;
109995
+ const replyTo = parseSourceMessageId2(args.reply_to) ?? undefined;
109871
109996
  const protectContent = args.protect_content === true;
109872
109997
  assertAllowedChat(chat_id);
109873
109998
  const { title: redactedTitle, tasks: redactedTasks } = redactChecklistFields(title, tasks, (t) => redactOutboundText(t, "send_checklist"));
@@ -116715,11 +116840,12 @@ async function startGateway() {
116715
116840
  ownerChatId: hbOwnerDm,
116716
116841
  taskDescription: description2,
116717
116842
  resultText,
116718
- jsonlAgentId: agentId
116843
+ jsonlAgentId: agentId,
116844
+ cliTaskNotificationSeen: cliTaskNotifLedger2.seenRecently(agentId, Date.now())
116719
116845
  });
116720
116846
  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
116847
+ if (decision.reason === "no-chat" || decision.reason === "cli-task-notification") {
116848
+ 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
116849
  `);
116724
116850
  }
116725
116851
  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