switchroom 0.18.13 → 0.18.14

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 (42) hide show
  1. package/dist/agent-scheduler/index.js +49 -9
  2. package/dist/auth-broker/index.js +111 -7
  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 +35 -6
  8. package/dist/host-control/main.js +1 -2
  9. package/dist/vault/approvals/kernel-server.js +0 -1
  10. package/dist/vault/broker/server.js +0 -1
  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 +2 -0
  19. package/telegram-plugin/dist/gateway/gateway.js +620 -67
  20. package/telegram-plugin/dist/server.js +2 -0
  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 +224 -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 +20 -2
  28. package/telegram-plugin/render/rich-render.ts +40 -32
  29. package/telegram-plugin/stream-controller.ts +3 -2
  30. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
  31. package/telegram-plugin/tests/forward-origin.test.ts +309 -0
  32. package/telegram-plugin/tests/history.test.ts +157 -0
  33. package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
  34. package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
  35. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
  36. package/telegram-plugin/tests/status-accent.test.ts +5 -3
  37. package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
  38. package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
  39. package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
  40. package/telegram-plugin/tests/throttle-tier.test.ts +278 -0
  41. package/telegram-plugin/throttle-tier.ts +226 -0
  42. 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
@@ -64684,6 +64853,8 @@ function formatReason(d, now) {
64684
64853
  return `quota exhausted${reset2}`;
64685
64854
  case "overload":
64686
64855
  return `model overloaded${reset2}`;
64856
+ case "rate_limited":
64857
+ return `account rate-limited${reset2}`;
64687
64858
  case "network":
64688
64859
  return "network unreachable";
64689
64860
  }
@@ -64703,6 +64874,339 @@ function resolveModelUnavailableFromOperatorEvent(ev) {
64703
64874
  return detectModelUnavailable(detail);
64704
64875
  }
64705
64876
 
