switchroom 0.18.13 → 0.18.15

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 (47) hide show
  1. package/dist/agent-scheduler/index.js +49 -9
  2. package/dist/auth-broker/index.js +152 -46
  3. package/dist/cli/autoaccept-poll.js +23 -0
  4. package/dist/cli/drive-write-pretool.mjs +24 -1
  5. package/dist/cli/foreground-hog-pretool.mjs +264 -0
  6. package/dist/cli/notion-write-pretool.mjs +0 -1
  7. package/dist/cli/switchroom.js +1185 -1072
  8. package/dist/host-control/main.js +53 -52
  9. package/dist/vault/approvals/kernel-server.js +16 -13
  10. package/dist/vault/broker/server.js +672 -669
  11. package/package.json +1 -1
  12. package/profiles/coding/CLAUDE.md.hbs +2 -0
  13. package/profiles/default/CLAUDE.md.hbs +2 -0
  14. package/skills/switchroom-architecture/telegram.md +0 -1
  15. package/telegram-plugin/auth-snapshot-format.ts +37 -5
  16. package/telegram-plugin/auto-fallback-fleet.ts +29 -1
  17. package/telegram-plugin/bridge/bridge.ts +2 -0
  18. package/telegram-plugin/dist/bridge/bridge.js +23 -0
  19. package/telegram-plugin/dist/gateway/gateway.js +765 -67
  20. package/telegram-plugin/dist/server.js +24 -1
  21. package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
  22. package/telegram-plugin/gateway/auth-command.ts +14 -0
  23. package/telegram-plugin/gateway/forward-origin.ts +235 -0
  24. package/telegram-plugin/gateway/gateway.ts +270 -10
  25. package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
  26. package/telegram-plugin/history.ts +55 -6
  27. package/telegram-plugin/model-unavailable.ts +234 -2
  28. package/telegram-plugin/render/rich-render.ts +40 -32
  29. package/telegram-plugin/runtime-metrics.ts +31 -0
  30. package/telegram-plugin/session-tail.ts +14 -2
  31. package/telegram-plugin/stream-controller.ts +3 -2
  32. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
  33. package/telegram-plugin/tests/forward-origin.test.ts +309 -0
  34. package/telegram-plugin/tests/history.test.ts +157 -0
  35. package/telegram-plugin/tests/model-unavailable.test.ts +187 -0
  36. package/telegram-plugin/tests/operator-events-session-tail.test.ts +55 -0
  37. package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
  38. package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
  39. package/telegram-plugin/tests/runtime-metrics.test.ts +24 -0
  40. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
  41. package/telegram-plugin/tests/status-accent.test.ts +5 -3
  42. package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
  43. package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
  44. package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
  45. package/telegram-plugin/tests/throttle-tier.test.ts +454 -0
  46. package/telegram-plugin/throttle-tier.ts +323 -0
  47. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
