switchroom 0.20.7 → 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 (52) hide show
  1. package/dist/agent-scheduler/index.js +111 -14
  2. package/dist/auth-broker/index.js +113 -30
  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 +67 -6
  7. package/dist/cli/switchroom.js +389 -31
  8. package/dist/host-control/main.js +69 -8
  9. package/dist/vault/approvals/kernel-server.js +68 -7
  10. package/dist/vault/broker/server.js +68 -7
  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/bridge/ipc-client.ts +17 -1
  15. package/telegram-plugin/dist/bridge/bridge.js +5 -2
  16. package/telegram-plugin/dist/gateway/gateway.js +410 -178
  17. package/telegram-plugin/dist/server.js +5 -2
  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/boot-reason.ts +61 -0
  21. package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
  22. package/telegram-plugin/gateway/cron-session.ts +66 -0
  23. package/telegram-plugin/gateway/gateway.ts +36 -34
  24. package/telegram-plugin/gateway/narrative-lane.ts +21 -1
  25. package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
  26. package/telegram-plugin/gateway/represent-delivery-guard.ts +33 -2
  27. package/telegram-plugin/gateway/stream-render.ts +11 -2
  28. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
  29. package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
  30. package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
  31. package/telegram-plugin/render/emphasis-guard.ts +92 -12
  32. package/telegram-plugin/render/line-start-guard.ts +27 -2
  33. package/telegram-plugin/sticker-aliases.ts +12 -14
  34. package/telegram-plugin/tests/ask-user.test.ts +15 -0
  35. package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
  36. package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
  37. package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
  38. package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
  39. package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
  40. package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
  41. package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
  42. package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
  43. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
  44. package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
  45. package/telegram-plugin/tests/represent-guard.test.ts +45 -0
  46. package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
  47. package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
  48. package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
  49. package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
  50. package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
  51. package/telegram-plugin/throttle-tier.ts +59 -0
  52. package/telegram-plugin/turn-flush-safety.ts +79 -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