64877
+ // throttle-tier.ts
64878
+ init_card_format();
64879
+ init_quota_check();
64880
+
64881
+ // model-unavailable.ts
64882
+ init_quota_check();
64883
+ init_card_format();
64884
+ var transientUpstreamSignals2 = [
64885
+ "not your usage limit",
64886
+ "not your account",
64887
+ "not your account's",
64888
+ "temporarily limiting requests",
64889
+ "temporarily rate",
64890
+ "server is temporarily",
64891
+ "would exceed your account\u2019s rate limit",
64892
+ "would exceed your account's rate limit"
64893
+ ];
64894
+ function isTransientUpstreamSignal(text4) {
64895
+ if (typeof text4 !== "string" || text4.length === 0)
64896
+ return false;
64897
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64898
+ const lower = sample.toLowerCase();
64899
+ return transientUpstreamSignals2.some((s) => lower.includes(s));
64900
+ }
64901
+ function parseResetTime2(text4, parseTimeNow = new Date) {
64902
+ const lower = text4.toLowerCase();
64903
+ const retryAfter = lower.match(/retry[\s-]*after[:\s]+(\d+)\s*(seconds?|s\b|minutes?|m\b|hours?|h\b)?/);
64904
+ if (retryAfter) {
64905
+ const n = Number(retryAfter[1]);
64906
+ if (Number.isFinite(n) && n > 0 && n < 7 * 24 * 3600) {
64907
+ const unit = (retryAfter[2] ?? "seconds").toLowerCase();
64908
+ const ms = unit.startsWith("h") ? n * 3600000 : unit.startsWith("m") ? n * 60000 : n * 1000;
64909
+ return new Date(parseTimeNow.getTime() + ms);
64910
+ }
64911
+ }
64912
+ const relReset = lower.match(/resets?\s+in\s+([0-9hms\s]+)/);
64913
+ if (relReset) {
64914
+ const ms = parseRelativeDuration2(relReset[1]);
64915
+ if (ms != null)
64916
+ return new Date(parseTimeNow.getTime() + ms);
64917
+ }
64918
+ 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/);
64919
+ if (iso) {
64920
+ const d = new Date(iso[0]);
64921
+ if (!Number.isNaN(d.getTime()))
64922
+ return d;
64923
+ }
64924
+ 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)?))?)/);
64925
+ if (calReset) {
64926
+ const candidate = `${calReset[1]} ${parseTimeNow.getUTCFullYear()}`;
64927
+ const d = new Date(candidate);
64928
+ if (!Number.isNaN(d.getTime()))
64929
+ return d;
64930
+ }
64931
+ 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);
64932
+ if (timeOnly) {
64933
+ const d = resolveNextWallClock2(Number(timeOnly[1]), timeOnly[2] ? Number(timeOnly[2]) : 0, timeOnly[3]?.toLowerCase(), timeOnly[4]?.trim(), parseTimeNow);
64934
+ if (d != null)
64935
+ return d;
64936
+ }
64937
+ return;
64938
+ }
64939
+ function resolveNextWallClock2(hour12or24, minute, ampm, tz, nowDate) {
64940
+ let hour = hour12or24;
64941
+ if (ampm === "pm" && hour < 12)
64942
+ hour += 12;
64943
+ if (ampm === "am" && hour === 12)
64944
+ hour = 0;
64945
+ if (!Number.isFinite(hour) || hour > 23 || hour < 0)
64946
+ return;
64947
+ if (!Number.isFinite(minute) || minute > 59 || minute < 0)
64948
+ return;
64949
+ const nowMs2 = nowDate.getTime();
64950
+ const base = new Date(nowMs2);
64951
+ for (let dayOffset = 0;dayOffset <= 2; dayOffset++) {
64952
+ const dateParts = tzDateParts2(new Date(nowMs2 + dayOffset * 86400000), tz);
64953
+ if (dateParts == null)
64954
+ return;
64955
+ const epoch = wallClockToEpoch2(dateParts.year, dateParts.month, dateParts.day, hour, minute, tz);
64956
+ if (epoch != null && epoch > nowMs2)
64957
+ return new Date(epoch);
64958
+ }
64959
+ return;
64960
+ }
64961
+ function tzDateParts2(d, tz) {
64962
+ if (!tz) {
64963
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth(), day: d.getUTCDate() };
64964
+ }
64965
+ try {
64966
+ const fmt = new Intl.DateTimeFormat("en-US", {
64967
+ timeZone: tz,
64968
+ year: "numeric",
64969
+ month: "2-digit",
64970
+ day: "2-digit"
64971
+ });
64972
+ const parts = Object.fromEntries(fmt.formatToParts(d).filter((p) => p.type !== "literal").map((p) => [p.type, p.value]));
64973
+ return {
64974
+ year: Number(parts.year),
64975
+ month: Number(parts.month) - 1,
64976
+ day: Number(parts.day)
64977
+ };
64978
+ } catch {
64979
+ return null;
64980
+ }
64981
+ }
64982
+ function wallClockToEpoch2(year, month, day, hour, minute, tz) {
64983
+ const asUtc = Date.UTC(year, month, day, hour, minute, 0);
64984
+ if (!tz)
64985
+ return asUtc;
64986
+ try {
64987
+ const fmt = new Intl.DateTimeFormat("en-US", {
64988
+ timeZone: tz,
64989
+ year: "numeric",
64990
+ month: "2-digit",
64991
+ day: "2-digit",
64992
+ hour: "2-digit",
64993
+ minute: "2-digit",
64994
+ second: "2-digit",
64995
+ hour12: false
64996
+ });
64997
+ const parts = Object.fromEntries(fmt.formatToParts(new Date(asUtc)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value]));
64998
+ const shown = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour) % 24, Number(parts.minute), Number(parts.second));
64999
+ const offset = shown - asUtc;
65000
+ return asUtc - offset;
65001
+ } catch {
65002
+ return null;
65003
+ }
65004
+ }
65005
+ function parseRelativeDuration2(s) {
65006
+ let total = 0;
65007
+ let matched = false;
65008
+ const re = /(\d+)\s*(h|hours?|m|minutes?|s|seconds?)/g;
65009
+ let m;
65010
+ while ((m = re.exec(s)) != null) {
65011
+ matched = true;
65012
+ const n = Number(m[1]);
65013
+ const unit = m[2].toLowerCase();
65014
+ if (unit.startsWith("h"))
65015
+ total += n * 3600000;
65016
+ else if (unit.startsWith("m"))
65017
+ total += n * 60000;
65018
+ else
65019
+ total += n * 1000;
65020
+ }
65021
+ return matched && total > 0 ? total : null;
65022
+ }
65023
+
65024
+ // throttle-tier.ts
65025
+ var accountScopedThrottleSignals = [
65026
+ "would exceed your account's rate limit",
65027
+ "would exceed your account\u2019s rate limit",
65028
+ "not your account"
65029
+ ];
65030
+ function isAccountScopedThrottle(text4) {
65031
+ if (typeof text4 !== "string" || text4.length === 0)
65032
+ return false;
65033
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
65034
+ const lower = sample.toLowerCase();
65035
+ return accountScopedThrottleSignals.some((s) => lower.includes(s));
65036
+ }
65037
+ var THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT = 5 * 60000;
65038
+ var THROTTLE_DEFAULT_WAIT_MS = 60000;
65039
+ function throttleRetryInPlaceMaxMs(env = process.env) {
65040
+ const raw = env.SWITCHROOM_THROTTLE_RETRY_IN_PLACE_MAX_MS;
65041
+ if (raw == null || raw === "")
65042
+ return THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT;
65043
+ const n = Number(raw);
65044
+ return Number.isFinite(n) && n > 0 ? n : THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT;
65045
+ }
65046
+ function decideThrottleTier(opts) {
65047
+ const { detail, now, thresholdMs } = opts;
65048
+ if (!isAccountScopedThrottle(detail))
65049
+ return { action: "none" };
65050
+ const resetAt = parseResetTime2(detail, new Date(now));
65051
+ const resetAtMs = resetAt?.getTime();
65052
+ if (resetAtMs == null || !Number.isFinite(resetAtMs) || resetAtMs <= now) {
65053
+ return {
65054
+ action: "throttle",
65055
+ throttledUntilMs: now + THROTTLE_DEFAULT_WAIT_MS,
65056
+ resetParsed: false
65057
+ };
65058
+ }
65059
+ if (resetAtMs - now <= thresholdMs) {
65060
+ return { action: "throttle", throttledUntilMs: resetAtMs, resetParsed: true };
65061
+ }
65062
+ return { action: "failover", resetAtMs };
65063
+ }
65064
+ var THROTTLE_NOTICE_COOLDOWN_MS = 10 * 60000;
65065
+
65066
+ // throttle-tier.ts
65067
+ init_card_format();
65068
+ init_quota_check();
65069
+ var THROTTLE_RETRY_IN_PLACE_MAX_MS_DEFAULT2 = 5 * 60000;
65070
+ var THROTTLE_NOTICE_COOLDOWN_MS2 = 10 * 60000;
65071
+ function evaluateThrottleNotice(prev, account, now, cooldownMs = THROTTLE_NOTICE_COOLDOWN_MS2) {
65072
+ const last = prev.lastSentAtMsByAccount[account] ?? 0;
65073
+ if (now - last >= cooldownMs) {
65074
+ return {
65075
+ send: true,
65076
+ next: {
65077
+ lastSentAtMsByAccount: {
65078
+ ...prev.lastSentAtMsByAccount,
65079
+ [account]: now
65080
+ }
65081
+ }
65082
+ };
65083
+ }
65084
+ return { send: false, next: prev };
65085
+ }
65086
+ function renderThrottleNotice(opts) {
65087
+ const now = opts.now ?? new Date;
65088
+ const resetStr = formatResetRelative(new Date(opts.throttledUntilMs), now);
65089
+ const acct = opts.account ? `\`${escapeMarkdown(opts.account)}\`` : "the active account";
65090
+ const lines = [
65091
+ `\uD83D\uDEA6 **Rate-limited, staying put** \u2014 ${acct} hit a transient rate limit on **${escapeMarkdown(opts.agent)}**.`,
65092
+ `This is a short throttle, not a quota wall \u2014 ${opts.resetParsed ? resetStr : `no reset given, retrying in ~60s`}.`,
65093
+ `_Staying on ${acct}; no failover needed. The turn retries automatically after the reset._`
65094
+ ];
65095
+ return lines.join(`
65096
+ `);
65097
+ }
65098
+ function renderThrottleEscalationNotice(opts) {
65099
+ const acct = opts.account ? `\`${escapeMarkdown(opts.account)}\`` : "the active account";
65100
+ 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.`;
65101
+ 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.`;
65102
+ return `${head}
65103
+ ${tail}`;
65104
+ }
65105
+
65106
+ // gateway/throttle-tier-wiring.ts
65107
+ var THROTTLE_RETRY_NUDGE_SLACK_MS = 5000;
65108
+ var THROTTLE_RETRY_NUDGE_JITTER_MAX_MS = 30000;
65109
+ function createThrottleTierRunner(deps) {
65110
+ const now = deps.now ?? (() => Date.now());
65111
+ const schedule = deps.schedule ?? ((fn, ms) => {
65112
+ const t = setTimeout(fn, ms);
65113
+ if (typeof t.unref === "function")
65114
+ t.unref();
65115
+ return { cancel: () => clearTimeout(t) };
65116
+ });
65117
+ const jitterMs = deps.jitterMs ?? (() => Math.floor(Math.random() * THROTTLE_RETRY_NUDGE_JITTER_MAX_MS));
65118
+ let noticeState = { lastSentAtMsByAccount: {} };
65119
+ let pendingNudge = null;
65120
+ async function broadcastDeduped(client3, keyPrefix, account, markdown) {
65121
+ for (const chatId of deps.listNoticeChats()) {
65122
+ let granted = true;
65123
+ if (client3 && account) {
65124
+ try {
65125
+ granted = (await client3.claimNotification(`${keyPrefix}:${account}:${chatId}`, THROTTLE_NOTICE_COOLDOWN_MS2)).granted;
65126
+ } catch {
65127
+ granted = true;
65128
+ }
65129
+ }
65130
+ if (granted) {
65131
+ deps.sendNotice(chatId, markdown);
65132
+ } else {
65133
+ deps.log(`[throttle-tier] notice suppressed (fleet claim) chat=${chatId}`);
65134
+ }
65135
+ }
65136
+ }
65137
+ function nudgeResume(reason, armedAtMs) {
65138
+ const newest = deps.newestActiveTurnStartedAtMs();
65139
+ if (deps.turnInFlight()) {
65140
+ if (newest != null && newest > armedAtMs) {
65141
+ deps.log(`[throttle-tier] resume skipped (superseded by a live newer turn) reason=${reason}`);
65142
+ return;
65143
+ }
65144
+ deps.log(`[throttle-tier] resume deferred to turn-complete reason=${reason}`);
65145
+ deps.deferRestartToTurnComplete(deps.agentName, reason);
65146
+ return;
65147
+ }
65148
+ const verdict = deps.resumeDecide(newest);
65149
+ if (verdict === "resume") {
65150
+ deps.log(`[throttle-tier] resuming dead turn via self-restart reason=${reason}`);
65151
+ deps.restartNow(deps.agentName, reason);
65152
+ } else {
65153
+ deps.log(`[throttle-tier] resume suppressed (${verdict}) reason=${reason}`);
65154
+ }
65155
+ }
65156
+ async function fire(triggerAgent, throttledUntilMs, resetParsed) {
65157
+ const armedAtMs = now();
65158
+ let client3 = null;
65159
+ let account = null;
65160
+ let escalated = false;
65161
+ let rolledTo = null;
65162
+ try {
65163
+ client3 = await deps.getBrokerClient();
65164
+ if (client3) {
65165
+ const r = await client3.markThrottled(throttledUntilMs);
65166
+ account = r.account;
65167
+ escalated = r.escalated;
65168
+ rolledTo = r.rolledTo ?? null;
65169
+ } else {
65170
+ deps.log(`[throttle-tier] broker unreachable \u2014 notice only, no ledger record agent=${triggerAgent}`);
65171
+ }
65172
+ } catch (err) {
65173
+ deps.log(`[throttle-tier] markThrottled failed agent=${triggerAgent}: ${err?.message ?? err}`);
65174
+ }
65175
+ if (escalated) {
65176
+ deps.log(`[throttle-tier] escalated to wall account=${account ?? "?"} ` + `rolledTo=${rolledTo ?? "none (all blocked)"}`);
65177
+ await broadcastDeduped(client3, "throttle-escalation", account, renderThrottleEscalationNotice({ account, agent: triggerAgent, rolledTo }));
65178
+ if (rolledTo)
65179
+ nudgeResume("throttle-escalation-resume", armedAtMs);
65180
+ return;
65181
+ }
65182
+ const cooldownKey = account ?? `agent:${triggerAgent}`;
65183
+ const verdict = evaluateThrottleNotice(noticeState, cooldownKey, now());
65184
+ if (verdict.send) {
65185
+ noticeState = verdict.next;
65186
+ await broadcastDeduped(client3, "throttle-notice", account, renderThrottleNotice({
65187
+ account,
65188
+ agent: triggerAgent,
65189
+ throttledUntilMs,
65190
+ resetParsed,
65191
+ now: new Date(now())
65192
+ }));
65193
+ } else {
65194
+ deps.log(`[throttle-tier] notice suppressed (cooldown) key=${cooldownKey}`);
65195
+ }
65196
+ const delayMs = Math.max(throttledUntilMs - now(), 0) + THROTTLE_RETRY_NUDGE_SLACK_MS + jitterMs();
65197
+ if (pendingNudge)
65198
+ pendingNudge.cancel();
65199
+ pendingNudge = schedule(() => {
65200
+ pendingNudge = null;
65201
+ nudgeResume("throttle-retry-resume", armedAtMs);
65202
+ }, delayMs);
65203
+ }
65204
+ return {
65205
+ fire,
65206
+ inspect: () => ({ noticeState, nudgePending: pendingNudge != null })
65207
+ };
65208
+ }
65209
+
64706
65210
  // auto-fallback-fleet.ts