@@ -19458,7 +19458,7 @@ function renderFallbackAnnouncement(input) {
19458
19458
  const tz = input.tz ?? "UTC";
19459
19459
  const lines = [];
19460
19460
  const limitWord = input.oldQuota ? limitWordFor(input.oldQuota) : "quota";
19461
- const headerLimit = limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
19461
+ const headerLimit = input.cause === "rate-limit" ? "rate limit" : limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
19462
19462
  if (!input.newLabel) {
19463
19463
  lines.push(`\uD83D\uDD34 **All accounts blocked \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
19464
19464
  lines.push("");
@@ -19479,8 +19479,8 @@ function renderFallbackAnnouncement(input) {
19479
19479
  lines.push("");
19480
19480
  lines.push(`Earliest recovery: \`${codeSpanSafe(earliest.label)}\` ` + `${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`);
19481
19481
  }
19482
- } else if (input.oldQuota) {
19483
- const recovery = recoveryAtFor(input.oldQuota);
19482
+ } else {
19483
+ const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
19484
19484
  if (recovery) {
19485
19485
  lines.push(`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute(recovery, tz)} ` + `(in ${formatRelative(recovery, now)})`);
19486
19486
  }
@@ -19495,8 +19495,8 @@ function renderFallbackAnnouncement(input) {
19495
19495
  lines.push(`\`${codeSpanSafe(input.oldLabel)}\` \u2192 \`${codeSpanSafe(input.newLabel)}\``);
19496
19496
  lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
19497
19497
  lines.push("");
19498
- if (input.oldQuota) {
19499
- const recovery = recoveryAtFor(input.oldQuota);
19498
+ {
19499
+ const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
19500
19500
  if (recovery) {
19501
19501
  lines.push(`\`${codeSpanSafe(input.oldLabel)}\` recovers ` + `${formatAbsolute(recovery, tz)} (in ${formatRelative(recovery, now)})`);
19502
19502
  }
@@ -19661,7 +19661,7 @@ function decodeResponse2(line) {
19661
19661
  }
19662
19662
  return ResponseSchema2.parse(parsed);
19663
19663
  }
19664
- var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
19664
+ var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, MarkThrottledRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, MarkThrottledDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
19665
19665
  var init_protocol2 = __esm(() => {
19666
19666
  init_zod();
19667
19667
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -19690,6 +19690,12 @@ var init_protocol2 = __esm(() => {
19690
19690
  id: exports_external.string().min(1),
19691
19691
  until: exports_external.number().int().positive().optional()
19692
19692
  });
19693
+ MarkThrottledRequestSchema = exports_external.object({
19694
+ v: exports_external.literal(PROTOCOL_VERSION),
19695
+ op: exports_external.literal("mark-throttled"),
19696
+ id: exports_external.string().min(1),
19697
+ until: exports_external.number().int().positive()
19698
+ });
19693
19699
  RefreshAccountRequestSchema = exports_external.object({
19694
19700
  v: exports_external.literal(PROTOCOL_VERSION),
19695
19701
  op: exports_external.literal("refresh-account"),
@@ -19790,6 +19796,7 @@ var init_protocol2 = __esm(() => {
19790
19796
  ListStateRequestSchema,
19791
19797
  SetActiveRequestSchema,
19792
19798
  MarkExhaustedRequestSchema,
19799
+ MarkThrottledRequestSchema,
19793
19800
  RefreshAccountRequestSchema,
19794
19801
  AddAccountRequestSchema,
19795
19802
  RmAccountRequestSchema,
@@ -19809,6 +19816,7 @@ var init_protocol2 = __esm(() => {
19809
19816
  expiresAt: exports_external.number().optional(),
19810
19817
  exhausted: exports_external.boolean(),
19811
19818
  exhausted_until: exports_external.number().optional(),
19819
+ throttled_until: exports_external.number().optional(),
19812
19820
  threshold_violations: exports_external.number().int().nonnegative().optional(),
19813
19821
  last_refreshed_at: exports_external.number().optional()
19814
19822
  });
@@ -19839,6 +19847,12 @@ var init_protocol2 = __esm(() => {
19839
19847
  rolled: exports_external.array(exports_external.string()),
19840
19848
  rolledTo: exports_external.string().nullable().optional()
19841
19849
  });
19850
+ MarkThrottledDataSchema = exports_external.object({
19851
+ account: exports_external.string(),
19852
+ throttled_until: exports_external.number(),
19853
+ escalated: exports_external.boolean(),
19854
+ rolledTo: exports_external.string().nullable().optional()
19855
+ });
19842
19856
  RefreshAccountDataSchema = exports_external.object({
19843
19857
  account: exports_external.string(),
19844
19858
  expiresAt: exports_external.number().optional()
@@ -20037,6 +20051,15 @@ class AuthBrokerClient {
20037
20051
  const data = await this.send(req);
20038
20052
  return data;
20039
20053
  }
20054
+ async markThrottled(until) {
20055
+ const data = await this.send({
20056
+ v: PROTOCOL_VERSION,
20057
+ id: randomUUID(),
20058
+ op: "mark-throttled",
20059
+ until
20060
+ });
20061
+ return data;
20062
+ }
20040
20063
  async claimNotification(key, windowMs) {
20041
20064
  const data = await this.send({
20042
20065
  v: PROTOCOL_VERSION,
@@ -20474,7 +20497,6 @@ var init_schema = __esm(() => {
20474
20497
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
20475
20498
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
20476
20499
  format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
20477
- rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
20478
20500
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
20479
20501
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
20480
20502
  clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
@@ -28164,7 +28186,16 @@ function initHistory(stateDir, retentionDays = 30) {
28164
28186
  CREATE INDEX IF NOT EXISTS idx_messages_recent
28165
28187
  ON messages (chat_id, thread_id, ts DESC)
28166
28188
  `);
28167
- for (const column of ["reply_to_message_id INTEGER", "reply_to_text TEXT", "user_reaction TEXT"]) {
28189
+ for (const column of [
28190
+ "reply_to_message_id INTEGER",
28191
+ "reply_to_text TEXT",
28192
+ "user_reaction TEXT",
28193
+ "forwarded_from TEXT",
28194
+ "forwarded_from_type TEXT",
28195
+ "forwarded_from_id TEXT",
28196
+ "forwarded_date TEXT",
28197
+ "forwarded_message_id INTEGER"
28198
+ ]) {
28168
28199
  try {
28169
28200
  db.exec(`ALTER TABLE messages ADD COLUMN ${column}`);
28170
28201
  } catch (err) {
@@ -28264,10 +28295,10 @@ function recordInbound(args) {
28264
28295
  return;
28265
28296
  const stmt = requireDb().prepare(`
28266
28297
  INSERT OR REPLACE INTO messages
28267
- (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, reply_to_message_id, reply_to_text)
28268
- VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?)
28298
+ (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, reply_to_message_id, reply_to_text, forwarded_from, forwarded_from_type, forwarded_from_id, forwarded_date, forwarded_message_id)
28299
+ VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
28269
28300
  `);
28270
- stmt.run(args.chat_id, args.thread_id ?? null, args.message_id, args.user ?? null, args.user_id ?? null, args.ts, redact(args.text), args.attachment_kind ?? null, args.reply_to_message_id ?? null, args.reply_to_text != null ? redact(args.reply_to_text) : args.reply_to_text ?? null);
28301
+ stmt.run(args.chat_id, args.thread_id ?? null, args.message_id, args.user ?? null, args.user_id ?? null, args.ts, redact(args.text), args.attachment_kind ?? null, args.reply_to_message_id ?? null, args.reply_to_text != null ? redact(args.reply_to_text) : args.reply_to_text ?? null, args.forwarded_from != null ? redact(args.forwarded_from) : null, args.forwarded_from_type ?? null, args.forwarded_from_id ?? null, args.forwarded_date ?? null, args.forwarded_message_id ?? null);
28271
28302
  }
28272
28303
  function recordOutbound(args) {
28273
28304
  if (args.message_ids.length === 0)
@@ -36143,7 +36174,7 @@ function renderFallbackAnnouncement2(input) {
36143
36174
  const tz = input.tz ?? "UTC";
36144
36175
  const lines = [];
36145
36176
  const limitWord = input.oldQuota ? limitWordFor2(input.oldQuota) : "quota";
36146
- const headerLimit = limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
36177
+ const headerLimit = input.cause === "rate-limit" ? "rate limit" : limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
36147
36178
  if (!input.newLabel) {
36148
36179
  lines.push(`\uD83D\uDD34 **All accounts blocked \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
36149
36180
  lines.push("");
@@ -36164,8 +36195,8 @@ function renderFallbackAnnouncement2(input) {
36164
36195
  lines.push("");
36165
36196
  lines.push(`Earliest recovery: \`${codeSpanSafe(earliest.label)}\` ` + `${formatAbsolute2(earliest.at, tz)} (in ${formatRelative2(earliest.at, now)})`);
36166
36197
  }
36167
- } else if (input.oldQuota) {
36168
- const recovery = recoveryAtFor2(input.oldQuota);
36198
+ } else {
36199
+ const recovery = (input.oldQuota ? recoveryAtFor2(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
36169
36200
  if (recovery) {
36170
36201
  lines.push(`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute2(recovery, tz)} ` + `(in ${formatRelative2(recovery, now)})`);
36171
36202
  }
@@ -36180,8 +36211,8 @@ function renderFallbackAnnouncement2(input) {
36180
36211
  lines.push(`\`${codeSpanSafe(input.oldLabel)}\` \u2192 \`${codeSpanSafe(input.newLabel)}\``);
36181
36212
  lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
36182
36213
  lines.push("");
36183
- if (input.oldQuota) {
36184
- const recovery = recoveryAtFor2(input.oldQuota);
36214
+ {
36215
+ const recovery = (input.oldQuota ? recoveryAtFor2(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
36185
36216
  if (recovery) {
36186
36217
  lines.push(`\`${codeSpanSafe(input.oldLabel)}\` recovers ` + `${formatAbsolute2(recovery, tz)} (in ${formatRelative2(recovery, now)})`);
36187
36218
  }
@@ -39275,6 +39306,133 @@ function buildExtraAttachmentMeta(resolved) {
39275
39306
  return out;
39276
39307
  }
39277
39308
 
39309
+ // steering.ts
39310
+ function escapeXmlAttribute(s) {
39311
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
39312
+ }
39313
+
39314
+ // gateway/forward-origin.ts
39315
+ var FORWARDED_FROM_NAME_MAX = 100;
39316
+ function truncateName(name) {
39317
+ return name.length > FORWARDED_FROM_NAME_MAX ? name.slice(0, FORWARDED_FROM_NAME_MAX - 1) + "\u2026" : name;
39318
+ }
39319
+ function personName(parts) {
39320
+ const name = [parts.first_name, parts.last_name].filter((p) => typeof p === "string" && p.length > 0).join(" ");
39321
+ const handle = parts.username ? `(@${parts.username})` : "";
39322
+ return [name, handle].filter((p) => p.length > 0).join(" ");
39323
+ }
39324
+ function chatTitle(chat) {
39325
+ if (chat.title && chat.title.length > 0) {
39326
+ const handle = chat.username ? `(@${chat.username})` : "";
39327
+ return [chat.title, handle].filter((p) => p.length > 0).join(" ");
39328
+ }
39329
+ return personName(chat);
39330
+ }
39331
+ function parseForwardOrigin(origin) {
39332
+ if (origin == null || typeof origin !== "object")
39333
+ return;
39334
+ const date = typeof origin.date === "number" ? origin.date : undefined;
39335
+ switch (origin.type) {
39336
+ case "user": {
39337
+ const u = origin.sender_user;
39338
+ if (u == null || typeof u !== "object")
39339
+ return;
39340
+ const name = personName(u);
39341
+ if (name.length === 0) {
39342
+ if (typeof u.id !== "number")
39343
+ return;
39344
+ return { name: String(u.id), type: "user", id: u.id, date };
39345
+ }
39346
+ return {
39347
+ name: truncateName(name),
39348
+ type: "user",
39349
+ ...typeof u.id === "number" ? { id: u.id } : {},
39350
+ date
39351
+ };
39352
+ }
39353
+ case "hidden_user": {
39354
+ const name = typeof origin.sender_user_name === "string" ? origin.sender_user_name : "";
39355
+ if (name.length === 0)
39356
+ return;
39357
+ return { name: truncateName(name), type: "hidden_user", date };
39358
+ }
39359
+ case "chat": {
39360
+ const c = origin.sender_chat;
39361
+ if (c == null || typeof c !== "object")
39362
+ return;
39363
+ const name = chatTitle(c);
39364
+ if (name.length === 0) {
39365
+ if (typeof c.id !== "number")
39366
+ return;
39367
+ return { name: String(c.id), type: "chat", id: c.id, date };
39368
+ }
39369
+ return {
39370
+ name: truncateName(name),
39371
+ type: "chat",
39372
+ ...typeof c.id === "number" ? { id: c.id } : {},
39373
+ date
39374
+ };
39375
+ }
39376
+ case "channel": {
39377
+ const c = origin.chat;
39378
+ if (c == null || typeof c !== "object")
39379
+ return;
39380
+ const name = chatTitle(c);
39381
+ const messageId = typeof origin.message_id === "number" ? origin.message_id : undefined;
39382
+ if (name.length === 0) {
39383
+ if (typeof c.id !== "number")
39384
+ return;
39385
+ return { name: String(c.id), type: "channel", id: c.id, date, messageId };
39386
+ }
39387
+ return {
39388
+ name: truncateName(name),
39389
+ type: "channel",
39390
+ ...typeof c.id === "number" ? { id: c.id } : {},
39391
+ date,
39392
+ messageId
39393
+ };
39394
+ }
39395
+ default:
39396
+ return;
39397
+ }
39398
+ }
39399
+ function forwardOriginKey(o) {
39400
+ return o.id != null ? `${o.type}:${o.id}` : `${o.type}:${o.name}`;
39401
+ }
39402
+ function dedupeForwardOrigins(origins) {
39403
+ const seen = new Set;
39404
+ const out = [];
39405
+ for (const o of origins) {
39406
+ if (o == null)
39407
+ continue;
39408
+ const key = forwardOriginKey(o);
39409
+ if (seen.has(key))
39410
+ continue;
39411
+ seen.add(key);
39412
+ out.push(o);
39413
+ }
39414
+ return out;
39415
+ }
39416
+ function buildForwardOriginMeta(origins) {
39417
+ const out = {};
39418
+ origins.forEach((o, i) => {
39419
+ const suffix = i === 0 ? "" : `_${i + 1}`;
39420
+ out[`forwarded_from${suffix}`] = escapeXmlAttribute(truncateName(o.name));
39421
+ out[`forwarded_from_type${suffix}`] = o.type;
39422
+ if (o.id != null)
39423
+ out[`forwarded_from_id${suffix}`] = String(o.id);
39424
+ if (o.date != null) {
39425
+ out[`forwarded_date${suffix}`] = new Date(o.date * 1000).toISOString();
39426
+ }
39427
+ });
39428
+ return out;
39429
+ }
39430
+ function forwardOriginDateIso(o) {
39431
+ if (o?.date == null)
39432
+ return null;
39433
+ return new Date(o.date * 1000).toISOString();
39434
+ }
39435
+
39278
39436
  // status-reactions.ts
39279
39437
  var TELEGRAM_REACTION_WHITELIST = new Set([
39280
39438
  "\uD83D\uDC4D",
@@ -42019,6 +42177,7 @@ function createAuthBrokerClient() {
42019
42177
  listState: () => broker.listState(),
42020
42178
  setActive: (label) => broker.setActive(label),
42021
42179
  markExhausted: (until) => broker.markExhausted(until),
42180
+ markThrottled: (until) => broker.markThrottled(until),
42022
42181
  rmAccount: (label) => broker.rmAccount(label),
42023
42182
  refreshAccount: (label) => broker.refreshAccount(label),
42024
42183
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -54665,9 +54824,9 @@ function renderSafe(doc, source, maxLen = RICH_MESSAGE_MAX_CHARS) {
54665
54824
  var PLAIN_TEXT_MAX_CHARS = 4096;
54666
54825
  function parseRichRenderEnabled(raw) {
54667
54826
  if (raw == null)
54668
- return false;
54827
+ return true;
54669
54828
  const v = raw.trim().toLowerCase();
54670
- return v === "1" || v === "true" || v === "on" || v === "yes";
54829
+ return !(v === "0" || v === "false" || v === "off" || v === "no");
54671
54830
  }
54672
54831
  function richRenderEnabled(env = process.env) {
54673
54832
  return parseRichRenderEnabled(env.SWITCHROOM_RICH_RENDER);
@@ -62755,6 +62914,7 @@ function createAuthBrokerClient2() {
62755
62914
  listState: () => broker.listState(),
62756
62915
  setActive: (label) => broker.setActive(label),
62757
62916
  markExhausted: (until) => broker.markExhausted(until),
62917
+ markThrottled: (until) => broker.markThrottled(until),
62758
62918
  rmAccount: (label) => broker.rmAccount(label),
62759
62919
  refreshAccount: (label) => broker.refreshAccount(label),
62760
62920
  setOverride: (agent, account) => broker.setOverride(agent, account),
@@ -63161,6 +63321,15 @@ class AuthBrokerClient2 {
63161
63321
  const data = await this.send(req);
63162
63322
  return data;
63163
63323
  }
63324
+ async markThrottled(until) {
63325
+ const data = await this.send({
63326
+ v: PROTOCOL_VERSION,
63327
+ id: randomUUID4(),
63328
+ op: "mark-throttled",
63329
+ until
63330
+ });
63331
+ return data;
63332
+ }
63164
63333
  async claimNotification(key, windowMs) {
63165
63334
  const data = await this.send({
63166
63335
  v: PROTOCOL_VERSION,
@@ -64205,7 +64374,7 @@ function parseSteerPrefix(body) {
64205
64374
  return { steering: false, body };
64206
64375
  return { steering: true, body: m[2].trim() };
64207
64376
  }
64208
- function escapeXmlAttribute(s) {
64377
+ function escapeXmlAttribute2(s) {
64209
64378
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
64210
64379
  }
64211
64380
  function decodeXmlEntities(s) {
@@ -64216,13 +64385,13 @@ function formatPriorAssistantPreview(text4, maxChars = 200) {
64216
64385
  const decoded = decodeXmlEntities(stripped);
64217
64386
  const collapsed = decoded.replace(/\s+/g, " ").trim();
64218
64387
  const truncated = collapsed.length > maxChars ? collapsed.slice(0, maxChars) : collapsed;
64219
- return escapeXmlAttribute(truncated);
64388
+ return escapeXmlAttribute2(truncated);
64220
64389
  }
64221
64390
  function formatReplyToText(text4, maxChars) {
64222
64391
  if (text4 == null)
64223
64392
  return;
64224
64393
  const truncated = text4.length > maxChars ? text4.slice(0, maxChars - 1) + "\u2026" : text4;
64225
- return escapeXmlAttribute(truncated);
64394
+ return escapeXmlAttribute2(truncated);
64226
64395
  }
64227
64396
 
64228
64397
  // gateway/auto-classify-mid-turn.ts
@@ -64471,6 +64640,26 @@ var transientUpstreamSignals = [
64471
64640
  "would exceed your account\u2019s rate limit",
64472
64641
  "would exceed your account's rate limit"
64473
64642
  ];
64643
+ var litellmProxyLocal429Signals = [
64644
+ "deployment over user-defined ratelimit",
64645
+ "model rate limit exceeded. tpm limit",
64646
+ "model rate limit exceeded. rpm limit",
64647
+ "deployment over defined rpm limit",
64648
+ "no deployments available for selected model",
64649
+ "litellm rate limit handler",
64650
+ "crossed tpm / rpm",
64651
+ "max parallel request limit reached"
64652
+ ];
64653
+ var litellmV3LimiterSignalPair = ["rate limit exceeded for ", "limit type:"];
64654
+ function isLitellmProxyLocal429(text4) {
64655
+ if (typeof text4 !== "string" || text4.length === 0)
64656
+ return false;
64657
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64658
+ const lower = sample.toLowerCase();
64659
+ if (litellmProxyLocal429Signals.some((s) => lower.includes(s)))
64660
+ return true;
64661
+ return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
64662
+ }
64474
64663
  function detectModelUnavailable(stderr) {
64475
64664
  if (typeof stderr !== "string" || stderr.length === 0)
64476
64665
  return null;
@@ -64480,6 +64669,10 @@ function detectModelUnavailable(stderr) {
64480
64669
  const resetAt = parseResetTime(sample);
64481
64670
  return resetAt !== undefined ? { kind: "overload", resetAt, raw: stderr } : { kind: "overload", raw: stderr };
64482
64671
  }
64672
+ if (isLitellmProxyLocal429(sample)) {
64673
+ const resetAt = parseResetTime(sample);
64674
+ return resetAt !== undefined ? { kind: "overload", resetAt, raw: stderr } : { kind: "overload", raw: stderr };
64675
+ }
64483
64676
  const quotaSignals = [
64484
64677
  "out of extra usage",
64485
64678
  "extra usage",
@@ -64684,6 +64877,8 @@ function formatReason(d, now) {
64684
64877
  return `quota exhausted${reset2}`;
64685
64878
  case "overload":
64686
64879
  return `model overloaded${reset2}`;
64880
+ case "rate_limited":
64881
+ return `account rate-limited${reset2}`;
64687
64882
  case "network":
64688
64883
  return "network unreachable";
64689
64884
  }
@@ -64703,6 +64898,433 @@ function resolveModelUnavailableFromOperatorEvent(ev) {
64703
64898
  return detectModelUnavailable(detail);
64704
64899
  }
64705
64900
 
64901
+ // throttle-tier.ts
64902
+ init_card_format();
64903
+ init_quota_check();
64904
+
64905
+ // model-unavailable.ts
64906
+ init_quota_check();
64907
+ init_card_format();
64908
+ var transientUpstreamSignals2 = [
64909
+ "not your usage limit",
64910
+ "not your account",
64911
+ "not your account's",
64912
+ "temporarily limiting requests",
64913
+ "temporarily rate",
64914
+ "server is temporarily",
64915
+ "would exceed your account\u2019s rate limit",
64916
+ "would exceed your account's rate limit"
64917
+ ];
64918
+ function isTransientUpstreamSignal(text4) {
64919
+ if (typeof text4 !== "string" || text4.length === 0)
64920
+ return false;
64921
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64922
+ const lower = sample.toLowerCase();
64923
+ return transientUpstreamSignals2.some((s) => lower.includes(s));
64924
+ }
64925
+ var litellmProxyLocal429Signals2 = [
64926
+ "deployment over user-defined ratelimit",
64927
+ "model rate limit exceeded. tpm limit",
64928
+ "model rate limit exceeded. rpm limit",
64929
+ "deployment over defined rpm limit",
64930
+ "no deployments available for selected model",
64931
+ "litellm rate limit handler",
64932
+ "crossed tpm / rpm",
64933
+ "max parallel request limit reached"
64934
+ ];
64935
+ var litellmV3LimiterSignalPair2 = ["rate limit exceeded for ", "limit type:"];
64936
+ function isLitellmProxyLocal4292(text4) {
64937
+ if (typeof text4 !== "string" || text4.length === 0)
64938
+ return false;
64939
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64940
+ const lower = sample.toLowerCase();
64941
+ if (litellmProxyLocal429Signals2.some((s) => lower.includes(s)))
64942
+ return true;
64943
+ return litellmV3LimiterSignalPair2.every((s) => lower.includes(s));
64944
+ }
64945
+ function parseLitellmLimitDetail(text4, parseTimeNow = new Date) {
64946
+ const empty2 = { limitType: null, limit: null, currentUsage: null, resetAtMs: null };
64947
+ if (typeof text4 !== "string" || text4.length === 0)
64948
+ return empty2;
64949
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64950
+ const lower = sample.toLowerCase();
64951
+ let limitType = null;
64952
+ let limit = null;
64953
+ const eqLimit = lower.match(/\b([tr]pm)[ _]limit[=:]\s*(\d+)/);
64954
+ if (eqLimit) {
64955
+ limitType = eqLimit[1];
64956
+ limit = Number(eqLimit[2]);
64957
+ }
64958
+ if (limitType == null) {
64959
+ const v3Type = lower.match(/limit type:\s*(tokens|requests|max_parallel_requests)/);
64960
+ if (v3Type)
64961
+ limitType = v3Type[1];
64962
+ }
64963
+ if (limit == null) {
64964
+ const v3Limit = lower.match(/current limit:\s*(\d+)/);
64965
+ if (v3Limit)
64966
+ limit = Number(v3Limit[1]);
64967
+ }
64968
+ let currentUsage = null;
64969
+ const usage = lower.match(/current usage=(\d+)/);
64970
+ if (usage)
64971
+ currentUsage = Number(usage[1]);
64972
+ let resetAtMs = null;
64973
+ const resetsAt = sample.match(/limit resets at:\s*(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) UTC/i);
64974
+ if (resetsAt) {
64975
+ const d = new Date(`${resetsAt[1]}T${resetsAt[2]}Z`);
64976
+ if (!Number.isNaN(d.getTime()))
64977
+ resetAtMs = d.getTime();
64978
+ }
64979
+ if (resetAtMs == null) {
64980
+ const tryAgain = lower.match(/try again in\s+(\d+(?:\.\d+)?)\s*seconds/);
64981
+ if (tryAgain) {
64982
+ const secs = Number(tryAgain[1]);
64983
+ if (Number.isFinite(secs) && secs > 0 && secs < 7 * 24 * 3600) {
64984
+ resetAtMs = parseTimeNow.getTime() + Math.round(secs * 1000);
64985
+ }
64986
+ }
64987
+ }
64988
+ return {
64989
+ limitType,
64990
+ limit: limit != null && Number.isFinite(limit) ? limit : null,
64991
+ currentUsage: currentUsage != null && Number.isFinite(currentUsage) ? currentUsage : null,
64992
+ resetAtMs
64993
+ };
64994
+ }
64995
+ function parseResetTime2(text4, parseTimeNow = new Date) {
64996
+ const lower = text4.toLowerCase();
64997
+ const retryAfter = lower.match(/retry[\s-]*after[:\s]+(\d+)\s*(seconds?|s\b|minutes?|m\b|hours?|h\b)?/);
64998
+ if (retryAfter) {
64999
+ const n = Number(retryAfter[1]);
65000
+ if (Number.isFinite(n) && n > 0 && n < 7 * 24 * 3600) {
65001
+ const unit = (retryAfter[2] ?? "seconds").toLowerCase();
65002
+ const ms = unit.startsWith("h") ? n * 3600000 : unit.startsWith("m") ? n * 60000 : n * 1000;
65003
+ return new Date(parseTimeNow.getTime() + ms);
65004
+ }
65005
+ }
65006
+ const relReset = lower.match(/resets?\s+in\s+([0-9hms\s]+)/);
65007
+ if (relReset) {
65008
+ const ms = parseRelativeDuration2(relReset[1]);
65009
+ if (ms != null)
65010
+ return new Date(parseTimeNow.getTime() + ms);
65011
+ }
65012
+ const iso = text4.match(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/);
65013
+ if (iso) {
65014
+ const d = new Date(iso[0]);
65015
+ if (!Number.isNaN(d.getTime()))
65016
+ return d;
65017
+ }
65018
+ const calReset = text4.match(/resets?\s+(?:at\s+)?([A-Z][a-z]{2,8}\s+\d{1,2}(?:,?\s*(?:\d{1,2}(?::\d{2})?\s*(?:am|pm|AM|PM)?))?)/);
65019
+ if (calReset) {
65020
+ const candidate = `${calReset[1]} ${parseTimeNow.getUTCFullYear()}`;
65021
+ const d = new Date(candidate);
65022
+ if (!Number.isNaN(d.getTime()))
65023
+ return d;
65024
+ }
65025
+ const timeOnly = text4.match(/resets?\s+(?:at\s+)?(?!(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b)(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:\(([^)]+)\))?/i);
65026
+ if (timeOnly) {
65027
+ const d = resolveNextWallClock2(Number(timeOnly[1]), timeOnly[2] ? Number(timeOnly[2]) : 0, timeOnly[3]?.toLowerCase(), timeOnly[4]?.trim(), parseTimeNow);
65028
+ if (d != null)
65029
+ return d;
65030
+ }
65031
+ return;
65032
+ }
65033
+ function resolveNextWallClock2(hour12or24, minute, ampm, tz, nowDate) {
65034
+ let hour = hour12or24;
65035
+ if (ampm === "pm" && hour < 12)
65036
+ hour += 12;
65037
+ if (ampm === "am" && hour === 12)
65038
+ hour = 0;
65039
+ if (!Number.isFinite(hour) || hour > 23 || hour < 0)
65040
+ return;
65041
+ if (!Number.isFinite(minute) || minute > 59 || minute < 0)
65042
+ return;
65043
+ const nowMs2 = nowDate.getTime();
65044
+ const base = new Date(nowMs2);
65045
+ for (let dayOffset = 0;dayOffset <= 2; dayOffset++) {
65046
+ const dateParts = tzDateParts2(new Date(nowMs2 + dayOffset * 86400000), tz);
65047
+ if (dateParts == null)
65048
+ return;
65049
+ const epoch = wallClockToEpoch2(dateParts.year, dateParts.month, dateParts.day, hour, minute, tz);
65050
+ if (epoch != null && epoch > nowMs2)
65051
+ return new Date(epoch);
65052
+ }
65053
+ return;
65054
+ }
65055
+ function tzDateParts2(d, tz) {
65056
+ if (!tz) {
65057
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth(), day: d.getUTCDate() };
65058
+ }
65059
+ try {
65060
+ const fmt = new Intl.DateTimeFormat("en-US", {
65061
+ timeZone: tz,
65062
+ year: "numeric",
65063
+ month: "2-digit",
65064
+ day: "2-digit"
65065
+ });
65066
+ const parts = Object.fromEntries(fmt.formatToParts(d).filter((p) => p.type !== "literal").map((p) => [p.type, p.value]));
65067
+ return {
65068
+ year: Number(parts.year),
65069
+ month: Number(parts.month) - 1,
65070
+ day: Number(parts.day)
65071
+ };
65072
+ } catch {
65073
+ return null;
65074
+ }
65075
+ }
65076
+ function wallClockToEpoch2(year, month, day, hour, minute, tz) {
65077
+ const asUtc = Date.UTC(year, month, day, hour, minute, 0);
65078
+ if (!tz)
65079
+ return asUtc;
65080
+ try {
65081
+ const fmt = new Intl.DateTimeFormat("en-US", {
65082
+ timeZone: tz,
65083
+ year: "numeric",
65084
+ month: "2-digit",
65085
+ day: "2-digit",
65086
+ hour: "2-digit",
65087
+ minute: "2-digit",
65088
+ second: "2-digit",
65089
+ hour12: false
65090
+ });
65091
+ const parts = Object.fromEntries(fmt.formatToParts(new Date(asUtc)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value]));
65092
+ const shown = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour) % 24, Number(parts.minute), Number(parts.second));
65093
+ const offset = shown - asUtc;
65094
+ return asUtc - offset;
65095
+ } catch {
65096
+ return null;
65097
+ }
65098
+ }
65099
+ function parseRelativeDuration2(s) {
65100
+ let total = 0;
65101
+ let matched = false;
65102
+ const re = /(\d+)\s*(h|hours?|m|minutes?|s|seconds?)/g;
65103
+ let m;
65104
+ while ((m = re.exec(s)) != null) {
65105
+ matched = true;
65106
+ const n = Number(m[1]);
65107
+ const unit = m[2].toLowerCase();
65108
+ if (unit.startsWith("h"))
65109
+ total += n * 3600000;
65110
+ else if (unit.startsWith("m"))
65111
+ total += n * 60000;
65112
+ else
65113
+ total += n * 1000;
65114
+ }
65115
+ return matched && total > 0 ? total : null;
65116
+ }
65117
+
65118
+ // throttle-tier.ts
65119
+ var accountScopedThrottleSignals = [
65120
+ "would exceed your account's rate limit",
65121
+ "would exceed your account\u2019s rate limit",
65122
+ "not your account"
65123
+ ];
65124
+ function isAccountScopedThrottle(text4) {
65125
+ if (typeof text4 !== "string" || text4.length === 0)
65126
+ return false;
65127
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
65128
+ const lower = sample.toLowerCase();
65129
+ return accountScopedThrottleSignals.some((s) => lower.includes(s));
65130
+ }
65131
+ function classify429Detail(text4) {
65132
+ if (isAccountScopedThrottle(text4))
65133
+ return "account-scoped";
65134
+ if (isLitellmProxyLocal4292(text4))
65135
+ return "litellm-local";
65136
+ return "generic-transient";
65137
+ }
65138
+ function build429ClassifiedMetric(opts) {
65139
+ const detail = typeof opts.detail === "string" ? opts.detail : "";
65140
+ const litellm = parseLitellmLimitDetail(detail, new Date(opts.now));
65141
+ const anthropicResetMs = parseResetTime2(detail, new Date(opts.now))?.getTime() ?? null;
65142
+ const resetAtMs = anthropicResetMs ?? litellm.resetAtMs;
65143
+ return {
65144
+ kind: "rate_limit_429_classified",
65145
+ agent: opts.agent,
65146
+ classification: opts.classification,
65147
+ action: opts.action,
65148
+ reset_at_ms: resetAtMs,
65149
+ reset_in_ms: resetAtMs != null ? Math.max(0, resetAtMs - opts.now) : null,
65150
+ limit_type: litellm.limitType,
65151
+ limit: litellm.limit,
65152
+ current_usage: litellm.currentUsage
65153
+ };
65154
+ }
65155
+ var THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT = 5 * 60000;
65156
+ var THROTTLE_DEFAULT_WAIT_MS = 60000;
65157
+ function throttleRetryInPlaceMaxMs(env = process.env) {
65158
+ const raw = env.SWITCHROOM_THROTTLE_RETRY_IN_PLACE_MAX_MS;
65159
+ if (raw == null || raw === "")
65160
+ return THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT;
65161
+ const n = Number(raw);
65162
+ return Number.isFinite(n) && n > 0 ? n : THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT;
65163
+ }
65164
+ function decideThrottleTier(opts) {
65165
+ const { detail, now, thresholdMs } = opts;
65166
+ if (!isAccountScopedThrottle(detail))
65167
+ return { action: "none" };
65168
+ const resetAt = parseResetTime2(detail, new Date(now));
65169
+ const resetAtMs = resetAt?.getTime();
65170
+ if (resetAtMs == null || !Number.isFinite(resetAtMs) || resetAtMs <= now) {
65171
+ return {
65172
+ action: "throttle",
65173
+ throttledUntilMs: now + THROTTLE_DEFAULT_WAIT_MS,
65174
+ resetParsed: false
65175
+ };
65176
+ }
65177
+ if (resetAtMs - now <= thresholdMs) {
65178
+ return { action: "throttle", throttledUntilMs: resetAtMs, resetParsed: true };
65179
+ }
65180
+ return { action: "failover", resetAtMs };
65181
+ }
65182
+ var THROTTLE_NOTICE_COOLDOWN_MS = 10 * 60000;
65183
+
65184
+ // throttle-tier.ts
65185
+ init_card_format();
65186
+ init_quota_check();
65187
+ var THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT2 = 5 * 60000;
65188
+ var THROTTLE_NOTICE_COOLDOWN_MS2 = 10 * 60000;
65189
+ function evaluateThrottleNotice(prev, account, now, cooldownMs = THROTTLE_NOTICE_COOLDOWN_MS2) {
65190
+ const last = prev.lastSentAtMsByAccount[account] ?? 0;
65191
+ if (now - last >= cooldownMs) {
65192
+ return {
65193
+ send: true,
65194
+ next: {
65195
+ lastSentAtMsByAccount: {
65196
+ ...prev.lastSentAtMsByAccount,
65197
+ [account]: now
65198
+ }
65199
+ }
65200
+ };
65201
+ }
65202
+ return { send: false, next: prev };
65203
+ }
65204
+ function renderThrottleNotice(opts) {
65205
+ const now = opts.now ?? new Date;
65206
+ const resetStr = formatResetRelative(new Date(opts.throttledUntilMs), now);
65207
+ const acct = opts.account ? `\`${escapeMarkdown(opts.account)}\`` : "the active account";
65208
+ const lines = [
65209
+ `\uD83D\uDEA6 **Rate-limited, staying put** \u2014 ${acct} hit a transient rate limit on **${escapeMarkdown(opts.agent)}**.`,
65210
+ `This is a short throttle, not a quota wall \u2014 ${opts.resetParsed ? resetStr : `no reset given, retrying in ~60s`}.`,
65211
+ `_Staying on ${acct}; no failover needed. The turn retries automatically after the reset._`
65212
+ ];
65213
+ return lines.join(`
65214
+ `);
65215
+ }
65216
+ function renderThrottleEscalationNotice(opts) {
65217
+ const acct = opts.account ? `\`${escapeMarkdown(opts.account)}\`` : "the active account";
65218
+ const head = `\u26d4\ufe0f **Rate limit was actually a wall** \u2014 repeated 429s on ${acct} ` + `(trigger: **${escapeMarkdown(opts.agent)}**) were corroborated by a live quota probe.`;
65219
+ const tail = opts.rolledTo ? `Marked exhausted and rolled to \`${escapeMarkdown(opts.rolledTo)}\`.` : `Marked exhausted \u2014 no fallback account had quota (all blocked). ` + `Use \`/auth add <label>\` to attach another subscription.`;
65220
+ return `${head}
65221
+ ${tail}`;
65222
+ }
65223
+
65224
+ // gateway/throttle-tier-wiring.ts
65225
+ var THROTTLE_RETRY_NUDGE_SLACK_MS = 5000;
65226
+ var THROTTLE_RETRY_NUDGE_JITTER_MAX_MS = 30000;
65227
+ function createThrottleTierRunner(deps) {
65228
+ const now = deps.now ?? (() => Date.now());
65229
+ const schedule = deps.schedule ?? ((fn, ms) => {
65230
+ const t = setTimeout(fn, ms);
65231
+ if (typeof t.unref === "function")
65232
+ t.unref();
65233
+ return { cancel: () => clearTimeout(t) };
65234
+ });
65235
+ const jitterMs = deps.jitterMs ?? (() => Math.floor(Math.random() * THROTTLE_RETRY_NUDGE_JITTER_MAX_MS));
65236
+ let noticeState = { lastSentAtMsByAccount: {} };
65237
+ let pendingNudge = null;
65238
+ async function broadcastDeduped(client3, keyPrefix, account, markdown) {
65239
+ for (const chatId of deps.listNoticeChats()) {
65240
+ let granted = true;
65241
+ if (client3 && account) {
65242
+ try {
65243
+ granted = (await client3.claimNotification(`${keyPrefix}:${account}:${chatId}`, THROTTLE_NOTICE_COOLDOWN_MS2)).granted;
65244
+ } catch {
65245
+ granted = true;
65246
+ }
65247
+ }
65248
+ if (granted) {
65249
+ deps.sendNotice(chatId, markdown);
65250
+ } else {
65251
+ deps.log(`[throttle-tier] notice suppressed (fleet claim) chat=${chatId}`);
65252
+ }
65253
+ }
65254
+ }
65255
+ function nudgeResume(reason, armedAtMs) {
65256
+ const newest = deps.newestActiveTurnStartedAtMs();
65257
+ if (deps.turnInFlight()) {
65258
+ if (newest != null && newest > armedAtMs) {
65259
+ deps.log(`[throttle-tier] resume skipped (superseded by a live newer turn) reason=${reason}`);
65260
+ return;
65261
+ }
65262
+ deps.log(`[throttle-tier] resume deferred to turn-complete reason=${reason}`);
65263
+ deps.deferRestartToTurnComplete(deps.agentName, reason);
65264
+ return;
65265
+ }
65266
+ const verdict = deps.resumeDecide(newest);
65267
+ if (verdict === "resume") {
65268
+ deps.log(`[throttle-tier] resuming dead turn via self-restart reason=${reason}`);
65269
+ deps.restartNow(deps.agentName, reason);
65270
+ } else {
65271
+ deps.log(`[throttle-tier] resume suppressed (${verdict}) reason=${reason}`);
65272
+ }
65273
+ }
65274
+ async function fire(triggerAgent, throttledUntilMs, resetParsed) {
65275
+ const armedAtMs = now();
65276
+ let client3 = null;
65277
+ let account = null;
65278
+ let escalated = false;
65279
+ let rolledTo = null;
65280
+ try {
65281
+ client3 = await deps.getBrokerClient();
65282
+ if (client3) {
65283
+ const r = await client3.markThrottled(throttledUntilMs);
65284
+ account = r.account;
65285
+ escalated = r.escalated;
65286
+ rolledTo = r.rolledTo ?? null;
65287
+ } else {
65288
+ deps.log(`[throttle-tier] broker unreachable \u2014 notice only, no ledger record agent=${triggerAgent}`);
65289
+ }
65290
+ } catch (err) {
65291
+ deps.log(`[throttle-tier] markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
65292
+ }
65293
+ if (escalated) {
65294
+ deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
65295
+ await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
65296
+ if (rolledTo)
65297
+ nudgeResume("throttle-escalation-resume", armedAtMs);
65298
+ return;
65299
+ }
65300
+ const cooldownKey = account ?? `agent:${triggerAgent}`;
65301
+ const verdict = evaluateThrottleNotice(noticeState, cooldownKey, now());
65302
+ if (verdict.send) {
65303
+ noticeState = verdict.next;
65304
+ await broadcastDeduped(client3, "throttle-notice", account, renderThrottleNotice({
65305
+ account,
65306
+ agent: triggerAgent,
65307
+ throttledUntilMs,
65308
+ resetParsed,
65309
+ now: new Date(now())
65310
+ }));
65311
+ } else {
65312
+ deps.log(`[throttle-tier] notice suppressed (cooldown) key=${cooldownKey}`);
65313
+ }
65314
+ const delayMs = Math.max(throttledUntilMs - now(), 0) + THROTTLE_RETRY_NUDGE_SLACK_MS + jitterMs();
65315
+ if (pendingNudge)
65316
+ pendingNudge.cancel();
65317
+ pendingNudge = schedule(() => {
65318
+ pendingNudge = null;
65319
+ nudgeResume("throttle-retry-resume", armedAtMs);
65320
+ }, delayMs);
65321
+ }
65322
+ return {
65323
+ fire,
65324
+ inspect: () => ({ noticeState, nudgePending: pendingNudge != null })
65325
+ };
65326
+ }
65327
+
64706
65328
  // auto-fallback-fleet.ts
64707
65329
  init_card_format();
64708
65330
  init_auth_snapshot_format();
@@ -64738,7 +65360,7 @@ async function runFleetAutoFallback(deps) {
64738
65360
  };
64739
65361
  }
64740
65362
  const oldHealth = classifyHealth(oldSnap, now);
64741
- if (oldHealth === "healthy") {
65363
+ if (oldHealth === "healthy" && !deps.rateLimitTrigger) {
64742
65364
  return {
64743
65365
  kind: "no-eligible-target",
64744
65366
  oldLabel: oldSnap.label,
@@ -64759,6 +65381,8 @@ async function runFleetAutoFallback(deps) {
64759
65381
  newQuota: null,
64760
65382
  triggerAgent: deps.triggerAgent,
64761
65383
  fleetSnapshots: snapshots,
65384
+ parsedResetAt: deps.parsedResetAt ?? null,
65385
+ cause: deps.rateLimitTrigger ? "rate-limit" : undefined,
64762
65386
  tz,
64763
65387
  now
64764
65388
  })
@@ -64777,6 +65401,8 @@ async function runFleetAutoFallback(deps) {
64777
65401
  newLabel: rolledTo,
64778
65402
  newQuota,
64779
65403
  triggerAgent: deps.triggerAgent,
65404
+ parsedResetAt: deps.parsedResetAt ?? null,
65405
+ cause: deps.rateLimitTrigger ? "rate-limit" : undefined,
64780
65406
  tz,
64781
65407
  now
64782
65408
  })
@@ -66830,13 +67456,13 @@ function buildFolderPickerCard(input) {
66830
67456
  validateDriveId(folder.id, "folder.id");
66831
67457
  rows.push([
66832
67458
  {
66833
- text: `\u2705 Allow "${truncateName(folder.name, 48)}"`,
67459
+ text: `\u2705 Allow "${truncateName2(folder.name, 48)}"`,
66834
67460
  callback_data: encodeCallback(["drvpick", "grant", input.agent, folder.id])
66835
67461
  }
66836
67462
  ]);
66837
67463
  rows.push([
66838
67464
  {
66839
- text: `\uD83D\uDCC2 Browse "${truncateName(folder.name, 46)}"`,
67465
+ text: `\uD83D\uDCC2 Browse "${truncateName2(folder.name, 46)}"`,
66840
67466
  callback_data: encodeCallback(["drvpick", "enter", input.agent, folder.id])
66841
67467
  }
66842
67468
  ]);
@@ -66885,14 +67511,14 @@ function buildFolderPickerCard(input) {
66885
67511
  return { body, rows };
66886
67512
  }
66887
67513
  function renderBreadcrumb(trail2, leafName) {
66888
- const leaf = truncateName(leafName, 24);
67514
+ const leaf = truncateName2(leafName, 24);
66889
67515
  if (trail2.length === 0)
66890
67516
  return `/${leaf}`;
66891
- const segments = trail2.map((t) => truncateName(t.name, 20));
67517
+ const segments = trail2.map((t) => truncateName2(t.name, 20));
66892
67518
  segments.push(leaf);
66893
67519
  return `/${segments.join("/")}`;
66894
67520
  }
66895
- function truncateName(name, max) {
67521
+ function truncateName2(name, max) {
66896
67522
  if (name.length <= max)
66897
67523
  return name;
66898
67524
  return `${name.slice(0, max - 1)}\u2026`;
@@ -68918,6 +69544,11 @@ function extractConfirmation(pane) {
68918
69544
  // ../src/agents/scaffold.ts
68919
69545
  import { join as join32, resolve as resolve6 } from "node:path";
68920
69546
  init_atomic();
69547
+
69548
+ // ../src/agents/agent-uid.ts
69549
+ init_peercred();
69550
+
69551
+ // ../src/agents/scaffold.ts
68921
69552
  init_schema();
68922
69553
 
68923
69554
  // ../src/config/users.ts
@@ -76405,27 +77036,6 @@ function getNestedObj(obj, key) {
76405
77036
  var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS2 = 5 * 60000;
76406
77037
  var cooldownMap2 = new Map;
76407
77038
 
76408
- // model-unavailable.ts
76409
- init_quota_check();
76410
- init_card_format();
76411
- var transientUpstreamSignals2 = [
76412
- "not your usage limit",
76413
- "not your account",
76414
- "not your account's",
76415
- "temporarily limiting requests",
76416
- "temporarily rate",
76417
- "server is temporarily",
76418
- "would exceed your account\u2019s rate limit",
76419
- "would exceed your account's rate limit"
76420
- ];
76421
- function isTransientUpstreamSignal(text4) {
76422
- if (typeof text4 !== "string" || text4.length === 0)
76423
- return false;
76424
- const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
76425
- const lower = sample.toLowerCase();
76426
- return transientUpstreamSignals2.some((s) => lower.includes(s));
76427
- }
76428
-
76429
77039
  // session-tail.ts
76430
77040
  function sanitizeCwdToProjectName(cwd) {
76431
77041
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -76610,6 +77220,7 @@ function detectErrorInTranscriptLine(line) {
76610
77220
  const errStr = typeof obj.error === "string" ? obj.error : "";
76611
77221
  const text4 = extractAssistantText(obj);
76612
77222
  const kind2 = status === 429 ? isTransientUpstreamSignal(`${text4}
77223
+ ${errStr}`) || isLitellmProxyLocal4292(`${text4}
76613
77224
  ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: errStr, status, message: text4 });
76614
77225
  return {
76615
77226
  kind: kind2,
@@ -80470,10 +81081,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
80470
81081
  }
80471
81082
 
80472
81083
  // ../src/build-info.ts
80473
- var VERSION = "0.18.13";
80474
- var COMMIT_SHA = "58d180bf";
80475
- var COMMIT_DATE = "2026-07-12T00:08:55Z";
80476
- var LATEST_PR = 3156;
81084
+ var VERSION = "0.18.15";
81085
+ var COMMIT_SHA = "2fa611a1";
81086
+ var COMMIT_DATE = "2026-07-12T04:44:56Z";
81087
+ var LATEST_PR = 3171;
80477
81088
  var COMMITS_AHEAD_OF_TAG = 0;
80478
81089
 
80479
81090
  // gateway/boot-version.ts
@@ -85140,22 +85751,71 @@ var inboundCoalescer = createInboundCoalescer({
85140
85751
  merge: (entries) => {
85141
85752
  const last = entries[entries.length - 1];
85142
85753
  const { primary, extras } = splitCoalescedAttachments(entries, (e) => e.downloadImage != null || e.attachment != null, coalesceMaxAttachments());
85754
+ const forwardOrigins = dedupeForwardOrigins(entries.map((e) => e.forwardOrigin));
85143
85755
  return {
85144
85756
  text: entries.map((e) => e.text).filter((t) => t.length > 0).join(`
85145
85757
  `),
85146
85758
  ctx: last.ctx,
85147
85759
  downloadImage: primary?.downloadImage,
85148
85760
  attachment: primary?.attachment,
85149
- extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined
85761
+ extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined,
85762
+ forwardOrigins: forwardOrigins.length > 0 ? forwardOrigins : undefined
85150
85763
  };
85151
85764
  },
85152
85765
  onFlush: (key, merged) => {
85153
85766
  bufferedAttachmentKeys.delete(key);
85154
- handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments);
85767
+ handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments, merged.forwardOrigins);
85155
85768
  }
85156
85769
  });
85157
85770
  function emitGatewayOperatorEvent(event) {
85158
85771
  const { agent, kind } = event;
85772
+ let throttleEscalation = null;
85773
+ let escalationFired = false;
85774
+ const rateLimit429Classification = kind === "rate-limited" ? classify429Detail(event.detail) : null;
85775
+ if (rateLimit429Classification != null && rateLimit429Classification !== "account-scoped") {
85776
+ emitRuntimeMetric(build429ClassifiedMetric({
85777
+ agent,
85778
+ detail: event.detail,
85779
+ classification: rateLimit429Classification,
85780
+ action: "calm",
85781
+ now: Date.now()
85782
+ }));
85783
+ if (rateLimit429Classification === "litellm-local") {
85784
+ process.stderr.write(`telegram gateway: 429 classified litellm-proxy-local agent=${agent} \u2014 ` + `calm path, no account attribution, no failover
85785
+ `);
85786
+ }
85787
+ }
85788
+ if (rateLimit429Classification === "account-scoped") {
85789
+ const throttleDecision = decideThrottleTier({
85790
+ detail: event.detail,
85791
+ now: Date.now(),
85792
+ thresholdMs: throttleRetryInPlaceMaxMs()
85793
+ });
85794
+ emitRuntimeMetric(build429ClassifiedMetric({
85795
+ agent,
85796
+ detail: event.detail,
85797
+ classification: "account-scoped",
85798
+ action: throttleDecision.action === "failover" ? "failover" : "throttle",
85799
+ now: Date.now()
85800
+ }));
85801
+ if (throttleDecision.action === "throttle") {
85802
+ process.stderr.write(`telegram gateway: throttle-tier staying-put agent=${agent} until=${new Date(throttleDecision.throttledUntilMs).toISOString()} parsedReset=${throttleDecision.resetParsed}
85803
+ `);
85804
+ try {
85805
+ recordOperatorEvent(event);
85806
+ } catch {}
85807
+ throttleTierRunner.fire(agent, throttleDecision.throttledUntilMs, throttleDecision.resetParsed);
85808
+ return;
85809
+ }
85810
+ if (throttleDecision.action === "failover") {
85811
+ const resetAt = new Date(throttleDecision.resetAtMs);
85812
+ throttleEscalation = { kind: "rate_limited", resetAt, raw: event.detail };
85813
+ if (wouldFireFleetAutoFallback()) {
85814
+ escalationFired = true;
85815
+ fireFleetAutoFallback(agent, resolveExhaustUntil(resetAt.getTime()), resetAt, "rate-limit");
85816
+ }
85817
+ }
85818
+ }
85159
85819
  if (!shouldEmitOperatorEvent(agent, kind)) {
85160
85820
  process.stderr.write(`telegram gateway: operator-event suppressed (cooldown) agent=${agent} kind=${kind}
85161
85821
  `);
@@ -85167,13 +85827,13 @@ function emitGatewayOperatorEvent(event) {
85167
85827
  process.stderr.write(`telegram gateway: recordOperatorEvent failed agent=${agent} kind=${kind}: ${err.message}
85168
85828
  `);
85169
85829
  }
85170
- const modelUnavailable = resolveModelUnavailableFromOperatorEvent(event);
85830
+ const modelUnavailable = throttleEscalation ?? resolveModelUnavailableFromOperatorEvent(event);
85171
85831
  let renderedText;
85172
85832
  let renderedKeyboard;
85173
85833
  let cardPromisedFallback = false;
85174
85834
  if (modelUnavailable) {
85175
- const isAutoKind = modelUnavailable.kind === "quota_exhausted";
85176
- const willActuallyFire = isAutoKind && wouldFireFleetAutoFallback();
85835
+ const isAutoKind = modelUnavailable.kind === "quota_exhausted" || modelUnavailable.kind === "rate_limited";
85836
+ const willActuallyFire = throttleEscalation != null ? escalationFired : isAutoKind && wouldFireFleetAutoFallback();
85177
85837
  process.stderr.write(`telegram gateway: operator-event suppressing-raw-stderr-for-model-unavailable agent=${agent} kind=${kind} detected=${modelUnavailable.kind} autoKind=${isAutoKind} willFire=${willActuallyFire}
85178
85838
  `);
85179
85839
  renderedText = formatModelUnavailableCard(modelUnavailable, agent, {
@@ -85181,9 +85841,9 @@ function emitGatewayOperatorEvent(event) {
85181
85841
  });
85182
85842
  renderedKeyboard = undefined;
85183
85843
  cardPromisedFallback = willActuallyFire;
85184
- if (willActuallyFire) {
85844
+ if (willActuallyFire && throttleEscalation == null) {
85185
85845
  const untilMs = resolveExhaustUntil(modelUnavailable.resetAt?.getTime());
85186
- fireFleetAutoFallback(agent, untilMs);
85846
+ fireFleetAutoFallback(agent, untilMs, modelUnavailable.resetAt);
85187
85847
  }
85188
85848
  } else {
85189
85849
  try {
@@ -86899,7 +87559,7 @@ var ipcServer = createIpcServer({
86899
87559
  const untilMs = resolveExhaustUntil(msg.resetAt);
86900
87560
  process.stderr.write(`telegram gateway: quota_wall_detected agent=${msg.agentName} until=${new Date(untilMs).toISOString()}` + (msg.resetAt == null ? " (reset unparsed \u2192 +7d default)" : "") + ` \u2014 triggering fleet auto-fallback
86901
87561
  `);
86902
- fireFleetAutoFallback(msg.agentName, untilMs);
87562
+ fireFleetAutoFallback(msg.agentName, untilMs, msg.resetAt != null ? new Date(msg.resetAt) : undefined);
86903
87563
  },
86904
87564
  onQueryPendingPermission(client3, msg) {
86905
87565
  const self = process.env.SWITCHROOM_AGENT_NAME;
@@ -90392,7 +91052,13 @@ async function handleInboundCoalesced(ctx, text5, downloadImage, attachment) {
90392
91052
  maybeEarlyAckReaction(ctx, from);
90393
91053
  maybePokeFloorForMidTurnInbound(ctx, from);
90394
91054
  const key = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
90395
- const result = inboundCoalescer.enqueue(key, { text: text5, ctx, downloadImage, attachment });
91055
+ const result = inboundCoalescer.enqueue(key, {
91056
+ text: text5,
91057
+ ctx,
91058
+ downloadImage,
91059
+ attachment,
91060
+ forwardOrigin: parseForwardOrigin(ctx.message?.forward_origin)
91061
+ });
90396
91062
  if (result.bypass)
90397
91063
  return handleInbound(ctx, text5, downloadImage, attachment);
90398
91064
  if (hasAttachment)
@@ -90430,7 +91096,7 @@ function maybePokeFloorForMidTurnInbound(ctx, from) {
90430
91096
  return;
90431
91097
  pokeFloorNow(key, Date.now());
90432
91098
  }
90433
- async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachments) {
91099
+ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachments, coalescedForwardOrigins) {
90434
91100
  markIdleActivity();
90435
91101
  const isTopicMessage = ctx.message?.is_topic_message ?? false;
90436
91102
  const messageThreadId = ctx.message?.message_thread_id;
@@ -91056,6 +91722,9 @@ ${preBlock(write.output)}`;
91056
91722
  const replyToTextRaw = replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? undefined : undefined;
91057
91723
  const replyToText = replyToTextRaw != null ? replyToTextRaw.length > REPLY_TO_TEXT_MAX ? replyToTextRaw.slice(0, REPLY_TO_TEXT_MAX - 1) + "\u2026" : replyToTextRaw : undefined;
91058
91724
  const replyToTextEscaped = formatReplyToText(replyToTextRaw, REPLY_TO_TEXT_MAX);
91725
+ const forwardOrigins = coalescedForwardOrigins ?? dedupeForwardOrigins([parseForwardOrigin(ctx.message?.forward_origin)]);
91726
+ const forwardOriginMeta = buildForwardOriginMeta(forwardOrigins);
91727
+ const primaryForwardOrigin = forwardOrigins[0];
91059
91728
  if (HISTORY_ENABLED) {
91060
91729
  try {
91061
91730
  recordInbound({
@@ -91068,7 +91737,12 @@ ${preBlock(write.output)}`;
91068
91737
  text: effectiveText,
91069
91738
  attachment_kind: attachment?.kind,
91070
91739
  reply_to_message_id: replyToMessageId ?? null,
91071
- reply_to_text: replyToText ?? null
91740
+ reply_to_text: replyToText ?? null,
91741
+ forwarded_from: primaryForwardOrigin?.name ?? null,
91742
+ forwarded_from_type: primaryForwardOrigin?.type ?? null,
91743
+ forwarded_from_id: primaryForwardOrigin?.id != null ? String(primaryForwardOrigin.id) : null,
91744
+ forwarded_date: forwardOriginDateIso(primaryForwardOrigin),
91745
+ forwarded_message_id: primaryForwardOrigin?.messageId ?? null
91072
91746
  });
91073
91747
  } catch (err) {
91074
91748
  process.stderr.write(`telegram gateway: history recordInbound failed: ${err}
@@ -91134,6 +91808,7 @@ ${preBlock(write.output)}`;
91134
91808
  ...imagePath ? { image_path: imagePath } : {},
91135
91809
  ...replyToMessageId != null ? { reply_to_message_id: String(replyToMessageId) } : {},
91136
91810
  ...replyToTextEscaped != null && replyToTextEscaped.length > 0 ? { reply_to_text: replyToTextEscaped } : {},
91811
+ ...forwardOriginMeta,
91137
91812
  ...isQueuedMidTurn || isQueuedPrefix ? { queued: "true" } : {},
91138
91813
  ...isSteering ? { steering: "true" } : {},
91139
91814
  ...priorTurnInProgress ? { prior_turn_in_progress: "true" } : {},
@@ -93238,12 +93913,33 @@ function newestActiveTurnStartedAtMs() {
93238
93913
  function wouldFireFleetAutoFallback() {
93239
93914
  return fleetFallbackGate.wouldFire();
93240
93915
  }
93241
- async function fireFleetAutoFallback(triggerAgent, untilMs) {
93242
- return fleetFallbackGate.fire(() => doFireFleetAutoFallback(triggerAgent, untilMs), (err) => {
93916
+ async function fireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger) {
93917
+ return fleetFallbackGate.fire(() => doFireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger), (err) => {
93243
93918
  process.stderr.write(`telegram gateway: [fleet-fallback] error agent=${triggerAgent}: ${err?.message ?? err}
93244
93919
  `);
93245
93920
  });
93246
93921
  }
93922
+ var throttleTierRunner = createThrottleTierRunner({
93923
+ agentName: process.env.SWITCHROOM_AGENT_NAME ?? "",
93924
+ getBrokerClient: () => getAuthBrokerClient2(process.env.SWITCHROOM_AGENT_NAME ?? ""),
93925
+ listNoticeChats: () => loadAccess().allowFrom,
93926
+ sendNotice: (chat_id, markdown) => {
93927
+ swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(markdown), { disable_notification: true }), { chat_id: String(chat_id), verb: "throttle-tier:notify" });
93928
+ },
93929
+ resumeDecide: (ts) => fleetFallbackResumeGate.decide(ts),
93930
+ newestActiveTurnStartedAtMs,
93931
+ turnInFlight: () => turnInFlightForGate(),
93932
+ deferRestartToTurnComplete: (agentName3, reason) => {
93933
+ process.stderr.write(`telegram gateway: [throttle-tier] restart deferred to turn-complete agent=${agentName3} reason=${reason}
93934
+ `);
93935
+ pendingRestarts.set(agentName3, Date.now());
93936
+ },
93937
+ restartNow: (agentName3, reason) => {
93938
+ triggerSelfRestart(agentName3, reason);
93939
+ },
93940
+ log: (m) => process.stderr.write(`telegram gateway: ${m}
93941
+ `)
93942
+ });
93247
93943
  var fallbackFailureNoticeState = { lastSentAtMs: 0 };
93248
93944
  var fallbackAllBlockedNoticeState = { lastSentAtMs: 0 };
93249
93945
  function broadcastFleetFallbackFailure(triggerAgent, reason) {
@@ -93264,7 +93960,7 @@ function broadcastFleetFallbackFailure(triggerAgent, reason) {
93264
93960
  swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(html), {}), { chat_id, verb: "fleet-fallback:failure-notify" });
93265
93961
  }
93266
93962
  }
93267
- async function doFireFleetAutoFallback(triggerAgent, untilMs) {
93963
+ async function doFireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger) {
93268
93964
  try {
93269
93965
  const client3 = await getAuthBrokerClient2(triggerAgent);
93270
93966
  if (!client3) {
@@ -93288,7 +93984,9 @@ async function doFireFleetAutoFallback(triggerAgent, untilMs) {
93288
93984
  return { rolledTo: r.rolledTo ?? null, rolled: r.rolled };
93289
93985
  },
93290
93986
  triggerAgent,
93291
- tz
93987
+ tz,
93988
+ parsedResetAt,
93989
+ rateLimitTrigger: trigger === "rate-limit"
93292
93990
  });
93293
93991
  process.stderr.write(`telegram gateway: [fleet-fallback] outcome=${outcome.kind} agent=${triggerAgent}` + (outcome.kind === "switched" ? ` old=${outcome.oldLabel} new=${outcome.newLabel}` : "") + `
93294
93992
  `);