@@ -22593,6 +22625,20 @@ var init_overlay_schema = __esm(() => {
22593
22625
  // ../src/config/overlay-loader.ts
22594
22626
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "node:fs";
22595
22627
  import { basename as basename4, resolve as resolve4 } from "node:path";
22628
+ function recordReadFailure(agentCfg, failure) {
22629
+ const node = agentCfg;
22630
+ const existing = node[OVERLAY_READ_FAILURES];
22631
+ if (Array.isArray(existing)) {
22632
+ existing.push(failure);
22633
+ return;
22634
+ }
22635
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
22636
+ value: [failure],
22637
+ enumerable: false,
22638
+ configurable: true,
22639
+ writable: false
22640
+ });
22641
+ }
22596
22642
  function deriveOverlayTitle(raw, fileName) {
22597
22643
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
22598
22644
  if (titleFromComment)
@@ -22602,17 +22648,39 @@ function deriveOverlayTitle(raw, fileName) {
22602
22648
  return;
22603
22649
  return base.length > 0 ? base : undefined;
22604
22650
  }
22651
+ function readOverlayFile(agentName, file, agentCfg, warnings, source) {
22652
+ try {
22653
+ return readFileSync5(file, "utf-8");
22654
+ } catch (err) {
22655
+ const code = err.code;
22656
+ if (code === "ENOENT")
22657
+ return;
22658
+ const w = {
22659
+ agent: agentName,
22660
+ file,
22661
+ reason: `read error: ${err.message}`,
22662
+ code: code ?? "EUNKNOWN"
22663
+ };
22664
+ recordReadFailure(agentCfg, { file, code: w.code, source });
22665
+ warnings.push(w);
22666
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
22667
+ return;
22668
+ }
22669
+ }
22605
22670
  function overlayDirFor(agentName, subdir) {
22606
22671
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
22607
22672
  return resolve4(base);
22608
22673
  }
22609
- function listYamlFiles(dir) {
22674
+ function listYamlFiles(dir, onUnreadableDir) {
22610
22675
  if (!existsSync5(dir))
22611
22676
  return [];
22612
22677
  let entries;
22613
22678
  try {
22614
22679
  entries = readdirSync3(dir);
22615
- } catch {
22680
+ } catch (err) {
22681
+ const code = err.code;
22682
+ if (code !== "ENOENT")
22683
+ onUnreadableDir?.(code ?? "EUNKNOWN");
22616
22684
  return [];
22617
22685
  }
22618
22686
  const out = [];
@@ -22650,12 +22718,24 @@ function applyAgentOverlays(config) {
22650
22718
  for (const [agentName, agentCfg] of Object.entries(agents)) {
22651
22719
  try {
22652
22720
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
22653
- const files = listYamlFiles(scheduleDir);
22721
+ const files = listYamlFiles(scheduleDir, (code) => {
22722
+ const w = {
22723
+ agent: agentName,
22724
+ file: scheduleDir,
22725
+ reason: `read error: cannot list overlay directory (${code})`,
22726
+ code
22727
+ };
22728
+ recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
22729
+ warnings.push(w);
22730
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
22731
+ });
22654
22732
  if (files.length > 0) {
22655
22733
  const merged = [...agentCfg.schedule ?? []];
22656
22734
  for (const file of files) {
22735
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
22736
+ if (raw === undefined)
22737
+ continue;
22657
22738
  try {
22658
- const raw = readFileSync5(file, "utf-8");
22659
22739
  const parsed = import_yaml.parse(raw);
22660
22740
  const doc = OverlayDocSchema.parse(parsed);
22661
22741
  const title = deriveOverlayTitle(raw, basename4(file));
@@ -22690,13 +22770,25 @@ function applyAgentOverlays(config) {
22690
22770
  }
22691
22771
  try {
22692
22772
  const skillsDir = overlayDirFor(agentName, "skills.d");
22693
- const skillFiles = listYamlFiles(skillsDir);
22773
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
22774
+ const w = {
22775
+ agent: agentName,
22776
+ file: skillsDir,
22777
+ reason: `read error: cannot list overlay directory (${code})`,
22778
+ code
22779
+ };
22780
+ recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
22781
+ warnings.push(w);
22782
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
22783
+ });
22694
22784
  if (skillFiles.length === 0) {} else {
22695
22785
  const merged = [...agentCfg.skills ?? []];
22696
22786
  const seen = new Set(merged);
22697
22787
  for (const file of skillFiles) {
22788
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
22789
+ if (raw === undefined)
22790
+ continue;
22698
22791
  try {
22699
- const raw = readFileSync5(file, "utf-8");
22700
22792
  const parsed = import_yaml.parse(raw);
22701
22793
  const doc = OverlayDocSchema.parse(parsed);
22702
22794
  for (const skillName of doc.skills ?? []) {
@@ -22724,7 +22816,7 @@ function applyAgentOverlays(config) {
22724
22816
  }
22725
22817
  return { config, warnings };
22726
22818
  }
22727
- var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
22819
+ var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
22728
22820
  var init_overlay_loader = __esm(() => {
22729
22821
  init_zod();
22730
22822
  init_overlay_schema();
@@ -22732,6 +22824,7 @@ var init_overlay_loader = __esm(() => {
22732
22824
  import_yaml = __toESM(require_dist(), 1);
22733
22825
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
22734
22826
  OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
22827
+ OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
22735
22828
  });
22736
22829
 
22737
22830
  // ../src/config/merge.ts
@@ -39556,6 +39649,22 @@ function redactAuthCodeMessage(api, chatId, messageId, log) {
39556
39649
 
39557
39650
  // ask-user.ts
39558
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
39559
39668
  var ASK_USER_DEFAULT_TIMEOUT_MS = 300000;
39560
39669
  var ASK_USER_MAX_TIMEOUT_MS = 1800000;
39561
39670
  var ASK_USER_MIN_TIMEOUT_MS = 5000;
@@ -39592,13 +39701,7 @@ function validateAskUserArgs(args) {
39592
39701
  throw new Error("ask_user: message_thread_id must be a positive integer string");
39593
39702
  }
39594
39703
  }
39595
- let replyTo;
39596
- if (args.reply_to != null) {
39597
- replyTo = Number(args.reply_to);
39598
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
39599
- throw new Error("ask_user: reply_to must be a positive integer string");
39600
- }
39601
- }
39704
+ const replyTo = parseSourceMessageId(args.reply_to) ?? undefined;
39602
39705
  let timeoutMs = args.timeout_ms ?? ASK_USER_DEFAULT_TIMEOUT_MS;
39603
39706
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
39604
39707
  throw new Error("ask_user: timeout_ms must be a number");
@@ -39678,6 +39781,7 @@ function applyChecklistPatch(cl, patch) {
39678
39781
  return { title: patch.title ?? cl.title, tasks };
39679
39782
  }
39680
39783
  function buildNativeChecklistPayload(p) {
39784
+ const replyAnchor = parseSourceMessageId(p.replyToMessageId);
39681
39785
  return {
39682
39786
  business_connection_id: p.businessConnectionId,
39683
39787
  chat_id: p.chatId,
@@ -39685,7 +39789,7 @@ function buildNativeChecklistPayload(p) {
39685
39789
  title: p.title,
39686
39790
  tasks: p.tasks.map((t) => ({ id: t.id, text: t.text }))
39687
39791
  },
39688
- ...p.replyToMessageId != null ? { reply_parameters: { message_id: p.replyToMessageId } } : {},
39792
+ ...replyAnchor != null ? { reply_parameters: { message_id: replyAnchor } } : {},
39689
39793
  ...p.protectContent === true ? { protect_content: true } : {}
39690
39794
  };
39691
39795
  }
@@ -39969,13 +40073,7 @@ function resolveStickerSendArgs(raw, aliasMap) {
39969
40073
  throw new Error("send_sticker: message_thread_id must be a positive integer string");
39970
40074
  }
39971
40075
  }
39972
- let replyTo;
39973
- if (raw.reply_to != null) {
39974
- replyTo = Number(raw.reply_to);
39975
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
39976
- throw new Error("send_sticker: reply_to must be a positive integer string");
39977
- }
39978
- }
40076
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
39979
40077
  return {
39980
40078
  chatId: raw.chat_id,
39981
40079
  fileId,
@@ -40027,13 +40125,7 @@ function resolveGifSendArgs(raw) {
40027
40125
  throw new Error("send_gif: message_thread_id must be a positive integer string");
40028
40126
  }
40029
40127
  }
40030
- let replyTo;
40031
- if (raw.reply_to != null) {
40032
- replyTo = Number(raw.reply_to);
40033
- if (!Number.isFinite(replyTo) || replyTo <= 0) {
40034
- throw new Error("send_gif: reply_to must be a positive integer string");
40035
- }
40036
- }
40128
+ const replyTo = parseSourceMessageId(raw.reply_to) ?? undefined;
40037
40129
  return {
40038
40130
  chatId: raw.chat_id,
40039
40131
  animationRef,
@@ -50147,6 +50239,20 @@ class PinRightsCache2 {
50147
50239
  }
50148
50240
  }
50149
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
+
50150
50256
  // gateway/permission-timeout.ts
50151
50257
  var SIGNATURE_SEP = String.fromCharCode(0);
50152
50258
  function permissionSignature(toolName, inputPreview) {
@@ -51160,7 +51266,7 @@ function createAuthBrokerClient() {
51160
51266
  listState: () => broker.listState(),
51161
51267
  setActive: (label) => broker.setActive(label),
51162
51268
  markExhausted: (until) => broker.markExhausted(until),
51163
- markThrottled: (until) => broker.markThrottled(until),
51269
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
51164
51270
  rmAccount: (label) => broker.rmAccount(label),
51165
51271
  refreshAccount: (label) => broker.refreshAccount(label),
51166
51272
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -73918,7 +74024,7 @@ function createAuthBrokerClient2() {
73918
74024
  listState: () => broker.listState(),
73919
74025
  setActive: (label) => broker.setActive(label),
73920
74026
  markExhausted: (until) => broker.markExhausted(until),
73921
- markThrottled: (until) => broker.markThrottled(until),
74027
+ markThrottled: (until, probeOnly) => broker.markThrottled(until, probeOnly),
73922
74028
  rmAccount: (label) => broker.rmAccount(label),
73923
74029
  refreshAccount: (label) => broker.refreshAccount(label),
73924
74030
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -74337,12 +74443,13 @@ class AuthBrokerClient2 {
74337
74443
  const data = await this.send(req);
74338
74444
  return data;
74339
74445
  }
74340
- async markThrottled(until) {
74446
+ async markThrottled(until, probeOnly = false) {
74341
74447
  const data = await this.send({
74342
74448
  v: PROTOCOL_VERSION,
74343
74449
  id: randomUUID5(),
74344
74450
  op: "mark-throttled",
74345
- until
74451
+ until,
74452
+ ...probeOnly ? { probeOnly: true } : {}
74346
74453
  });
74347
74454
  return data;
74348
74455
  }
@@ -77042,6 +77149,15 @@ function classify429Detail2(text4) {
77042
77149
  return "litellm-local";
77043
77150
  return "generic-transient";
77044
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
+ }
77045
77161
  function build429ClassifiedMetric(opts) {
77046
77162
  const detail = typeof opts.detail === "string" ? opts.detail : "";
77047
77163
  const litellm = parseLitellmLimitDetail(detail, new Date(opts.now));
@@ -77138,6 +77254,12 @@ function createThrottleTierRunner(deps) {
77138
77254
  deps.log(`[throttle-tier] resume suppressed (${verdict}) reason=${reason}`);
77139
77255
  }
77140
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
+ }
77141
77263
  async function fire(triggerAgent, throttledUntilMs, resetParsed) {
77142
77264
  const armedAtMs = now();
77143
77265
  let client3 = null;
@@ -77158,10 +77280,7 @@ function createThrottleTierRunner(deps) {
77158
77280
  deps.log(`[throttle-tier] markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
77159
77281
  }
77160
77282
  if (escalated) {
77161
- deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
77162
- await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
77163
- if (rolledTo)
77164
- nudgeResume("throttle-escalation-resume", armedAtMs);
77283
+ await announceEscalation(client3, account, rolledTo, triggerAgent, armedAtMs);
77165
77284
  return;
77166
77285
  }
77167
77286
  const cooldownKey = account ?? `agent:${triggerAgent}`;
@@ -77186,8 +77305,34 @@ function createThrottleTierRunner(deps) {
77186
77305
  nudgeResume("throttle-retry-resume", armedAtMs);
77187
77306
  }, delayMs);
77188
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
+ }
77189
77333
  return {
77190
77334
  fire,
77335
+ fireProbeOnly,
77191
77336
  inspect: () => ({ noticeState, nudgePending: pendingNudge != null })
77192
77337
  };
77193
77338
  }
@@ -79396,6 +79541,26 @@ function stampsHandbackMarker(source) {
79396
79541
  return known.decoupledCompletion;
79397
79542
  }
79398
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;
79399
79564
  var MAIN_THREAD_KEY = "<main>";
79400
79565
 
79401
79566
  class SubagentHandbackMarker {
@@ -80144,7 +80309,7 @@ async function sendReply(deps, req) {
80144
80309
  }
80145
80310
  const files = args.files ?? [];
80146
80311
  const quoteOptIn = args.quote !== false;
80147
- let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined;
80312
+ let reply_to = parseSourceMessageId(args.reply_to) ?? undefined;
80148
80313
  const protectContent = args.protect_content === true;
80149
80314
  const quoteText = args.quote_text;
80150
80315
  const access = loadAccess();
@@ -80949,6 +81114,22 @@ function endsWithSilentMarker(text4) {
80949
81114
  return false;
80950
81115
  return isSilentFlushMarker(lines[lines.length - 1]);
80951
81116
  }
81117
+ function isSilentSentinelCardOutcome(input) {
81118
+ if (input.finalAnswerEverDelivered)
81119
+ return false;
81120
+ if (isSilentFlushMarker(input.lastReplyText) || isCompositeSilentNoise(input.lastReplyText)) {
81121
+ return true;
81122
+ }
81123
+ if (!input.replyCalled) {
81124
+ const joined = input.capturedText.join(`
81125
+
81126
+ `).trim();
81127
+ if (joined.length > 0 && (isSilentFlushMarker(joined) || isCompositeSilentNoise(joined) || endsWithSilentMarker(joined))) {
81128
+ return true;
81129
+ }
81130
+ }
81131
+ return false;
81132
+ }
80952
81133
  function decideTurnFlush(input) {
80953
81134
  const flushEnabled = input.flushEnabled !== false;
80954
81135
  if (!flushEnabled)
@@ -81934,20 +82115,6 @@ function formatEventDetail(event) {
81934
82115
  }
81935
82116
  }
81936
82117
 
81937
- // gateway/source-message-id.ts
81938
- var MAX_TELEGRAM_MESSAGE_ID = 2 ** 31;
81939
- function parseSourceMessageId(raw) {
81940
- if (raw == null)
81941
- return null;
81942
- const s = String(raw);
81943
- if (!/^\d+$/.test(s))
81944
- return null;
81945
- const n = Number(s);
81946
- if (!Number.isSafeInteger(n) || n <= 0 || n >= MAX_TELEGRAM_MESSAGE_ID)
81947
- return null;
81948
- return n;
81949
- }
81950
-
81951
82118
  // gateway/status-surface-log.ts
81952
82119
  function formatTurnLifecycle(action, reason, t, now) {
81953
82120
  const ageMs = action === "clear" ? Math.max(0, now - t.startedAt) : 0;
@@ -82614,8 +82781,7 @@ function handleSessionEvent(deps, ev) {
82614
82781
  }
82615
82782
  if (QUEUED_CARD_ENABLED && !handbackOwnsSurface) {
82616
82783
  const cardChatId = ev.chatId;
82617
- const replyToRaw = ev.messageId != null ? Number(ev.messageId) : null;
82618
- const replyTo = replyToRaw != null && Number.isFinite(replyToRaw) ? replyToRaw : null;
82784
+ const replyTo = parseSourceMessageId(ev.messageId);
82619
82785
  openQueuedCard(deps, cardChatId, enqThreadIdNum ?? null, replyTo).then((cardId) => {
82620
82786
  if (cardId == null)
82621
82787
  return;
@@ -83899,7 +84065,13 @@ function createNarrativeLane(deps) {
83899
84065
  clearActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, statusKey(chat, thread), id);
83900
84066
  }
83901
84067
  await reconcileStatusPin(`fg:${statusKey(chat, thread)}`, chat, { pinned: false });
83902
- if (CLEAR_STATUS_ON_COMPLETION) {
84068
+ const silentSentinelTurn = isSilentSentinelCardOutcome({
84069
+ replyCalled: turn.replyCalled,
84070
+ lastReplyText: turn.lastReplyText,
84071
+ capturedText: turn.capturedText,
84072
+ finalAnswerEverDelivered: turn.finalAnswerEverDelivered
84073
+ });
84074
+ if (CLEAR_STATUS_ON_COMPLETION || silentSentinelTurn) {
83903
84075
  try {
83904
84076
  await robustApiCall(() => bot.api.deleteMessage(chat, id), { chat_id: chat, ...thread != null ? { threadId: thread } : {}, verb: "activity-summary.delete" });
83905
84077
  } catch (err) {
@@ -84148,6 +84320,26 @@ function isTurnFlushSafetyEnabled(env = process.env) {
84148
84320
  }
84149
84321
 
84150
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;
84151
84343
  var MAIN_THREAD_KEY2 = "<main>";
84152
84344
 
84153
84345
  class SubagentHandbackMarker2 {
@@ -92982,19 +93174,121 @@ function makeRepresentRedeliveryGuard(deps) {
92982
93174
  return true;
92983
93175
  };
92984
93176
  }
92985
- function makeSessionBusyDrainDeferral(boundMs) {
93177
+ var DEFAULT_DRAIN_DEFER_STALE_GAP_MS = 15000;
93178
+ function makeSessionBusyDrainDeferral(boundMs, staleGapMs = DEFAULT_DRAIN_DEFER_STALE_GAP_MS) {
92986
93179
  let deferringSince = null;
93180
+ let lastCallAt = null;
92987
93181
  return (busy, now) => {
93182
+ const gapSinceLastCall = lastCallAt == null ? null : now - lastCallAt;
93183
+ lastCallAt = now;
92988
93184
  if (!busy || boundMs <= 0) {
92989
93185
  deferringSince = null;
92990
93186
  return false;
92991
93187
  }
92992
- if (deferringSince == null)
93188
+ if (deferringSince == null || staleGapMs > 0 && gapSinceLastCall != null && gapSinceLastCall > staleGapMs) {
92993
93189
  deferringSince = now;
93190
+ }
92994
93191
  return now - deferringSince < boundMs;
92995
93192
  };
92996
93193
  }
92997
93194
 
93195
+ // gateway/pending-inbound-buffer.ts
93196
+ function redeliverBufferedInbound2(buffer, agent, send, spool, onDelivered) {
93197
+ const pending = buffer.drain(agent);
93198
+ let redelivered = 0;
93199
+ let rebuffered = 0;
93200
+ let retracted = 0;
93201
+ for (const { merged, originals } of planBufferedRedelivery2(pending)) {
93202
+ let proceed = true;
93203
+ if (buffer.beforeRedeliver != null) {
93204
+ try {
93205
+ proceed = buffer.beforeRedeliver(merged);
93206
+ } catch (e) {
93207
+ proceed = true;
93208
+ process.stderr.write(`redeliver beforeRedeliver threw \u2014 failing open (deliver): ${String(e)}
93209
+ `);
93210
+ }
93211
+ }
93212
+ if (!proceed) {
93213
+ for (const o of originals)
93214
+ spool?.ack(o);
93215
+ retracted += originals.length;
93216
+ continue;
93217
+ }
93218
+ let delivered = false;
93219
+ try {
93220
+ delivered = send(merged);
93221
+ } catch {
93222
+ delivered = false;
93223
+ }
93224
+ if (delivered) {
93225
+ for (const o of originals)
93226
+ spool?.ack(o);
93227
+ redelivered += originals.length;
93228
+ onDelivered?.(merged, originals);
93229
+ } else {
93230
+ for (const o of originals)
93231
+ buffer.push(agent, o);
93232
+ rebuffered += originals.length;
93233
+ }
93234
+ }
93235
+ return { drained: pending.length, redelivered, rebuffered, retracted };
93236
+ }
93237
+ function isMergeableUserInbound2(msg) {
93238
+ return msg.type === "inbound" && (msg.meta == null || msg.meta.source == null && msg.meta.button_callback == null);
93239
+ }
93240
+ function inboundHasMedia2(msg) {
93241
+ return msg.imagePath != null || msg.attachment != null;
93242
+ }
93243
+ function planBufferedRedelivery2(pending) {
93244
+ const out = [];
93245
+ let run3 = [];
93246
+ let runHasMedia = false;
93247
+ const sameTarget = (a, b) => a.chatId === b.chatId && (a.threadId ?? null) === (b.threadId ?? null) && a.userId === b.userId;
93248
+ const flush = () => {
93249
+ if (run3.length === 0)
93250
+ return;
93251
+ out.push({ merged: run3.length === 1 ? run3[0] : mergeRun2(run3), originals: run3 });
93252
+ run3 = [];
93253
+ runHasMedia = false;
93254
+ };
93255
+ for (const msg of pending) {
93256
+ const msgHasMedia = inboundHasMedia2(msg);
93257
+ const canJoin = run3.length > 0 && isMergeableUserInbound2(msg) && isMergeableUserInbound2(run3[run3.length - 1]) && sameTarget(run3[run3.length - 1], msg) && !(runHasMedia && msgHasMedia);
93258
+ if (!canJoin)
93259
+ flush();
93260
+ run3.push(msg);
93261
+ runHasMedia = runHasMedia || msgHasMedia;
93262
+ }
93263
+ flush();
93264
+ return out;
93265
+ }
93266
+ var ATTACHMENT_META_RE2 = /^(image_path|attachment_)/;
93267
+ function mergeRun2(run3) {
93268
+ const last = run3[run3.length - 1];
93269
+ const mediaEntry = run3.find(inboundHasMedia2);
93270
+ const merged = {
93271
+ ...last,
93272
+ text: run3.map((m) => m.text).join(`
93273
+ `)
93274
+ };
93275
+ delete merged.imagePath;
93276
+ delete merged.attachment;
93277
+ if (mediaEntry != null && mediaEntry !== last) {
93278
+ const splicedMeta = { ...merged.meta };
93279
+ for (const [k, v] of Object.entries(mediaEntry.meta)) {
93280
+ if (ATTACHMENT_META_RE2.test(k))
93281
+ splicedMeta[k] = v;
93282
+ }
93283
+ merged.meta = splicedMeta;
93284
+ }
93285
+ if (mediaEntry?.imagePath != null)
93286
+ merged.imagePath = mediaEntry.imagePath;
93287
+ if (mediaEntry?.attachment != null)
93288
+ merged.attachment = mediaEntry.attachment;
93289
+ return merged;
93290
+ }
93291
+
92998
93292
  // gateway/cron-session.ts
92999
93293
  var CRON_IDENTITY_SUFFIX = "-cron";
93000
93294
  function cronIdentity(agent) {
@@ -93024,6 +93318,23 @@ function deliverInjectWithFallback(agentName3, meta, send) {
93024
93318
  }
93025
93319
  return { target, delivered: false, fellBackToMain: false };
93026
93320
  }
93321
+ function drainCronBridgeOnRegister(client3, buffer, spool, log) {
93322
+ client3.send({ type: "status", status: "agent_connected" });
93323
+ const send = (msg) => {
93324
+ try {
93325
+ client3.send(msg);
93326
+ return true;
93327
+ } catch {
93328
+ return false;
93329
+ }
93330
+ };
93331
+ const result = redeliverBufferedInbound2(buffer, client3.agentName ?? "", send, spool);
93332
+ if (result.drained > 0 && log != null) {
93333
+ log(`telegram gateway: cron-bridge drain agent=${client3.agentName} ` + `drained=${result.drained} redelivered=${result.redelivered} ` + `rebuffered=${result.rebuffered}
93334
+ `);
93335
+ }
93336
+ return result;
93337
+ }
93027
93338
 
93028
93339
  // gateway/obligation-ledger.ts
93029
93340
  class ObligationLedger {
@@ -94586,103 +94897,6 @@ function formatEventDetail2(event) {
94586
94897
  }
94587
94898
  }
94588
94899
 
94589
- // gateway/pending-inbound-buffer.ts
94590
- function redeliverBufferedInbound2(buffer, agent, send, spool, onDelivered) {
94591
- const pending = buffer.drain(agent);
94592
- let redelivered = 0;
94593
- let rebuffered = 0;
94594
- let retracted = 0;
94595
- for (const { merged, originals } of planBufferedRedelivery2(pending)) {
94596
- let proceed = true;
94597
- if (buffer.beforeRedeliver != null) {
94598
- try {
94599
- proceed = buffer.beforeRedeliver(merged);
94600
- } catch (e) {
94601
- proceed = true;
94602
- process.stderr.write(`redeliver beforeRedeliver threw \u2014 failing open (deliver): ${String(e)}
94603
- `);
94604
- }
94605
- }
94606
- if (!proceed) {
94607
- for (const o of originals)
94608
- spool?.ack(o);
94609
- retracted += originals.length;
94610
- continue;
94611
- }
94612
- let delivered = false;
94613
- try {
94614
- delivered = send(merged);
94615
- } catch {
94616
- delivered = false;
94617
- }
94618
- if (delivered) {
94619
- for (const o of originals)
94620
- spool?.ack(o);
94621
- redelivered += originals.length;
94622
- onDelivered?.(merged, originals);
94623
- } else {
94624
- for (const o of originals)
94625
- buffer.push(agent, o);
94626
- rebuffered += originals.length;
94627
- }
94628
- }
94629
- return { drained: pending.length, redelivered, rebuffered, retracted };
94630
- }
94631
- function isMergeableUserInbound2(msg) {
94632
- return msg.type === "inbound" && (msg.meta == null || msg.meta.source == null && msg.meta.button_callback == null);
94633
- }
94634
- function inboundHasMedia2(msg) {
94635
- return msg.imagePath != null || msg.attachment != null;
94636
- }
94637
- function planBufferedRedelivery2(pending) {
94638
- const out = [];
94639
- let run3 = [];
94640
- let runHasMedia = false;
94641
- const sameTarget = (a, b) => a.chatId === b.chatId && (a.threadId ?? null) === (b.threadId ?? null) && a.userId === b.userId;
94642
- const flush = () => {
94643
- if (run3.length === 0)
94644
- return;
94645
- out.push({ merged: run3.length === 1 ? run3[0] : mergeRun2(run3), originals: run3 });
94646
- run3 = [];
94647
- runHasMedia = false;
94648
- };
94649
- for (const msg of pending) {
94650
- const msgHasMedia = inboundHasMedia2(msg);
94651
- const canJoin = run3.length > 0 && isMergeableUserInbound2(msg) && isMergeableUserInbound2(run3[run3.length - 1]) && sameTarget(run3[run3.length - 1], msg) && !(runHasMedia && msgHasMedia);
94652
- if (!canJoin)
94653
- flush();
94654
- run3.push(msg);
94655
- runHasMedia = runHasMedia || msgHasMedia;
94656
- }
94657
- flush();
94658
- return out;
94659
- }
94660
- var ATTACHMENT_META_RE2 = /^(image_path|attachment_)/;
94661
- function mergeRun2(run3) {
94662
- const last = run3[run3.length - 1];
94663
- const mediaEntry = run3.find(inboundHasMedia2);
94664
- const merged = {
94665
- ...last,
94666
- text: run3.map((m) => m.text).join(`
94667
- `)
94668
- };
94669
- delete merged.imagePath;
94670
- delete merged.attachment;
94671
- if (mediaEntry != null && mediaEntry !== last) {
94672
- const splicedMeta = { ...merged.meta };
94673
- for (const [k, v] of Object.entries(mediaEntry.meta)) {
94674
- if (ATTACHMENT_META_RE2.test(k))
94675
- splicedMeta[k] = v;
94676
- }
94677
- merged.meta = splicedMeta;
94678
- }
94679
- if (mediaEntry?.imagePath != null)
94680
- merged.imagePath = mediaEntry.imagePath;
94681
- if (mediaEntry?.attachment != null)
94682
- merged.attachment = mediaEntry.attachment;
94683
- return merged;
94684
- }
94685
-
94686
94900
  // gateway/obligation-turn-end.ts
94687
94901
  function decideObligationTurnEnd(finalAnswerDelivered, replyCalled) {
94688
94902
  return finalAnswerDelivered || replyCalled ? "close" : "note-ended";
@@ -96907,6 +97121,9 @@ function decideSubagentHandback(input) {
96907
97121
  if (!input.isBackground) {
96908
97122
  return { deliver: false, reason: "foreground" };
96909
97123
  }
97124
+ if (input.cliTaskNotificationSeen === true) {
97125
+ return { deliver: false, reason: "cli-task-notification" };
97126
+ }
96910
97127
  const chatId = input.fleetChatId || input.ownerChatId;
96911
97128
  if (!chatId) {
96912
97129
  return { deliver: false, reason: "no-chat" };
@@ -99732,6 +99949,14 @@ function determineRestartReason(opts) {
99732
99949
  return "crash";
99733
99950
  return "fresh";
99734
99951
  }
99952
+ var BOOT_REASON_REUSE_WINDOW_MS = 5 * 60000;
99953
+ function determineBridgeReconnectReason(opts) {
99954
+ const { gatewayStartedAtMs, bootReason, bootReasonReuseWindowMs = BOOT_REASON_REUSE_WINDOW_MS } = opts;
99955
+ if (opts.marker == null && opts.cleanMarker == null && bootReason != null && opts.now - gatewayStartedAtMs < bootReasonReuseWindowMs) {
99956
+ return bootReason;
99957
+ }
99958
+ return determineRestartReason(opts);
99959
+ }
99735
99960
 
99736
99961
  // gateway/update-announce.ts
99737
99962
  import { existsSync as existsSync50, mkdirSync as mkdirSync44, openSync as openSync12, closeSync as closeSync12, readFileSync as readFileSync53 } from "node:fs";
@@ -102581,10 +102806,10 @@ function startOutboxSweep(deps) {
102581
102806
  }
102582
102807
 
102583
102808
  // ../src/build-info.ts
102584
- var VERSION2 = "0.20.7";
102585
- var COMMIT_SHA = "bc107185";
102586
- var COMMIT_DATE = "2026-08-04T10:20:28Z";
102587
- var LATEST_PR = 4343;
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;
102588
102813
  var COMMITS_AHEAD_OF_TAG = 0;
102589
102814
 
102590
102815
  // gateway/boot-version.ts
@@ -107761,6 +107986,7 @@ function emitGatewayOperatorEvent(event) {
107761
107986
  agent,
107762
107987
  shouldEmitCard: (a) => shouldEmitOperatorEvent(a, "rate-limited")
107763
107988
  });
107989
+ routeRateLimit429(rateLimit429Classification, throttleTierRunner, agent);
107764
107990
  if (surface === "litellm-local-notice") {
107765
107991
  process.stderr.write(`telegram gateway: 429 classified litellm-proxy-local agent=${agent} \u2014 ` + `calm path, no account attribution, no failover
107766
107992
  `);
@@ -108342,6 +108568,7 @@ var GATEWAY_STARTED_AT_MS = Date.now();
108342
108568
  var BOOT_CARD_ENABLED = process.env.SWITCHROOM_BOOT_CARD !== "false";
108343
108569
  var activeBootCard = null;
108344
108570
  var bootCardPending = false;
108571
+ var bootReasonAtStartup = null;
108345
108572
  var ISSUES_CARD_ENABLED = process.env.SWITCHROOM_ISSUES_CARD !== "false";
108346
108573
  var activeIssuesCard = null;
108347
108574
  var activeIssuesWatcher = null;
@@ -108835,13 +109062,7 @@ if (isGatewayMain)
108835
109062
  process.stderr.write(`telegram gateway: bridge registered \u2014 agent=${client3.agentName}
108836
109063
  `);
108837
109064
  if (isCronIdentity(client3.agentName)) {
108838
- client3.send({ type: "status", status: "agent_connected" });
108839
- const pending2 = pendingInboundBuffer.drain(client3.agentName ?? "");
108840
- for (const m of pending2) {
108841
- try {
108842
- client3.send(m);
108843
- } catch {}
108844
- }
109065
+ drainCronBridgeOnRegister(client3, pendingInboundBuffer, inboundSpool ?? undefined, (l) => process.stderr.write(l));
108845
109066
  return;
108846
109067
  }
108847
109068
  const bridgeUpEffects = client3.agentName != null ? shadowEmit2({ kind: "bridgeUp", at: Date.now() }) : [];
@@ -108888,7 +109109,14 @@ if (isGatewayMain)
108888
109109
  `);
108889
109110
  clearRestartMarker();
108890
109111
  }
108891
- const reason = determineRestartReason({ marker, cleanMarker, sessionMarker: storedSession, now: nowMs3 });
109112
+ const reason = determineBridgeReconnectReason({
109113
+ marker,
109114
+ cleanMarker,
109115
+ sessionMarker: storedSession,
109116
+ now: nowMs3,
109117
+ gatewayStartedAtMs: GATEWAY_STARTED_AT_MS,
109118
+ bootReason: bootReasonAtStartup
109119
+ });
108892
109120
  const target = resolveBootChatId(marker, markerAgeMs);
108893
109121
  if (target) {
108894
109122
  const { chatId, threadId, ackMsgId } = target;
@@ -109036,6 +109264,8 @@ if (isGatewayMain)
109036
109264
  }
109037
109265
  }
109038
109266
  const ev = msg.event;
109267
+ if (ev.kind === "task_notification")
109268
+ cliTaskNotifLedger2.record(ev.taskId, ev.status, Date.now());
109039
109269
  handleSessionEvent2(ev);
109040
109270
  toolFlightTracker.onEvent(ev);
109041
109271
  if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
@@ -109635,7 +109865,7 @@ if (isGatewayMain)
109635
109865
  }
109636
109866
  })();
109637
109867
  var IDLE_DRAIN_INTERVAL_MS = 5000;
109638
- var idleDrainBusyDefer = makeSessionBusyDrainDeferral(OBLIGATION_BACKGROUND_WORK_GRACE_MS);
109868
+ var idleDrainBusyDefer = makeSessionBusyDrainDeferral(OBLIGATION_BACKGROUND_WORK_GRACE_MS, IDLE_DRAIN_INTERVAL_MS * 3);
109639
109869
  if (isGatewayMain && !STATIC) {
109640
109870
  setInterval(() => {
109641
109871
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
@@ -109753,7 +109983,7 @@ async function executeSendChecklist(args) {
109753
109983
  if (!Array.isArray(tasks) || tasks.length === 0)
109754
109984
  throw new Error("send_checklist: tasks must be a non-empty array");
109755
109985
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
109756
- const replyTo = args.reply_to != null ? Number(args.reply_to) : undefined;
109986
+ const replyTo = parseSourceMessageId2(args.reply_to) ?? undefined;
109757
109987
  const protectContent = args.protect_content === true;
109758
109988
  assertAllowedChat(chat_id);
109759
109989
  const { title: redactedTitle, tasks: redactedTasks } = redactChecklistFields(title, tasks, (t) => redactOutboundText(t, "send_checklist"));
@@ -116154,6 +116384,7 @@ async function startGateway() {
116154
116384
  } else {
116155
116385
  const markerAgeMs = marker ? nowMs3 - marker.ts : undefined;
116156
116386
  const reason = determineRestartReason({ marker, cleanMarker, sessionMarker: storedSession, now: nowMs3 });
116387
+ bootReasonAtStartup = reason;
116157
116388
  const target = resolveBootChatId(marker, markerAgeMs);
116158
116389
  if (reason === "crash") {
116159
116390
  const cleanMarkerStale = cleanMarker ? !shouldSuppressRecoveryBanner(cleanMarker, nowMs3, DEFAULT_MAX_AGE_MS) : false;
@@ -116600,11 +116831,12 @@ async function startGateway() {
116600
116831
  ownerChatId: hbOwnerDm,
116601
116832
  taskDescription: description2,
116602
116833
  resultText,
116603
- jsonlAgentId: agentId
116834
+ jsonlAgentId: agentId,
116835
+ cliTaskNotificationSeen: cliTaskNotifLedger2.seenRecently(agentId, Date.now())
116604
116836
  });
116605
116837
  if (!decision.deliver) {
116606
- if (decision.reason === "no-chat") {
116607
- 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)"}
116608
116840
  `);
116609
116841
  }
116610
116842
  return;