64707
65211
  init_card_format();
64708
65212
  init_auth_snapshot_format();
@@ -64738,7 +65242,7 @@ async function runFleetAutoFallback(deps) {
64738
65242
  };
64739
65243
  }
64740
65244
  const oldHealth = classifyHealth(oldSnap, now);
64741
- if (oldHealth === "healthy") {
65245
+ if (oldHealth === "healthy" && !deps.rateLimitTrigger) {
64742
65246
  return {
64743
65247
  kind: "no-eligible-target",
64744
65248
  oldLabel: oldSnap.label,
@@ -64759,6 +65263,8 @@ async function runFleetAutoFallback(deps) {
64759
65263
  newQuota: null,
64760
65264
  triggerAgent: deps.triggerAgent,
64761
65265
  fleetSnapshots: snapshots,
65266
+ parsedResetAt: deps.parsedResetAt ?? null,
65267
+ cause: deps.rateLimitTrigger ? "rate-limit" : undefined,
64762
65268
  tz,
64763
65269
  now
64764
65270
  })
@@ -64777,6 +65283,8 @@ async function runFleetAutoFallback(deps) {
64777
65283
  newLabel: rolledTo,
64778
65284
  newQuota,
64779
65285
  triggerAgent: deps.triggerAgent,
65286
+ parsedResetAt: deps.parsedResetAt ?? null,
65287
+ cause: deps.rateLimitTrigger ? "rate-limit" : undefined,
64780
65288
  tz,
64781
65289
  now
64782
65290
  })
@@ -66830,13 +67338,13 @@ function buildFolderPickerCard(input) {
66830
67338
  validateDriveId(folder.id, "folder.id");
66831
67339
  rows.push([
66832
67340
  {
66833
- text: `\u2705 Allow "${truncateName(folder.name, 48)}"`,
67341
+ text: `\u2705 Allow "${truncateName2(folder.name, 48)}"`,
66834
67342
  callback_data: encodeCallback(["drvpick", "grant", input.agent, folder.id])
66835
67343
  }
66836
67344
  ]);
66837
67345
  rows.push([
66838
67346
  {
66839
- text: `\uD83D\uDCC2 Browse "${truncateName(folder.name, 46)}"`,
67347
+ text: `\uD83D\uDCC2 Browse "${truncateName2(folder.name, 46)}"`,
66840
67348
  callback_data: encodeCallback(["drvpick", "enter", input.agent, folder.id])
66841
67349
  }
66842
67350
  ]);
@@ -66885,14 +67393,14 @@ function buildFolderPickerCard(input) {
66885
67393
  return { body, rows };
66886
67394
  }
66887
67395
  function renderBreadcrumb(trail2, leafName) {
66888
- const leaf = truncateName(leafName, 24);
67396
+ const leaf = truncateName2(leafName, 24);
66889
67397
  if (trail2.length === 0)
66890
67398
  return `/${leaf}`;
66891
- const segments = trail2.map((t) => truncateName(t.name, 20));
67399
+ const segments = trail2.map((t) => truncateName2(t.name, 20));
66892
67400
  segments.push(leaf);
66893
67401
  return `/${segments.join("/")}`;
66894
67402
  }
66895
- function truncateName(name, max) {
67403
+ function truncateName2(name, max) {
66896
67404
  if (name.length <= max)
66897
67405
  return name;
66898
67406
  return `${name.slice(0, max - 1)}\u2026`;
@@ -76405,27 +76913,6 @@ function getNestedObj(obj, key) {
76405
76913
  var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS2 = 5 * 60000;
76406
76914
  var cooldownMap2 = new Map;
76407
76915
 
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
76916
  // session-tail.ts
76430
76917
  function sanitizeCwdToProjectName(cwd) {
76431
76918
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -80470,10 +80957,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
80470
80957
  }
80471
80958
 
80472
80959
  // ../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;
80960
+ var VERSION = "0.18.14";
80961
+ var COMMIT_SHA = "b0db04aa";
80962
+ var COMMIT_DATE = "2026-07-12T12:40:07+10:00";
80963
+ var LATEST_PR = 3165;
80477
80964
  var COMMITS_AHEAD_OF_TAG = 0;
80478
80965
 
80479
80966
  // gateway/boot-version.ts
@@ -85140,22 +85627,50 @@ var inboundCoalescer = createInboundCoalescer({
85140
85627
  merge: (entries) => {
85141
85628
  const last = entries[entries.length - 1];
85142
85629
  const { primary, extras } = splitCoalescedAttachments(entries, (e) => e.downloadImage != null || e.attachment != null, coalesceMaxAttachments());
85630
+ const forwardOrigins = dedupeForwardOrigins(entries.map((e) => e.forwardOrigin));
85143
85631
  return {
85144
85632
  text: entries.map((e) => e.text).filter((t) => t.length > 0).join(`
85145
85633
  `),
85146
85634
  ctx: last.ctx,
85147
85635
  downloadImage: primary?.downloadImage,
85148
85636
  attachment: primary?.attachment,
85149
- extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined
85637
+ extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined,
85638
+ forwardOrigins: forwardOrigins.length > 0 ? forwardOrigins : undefined
85150
85639
  };
85151
85640
  },
85152
85641
  onFlush: (key, merged) => {
85153
85642
  bufferedAttachmentKeys.delete(key);
85154
- handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments);
85643
+ handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments, merged.forwardOrigins);
85155
85644
  }
85156
85645
  });
85157
85646
  function emitGatewayOperatorEvent(event) {
85158
85647
  const { agent, kind } = event;
85648
+ let throttleEscalation = null;
85649
+ let escalationFired = false;
85650
+ if (kind === "rate-limited" && isAccountScopedThrottle(event.detail)) {
85651
+ const throttleDecision = decideThrottleTier({
85652
+ detail: event.detail,
85653
+ now: Date.now(),
85654
+ thresholdMs: throttleRetryInPlaceMaxMs()
85655
+ });
85656
+ if (throttleDecision.action === "throttle") {
85657
+ process.stderr.write(`telegram gateway: throttle-tier staying-put agent=${agent} until=${new Date(throttleDecision.throttledUntilMs).toISOString()} parsedReset=${throttleDecision.resetParsed}
85658
+ `);
85659
+ try {
85660
+ recordOperatorEvent(event);
85661
+ } catch {}
85662
+ throttleTierRunner.fire(agent, throttleDecision.throttledUntilMs, throttleDecision.resetParsed);
85663
+ return;
85664
+ }
85665
+ if (throttleDecision.action === "failover") {
85666
+ const resetAt = new Date(throttleDecision.resetAtMs);
85667
+ throttleEscalation = { kind: "rate_limited", resetAt, raw: event.detail };
85668
+ if (wouldFireFleetAutoFallback()) {
85669
+ escalationFired = true;
85670
+ fireFleetAutoFallback(agent, resolveExhaustUntil(resetAt.getTime()), resetAt, "rate-limit");
85671
+ }
85672
+ }
85673
+ }
85159
85674
  if (!shouldEmitOperatorEvent(agent, kind)) {
85160
85675
  process.stderr.write(`telegram gateway: operator-event suppressed (cooldown) agent=${agent} kind=${kind}
85161
85676
  `);
@@ -85167,13 +85682,13 @@ function emitGatewayOperatorEvent(event) {
85167
85682
  process.stderr.write(`telegram gateway: recordOperatorEvent failed agent=${agent} kind=${kind}: ${err.message}
85168
85683
  `);
85169
85684
  }
85170
- const modelUnavailable = resolveModelUnavailableFromOperatorEvent(event);
85685
+ const modelUnavailable = throttleEscalation ?? resolveModelUnavailableFromOperatorEvent(event);
85171
85686
  let renderedText;
85172
85687
  let renderedKeyboard;
85173
85688
  let cardPromisedFallback = false;
85174
85689
  if (modelUnavailable) {
85175
- const isAutoKind = modelUnavailable.kind === "quota_exhausted";
85176
- const willActuallyFire = isAutoKind && wouldFireFleetAutoFallback();
85690
+ const isAutoKind = modelUnavailable.kind === "quota_exhausted" || modelUnavailable.kind === "rate_limited";
85691
+ const willActuallyFire = throttleEscalation != null ? escalationFired : isAutoKind && wouldFireFleetAutoFallback();
85177
85692
  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
85693
  `);
85179
85694
  renderedText = formatModelUnavailableCard(modelUnavailable, agent, {
@@ -85181,9 +85696,9 @@ function emitGatewayOperatorEvent(event) {
85181
85696
  });
85182
85697
  renderedKeyboard = undefined;
85183
85698
  cardPromisedFallback = willActuallyFire;
85184
- if (willActuallyFire) {
85699
+ if (willActuallyFire && throttleEscalation == null) {
85185
85700
  const untilMs = resolveExhaustUntil(modelUnavailable.resetAt?.getTime());
85186
- fireFleetAutoFallback(agent, untilMs);
85701
+ fireFleetAutoFallback(agent, untilMs, modelUnavailable.resetAt);
85187
85702
  }
85188
85703
  } else {
85189
85704
  try {
@@ -86899,7 +87414,7 @@ var ipcServer = createIpcServer({
86899
87414
  const untilMs = resolveExhaustUntil(msg.resetAt);
86900
87415
  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
87416
  `);
86902
- fireFleetAutoFallback(msg.agentName, untilMs);
87417
+ fireFleetAutoFallback(msg.agentName, untilMs, msg.resetAt != null ? new Date(msg.resetAt) : undefined);
86903
87418
  },
86904
87419
  onQueryPendingPermission(client3, msg) {
86905
87420
  const self = process.env.SWITCHROOM_AGENT_NAME;
@@ -90392,7 +90907,13 @@ async function handleInboundCoalesced(ctx, text5, downloadImage, attachment) {
90392
90907
  maybeEarlyAckReaction(ctx, from);
90393
90908
  maybePokeFloorForMidTurnInbound(ctx, from);
90394
90909
  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 });
90910
+ const result = inboundCoalescer.enqueue(key, {
90911
+ text: text5,
90912
+ ctx,
90913
+ downloadImage,
90914
+ attachment,
90915
+ forwardOrigin: parseForwardOrigin(ctx.message?.forward_origin)
90916
+ });
90396
90917
  if (result.bypass)
90397
90918
  return handleInbound(ctx, text5, downloadImage, attachment);
90398
90919
  if (hasAttachment)
@@ -90430,7 +90951,7 @@ function maybePokeFloorForMidTurnInbound(ctx, from) {
90430
90951
  return;
90431
90952
  pokeFloorNow(key, Date.now());
90432
90953
  }
90433
- async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachments) {
90954
+ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachments, coalescedForwardOrigins) {
90434
90955
  markIdleActivity();
90435
90956
  const isTopicMessage = ctx.message?.is_topic_message ?? false;
90436
90957
  const messageThreadId = ctx.message?.message_thread_id;
@@ -91056,6 +91577,9 @@ ${preBlock(write.output)}`;
91056
91577
  const replyToTextRaw = replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? undefined : undefined;
91057
91578
  const replyToText = replyToTextRaw != null ? replyToTextRaw.length > REPLY_TO_TEXT_MAX ? replyToTextRaw.slice(0, REPLY_TO_TEXT_MAX - 1) + "\u2026" : replyToTextRaw : undefined;
91058
91579
  const replyToTextEscaped = formatReplyToText(replyToTextRaw, REPLY_TO_TEXT_MAX);
91580
+ const forwardOrigins = coalescedForwardOrigins ?? dedupeForwardOrigins([parseForwardOrigin(ctx.message?.forward_origin)]);
91581
+ const forwardOriginMeta = buildForwardOriginMeta(forwardOrigins);
91582
+ const primaryForwardOrigin = forwardOrigins[0];
91059
91583
  if (HISTORY_ENABLED) {
91060
91584
  try {
91061
91585
  recordInbound({
@@ -91068,7 +91592,12 @@ ${preBlock(write.output)}`;
91068
91592
  text: effectiveText,
91069
91593
  attachment_kind: attachment?.kind,
91070
91594
  reply_to_message_id: replyToMessageId ?? null,
91071
- reply_to_text: replyToText ?? null
91595
+ reply_to_text: replyToText ?? null,
91596
+ forwarded_from: primaryForwardOrigin?.name ?? null,
91597
+ forwarded_from_type: primaryForwardOrigin?.type ?? null,
91598
+ forwarded_from_id: primaryForwardOrigin?.id != null ? String(primaryForwardOrigin.id) : null,
91599
+ forwarded_date: forwardOriginDateIso(primaryForwardOrigin),
91600
+ forwarded_message_id: primaryForwardOrigin?.messageId ?? null
91072
91601
  });
91073
91602
  } catch (err) {
91074
91603
  process.stderr.write(`telegram gateway: history recordInbound failed: ${err}
@@ -91134,6 +91663,7 @@ ${preBlock(write.output)}`;
91134
91663
  ...imagePath ? { image_path: imagePath } : {},
91135
91664
  ...replyToMessageId != null ? { reply_to_message_id: String(replyToMessageId) } : {},
91136
91665
  ...replyToTextEscaped != null && replyToTextEscaped.length > 0 ? { reply_to_text: replyToTextEscaped } : {},
91666
+ ...forwardOriginMeta,
91137
91667
  ...isQueuedMidTurn || isQueuedPrefix ? { queued: "true" } : {},
91138
91668
  ...isSteering ? { steering: "true" } : {},
91139
91669
  ...priorTurnInProgress ? { prior_turn_in_progress: "true" } : {},
@@ -93238,12 +93768,33 @@ function newestActiveTurnStartedAtMs() {
93238
93768
  function wouldFireFleetAutoFallback() {
93239
93769
  return fleetFallbackGate.wouldFire();
93240
93770
  }
93241
- async function fireFleetAutoFallback(triggerAgent, untilMs) {
93242
- return fleetFallbackGate.fire(() => doFireFleetAutoFallback(triggerAgent, untilMs), (err) => {
93771
+ async function fireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger) {
93772
+ return fleetFallbackGate.fire(() => doFireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger), (err) => {
93243
93773
  process.stderr.write(`telegram gateway: [fleet-fallback] error agent=${triggerAgent}: ${err?.message ?? err}
93244
93774
  `);
93245
93775
  });
93246
93776
  }
93777
+ var throttleTierRunner = createThrottleTierRunner({
93778
+ agentName: process.env.SWITCHROOM_AGENT_NAME ?? "",
93779
+ getBrokerClient: () => getAuthBrokerClient2(process.env.SWITCHROOM_AGENT_NAME ?? ""),
93780
+ listNoticeChats: () => loadAccess().allowFrom,
93781
+ sendNotice: (chat_id, markdown) => {
93782
+ swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(markdown), { disable_notification: true }), { chat_id: String(chat_id), verb: "throttle-tier:notify" });
93783
+ },
93784
+ resumeDecide: (ts) => fleetFallbackResumeGate.decide(ts),
93785
+ newestActiveTurnStartedAtMs,
93786
+ turnInFlight: () => turnInFlightForGate(),
93787
+ deferRestartToTurnComplete: (agentName3, reason) => {
93788
+ process.stderr.write(`telegram gateway: [throttle-tier] restart deferred to turn-complete agent=${agentName3} reason=${reason}
93789
+ `);
93790
+ pendingRestarts.set(agentName3, Date.now());
93791
+ },
93792
+ restartNow: (agentName3, reason) => {
93793
+ triggerSelfRestart(agentName3, reason);
93794
+ },
93795
+ log: (m) => process.stderr.write(`telegram gateway: ${m}
93796
+ `)
93797
+ });
93247
93798
  var fallbackFailureNoticeState = { lastSentAtMs: 0 };
93248
93799
  var fallbackAllBlockedNoticeState = { lastSentAtMs: 0 };
93249
93800
  function broadcastFleetFallbackFailure(triggerAgent, reason) {
@@ -93264,7 +93815,7 @@ function broadcastFleetFallbackFailure(triggerAgent, reason) {
93264
93815
  swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(html), {}), { chat_id, verb: "fleet-fallback:failure-notify" });
93265
93816
  }
93266
93817
  }
93267
- async function doFireFleetAutoFallback(triggerAgent, untilMs) {
93818
+ async function doFireFleetAutoFallback(triggerAgent, untilMs, parsedResetAt, trigger) {
93268
93819
  try {
93269
93820
  const client3 = await getAuthBrokerClient2(triggerAgent);
93270
93821
  if (!client3) {
@@ -93288,7 +93839,9 @@ async function doFireFleetAutoFallback(triggerAgent, untilMs) {
93288
93839
  return { rolledTo: r.rolledTo ?? null, rolled: r.rolled };
93289
93840
  },
93290
93841
  triggerAgent,
93291
- tz
93842
+ tz,
93843
+ parsedResetAt,
93844
+ rateLimitTrigger: trigger === "rate-limit"
93292
93845
  });
93293
93846
  process.stderr.write(`telegram gateway: [fleet-fallback] outcome=${outcome.kind} agent=${triggerAgent}` + (outcome.kind === "switched" ? ` old=${outcome.oldLabel} new=${outcome.newLabel}` : "") + `
93294
93847
  `);