switchroom 0.19.27 → 0.19.28

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 (32) hide show
  1. package/dist/agent-scheduler/index.js +5 -2
  2. package/dist/auth-broker/index.js +129 -8
  3. package/dist/cli/autoaccept-poll.js +225 -17
  4. package/dist/cli/notion-write-pretool.mjs +5 -2
  5. package/dist/cli/switchroom.js +796 -35
  6. package/dist/host-control/main.js +130 -9
  7. package/dist/vault/approvals/kernel-server.js +129 -8
  8. package/dist/vault/broker/server.js +129 -8
  9. package/package.json +3 -2
  10. package/profiles/_base/start.sh.hbs +70 -15
  11. package/telegram-plugin/dist/bridge/bridge.js +1 -0
  12. package/telegram-plugin/dist/gateway/gateway.js +568 -49
  13. package/telegram-plugin/dist/server.js +1 -0
  14. package/telegram-plugin/edit-flood-fuse.ts +230 -27
  15. package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
  16. package/telegram-plugin/gateway/gateway.ts +9 -2
  17. package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
  18. package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
  19. package/telegram-plugin/mcp-credential-failure.ts +459 -0
  20. package/telegram-plugin/operator-events.ts +38 -0
  21. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +58 -1
  22. package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
  23. package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
  24. package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
  25. package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
  26. package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
  27. package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
  28. package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
  29. package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
  30. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
  31. package/vendor/hindsight-memory/settings.json +1 -1
  32. package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
@@ -21944,7 +21944,10 @@ var init_schema = __esm(() => {
21944
21944
  admin_key: exports_external.string().optional().describe("LiteLLM master/admin key used at apply time to provision the team + " + "virtual key. Supports a vault reference (e.g. " + "'vault:litellm/master-key') \u2014 resolution happens at apply time via " + "the vault-broker. Never injected into the agent container."),
21945
21945
  team: exports_external.string().optional().describe("LiteLLM team alias the per-agent key is created under. Defaults to " + "'switchroom' (applied in code, not as a schema default)."),
21946
21946
  small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
21947
- tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
21947
+ tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins)."),
21948
+ max_budget: exports_external.number().positive().optional().describe("HARD spend cap in USD for this agent's virtual key over one " + "`budget_duration` window. LiteLLM refuses the request once the key's " + "tracked spend exceeds it, so a runaway loop costs at most this much " + "before it is stopped. Defaults to " + "DEFAULT_KEY_MAX_BUDGET_USD (see src/litellm/budget.ts) \u2014 deliberately " + "conservative; raise it per-agent rather than removing it. Set 0 or " + "omit `budget_duration` at your own risk: an uncapped key is only as " + "bounded as the upstream account balance."),
21949
+ soft_budget: exports_external.number().positive().optional().describe("ADVISORY spend threshold in USD. LiteLLM keeps serving past it and " + "raises a budget alert instead. Must be < max_budget. NOTE: LiteLLM " + "accepts soft_budget only on POST /key/generate (GenerateKeyRequest); " + "UpdateKeyRequest does NOT carry it, so changing this value only takes " + "effect on a key that is (re)generated, not on an existing one."),
21950
+ budget_duration: exports_external.string().regex(/^\d+(s|m|h|d|mo)$/, "budget_duration must be a LiteLLM duration like '30d', '24h', '1mo'").optional().describe("Rolling window the budget resets on, in LiteLLM duration syntax " + "('30d', '24h', '1mo'). Defaults to DEFAULT_KEY_BUDGET_DURATION. " + "WITHOUT a duration LiteLLM treats max_budget as a LIFETIME cap that " + "never resets \u2014 the key silently dies for good once it is hit.")
21948
21951
  }).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
21949
21952
  HindsightPerOpLlmSchema = exports_external.object({
21950
21953
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
@@ -21963,7 +21966,7 @@ var init_schema = __esm(() => {
21963
21966
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
21964
21967
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
21965
21968
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
21966
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21969
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_MAX_SLOTS reserves out of; " + "unset means upstream's own default), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21967
21970
  });
21968
21971
  MicrosoftWorkspaceConfigSchema = exports_external.object({
21969
21972
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -46601,24 +46604,62 @@ async function applyTapAnnotationEdit(io, text) {
46601
46604
  }
46602
46605
  }
46603
46606
  }
46607
+ function extractCallbackChatId(callbackQuery) {
46608
+ const msg = callbackQuery?.message;
46609
+ const chat = msg?.chat;
46610
+ const id = chat?.id;
46611
+ if (typeof id === "number" && Number.isFinite(id))
46612
+ return String(id);
46613
+ if (typeof id === "string" && id !== "")
46614
+ return id;
46615
+ return;
46616
+ }
46617
+ var DEAD_CARD_NOTICE = "\u26a0\ufe0f Your tap was applied, but this card could not be updated. " + "The buttons on it are STALE \u2014 tapping them again will not change anything. " + "Scroll down for the outcome, or ask the agent to re-send the card.";
46618
+ async function disarmDeadCard(ctx, apiCall, scope, log) {
46619
+ try {
46620
+ await apiCall(() => ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }), { ...scope, verb: "editMessageReplyMarkup" });
46621
+ log("finalizeCallback: repaint failed but the keyboard was stripped \u2014 " + `the card shows stale text and is no longer tappable
46622
+ `);
46623
+ return;
46624
+ } catch (err) {
46625
+ log(`finalizeCallback: editMessageReplyMarkup fallback failed: ${err.message}
46626
+ `);
46627
+ }
46628
+ try {
46629
+ await apiCall(() => ctx.reply(DEAD_CARD_NOTICE, { link_preview_options: { is_disabled: true } }), { ...scope, verb: "sendMessage" });
46630
+ } catch (err) {
46631
+ log(`finalizeCallback: dead-card notice failed: ${err.message}
46632
+ `);
46633
+ }
46634
+ }
46604
46635
  async function finalizeCallback(ctx, opts) {
46605
46636
  const log = opts.log ?? ((line) => process.stderr.write(line));
46606
- ctx.answerCallbackQuery({
46637
+ const apiCall = opts.apiCall;
46638
+ const chatId = extractCallbackChatId(ctx.callbackQuery);
46639
+ const scope = {
46640
+ ...chatId != null ? { chat_id: chatId } : {},
46641
+ priorityClass: "critical"
46642
+ };
46643
+ apiCall(() => ctx.answerCallbackQuery({
46607
46644
  text: opts.ackText,
46608
46645
  ...opts.alert ? { show_alert: true } : {}
46609
- }).catch((err) => {
46646
+ }), { ...scope, verb: "answerCallbackQuery" }).catch((err) => {
46610
46647
  log(`finalizeCallback: answerCallbackQuery failed: ${err.message}
46611
46648
  `);
46612
46649
  });
46650
+ let repainted = true;
46613
46651
  try {
46614
- await ctx.editMessageText(opts.literalText ? opts.newText : { markdown: opts.newText }, {
46652
+ await apiCall(() => ctx.editMessageText(opts.literalText ? opts.newText : { markdown: opts.newText }, {
46615
46653
  reply_markup: { inline_keyboard: [] },
46616
46654
  link_preview_options: { is_disabled: true }
46617
- });
46655
+ }), { ...scope, verb: "editMessageText" });
46618
46656
  } catch (err) {
46657
+ repainted = false;
46619
46658
  log(`finalizeCallback: editMessageText failed: ${err.message}
46620
46659
  `);
46621
46660
  }
46661
+ if (!repainted)
46662
+ await disarmDeadCard(ctx, apiCall, scope, log);
46622
46663
  if (opts.synthInbound != null) {
46623
46664
  try {
46624
46665
  const r = opts.synthInbound();
@@ -50153,6 +50194,7 @@ function createCallbackQueryHandlers(deps) {
50153
50194
 
50154
50195
  \u2705 **${escapeHtmlForTg2(agentName)}** granted read access to ` + `\`${keyName}\` for 30 days ` + `(grant \`${id}\`). ` + `Re-run /vault audit to act on remaining denials.`;
50155
50196
  await finalizeCallback(ctx, {
50197
+ apiCall: robustApiCall,
50156
50198
  ackText: "\u2705 Grant minted",
50157
50199
  newText: baseText ? `${baseText}${statusLine}` : statusLine
50158
50200
  });
@@ -50731,6 +50773,7 @@ _Approver verified by Telegram identity \u2014 broker auto-unlocked at startup._
50731
50773
 
50732
50774
  \u270f\ufe0f **Rename mode** \u2014 send the new key name as your next message. ` + `The current proposed key is \`${pending.key}\`.`;
50733
50775
  await finalizeCallback(ctx, {
50776
+ apiCall: robustApiCall,
50734
50777
  ackText: "Send the new key name as your next message.",
50735
50778
  newText: baseText ? `${baseText}${statusLine}` : statusLine
50736
50779
  });
@@ -51129,6 +51172,7 @@ ${keyList}`,
51129
51172
  const successText = `\u2705 Grant \`${id}\` created. Written to \`~/.switchroom/agents/${escapeHtmlForTg2(state.agent)}/.vault-token\``;
51130
51173
  if (msgId != null) {
51131
51174
  await finalizeCallback(ctx, {
51175
+ apiCall: robustApiCall,
51132
51176
  ackText: "\u2705 Grant created",
51133
51177
  newText: successText
51134
51178
  });
@@ -51405,6 +51449,7 @@ Reply \`rename NEW_NAME\` to relabel.`), { reply_markup: { inline_keyboard: [] }
51405
51449
 
51406
51450
  \u2717 _Dismissed by operator._`;
51407
51451
  await finalizeCallback(ctx, {
51452
+ apiCall: robustApiCall,
51408
51453
  ackText: "Dismissed",
51409
51454
  newText: sourceMsgText ? `${sourceMsgText}${status}` : status
51410
51455
  });
@@ -51417,6 +51462,7 @@ Reply \`rename NEW_NAME\` to relabel.`), { reply_markup: { inline_keyboard: [] }
51417
51462
 
51418
51463
  \uD83D\uDD04 _**${escapeHtmlForTg2(agent)}** restart requested by operator._`;
51419
51464
  await finalizeCallback(ctx, {
51465
+ apiCall: robustApiCall,
51420
51466
  ackText: `Restarting ${agent}\u2026`,
51421
51467
  newText: sourceMsgText ? `${sourceMsgText}${status}` : status
51422
51468
  });
@@ -51433,6 +51479,7 @@ Reply \`rename NEW_NAME\` to relabel.`), { reply_markup: { inline_keyboard: [] }
51433
51479
 
51434
51480
  \uD83D\uDD10 _Reauth started for **${escapeHtmlForTg2(agent)}** \u2014 follow the login URL below._`;
51435
51481
  await finalizeCallback(ctx, {
51482
+ apiCall: robustApiCall,
51436
51483
  ackText: `Starting reauth for ${agent}\u2026`,
51437
51484
  newText: sourceMsgText ? `${sourceMsgText}${status}` : status,
51438
51485
  synthInbound: async () => {
@@ -63318,6 +63365,7 @@ var EDIT_FLOOD_FUSE_DEFAULTS = {
63318
63365
  cosmeticPerChatMaxPerWindow: 6,
63319
63366
  perChatTotalMaxPerWindow: 20,
63320
63367
  perChatReplyReserve: 8,
63368
+ perChatCriticalMinPerWindow: 3,
63321
63369
  perTokenMaxPerWindow: 25,
63322
63370
  perTokenWindowMs: 1000,
63323
63371
  chatActionMaxDeferMs: 3000,
@@ -63335,6 +63383,12 @@ function envInt(raw) {
63335
63383
  const n = Number(raw);
63336
63384
  return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;
63337
63385
  }
63386
+ function envFactor(raw) {
63387
+ if (raw == null || raw.trim() === "")
63388
+ return;
63389
+ const n = Number(raw);
63390
+ return Number.isFinite(n) && n > 0 && n <= 1 ? n : undefined;
63391
+ }
63338
63392
  function editFloodFuseConfigFromEnv(env) {
63339
63393
  const cfg = { enabled: env.SWITCHROOM_EDIT_FUSE !== "0" };
63340
63394
  const assign = (k, v) => {
@@ -63345,8 +63399,11 @@ function editFloodFuseConfigFromEnv(env) {
63345
63399
  assign("cosmeticPerChatMaxPerWindow", envInt(env.SWITCHROOM_FEED_EDIT_MAX_PER_CHAT_PER_MIN));
63346
63400
  assign("perChatTotalMaxPerWindow", envInt(env.SWITCHROOM_CHAT_TOTAL_MAX_PER_MIN));
63347
63401
  assign("perChatReplyReserve", envInt(env.SWITCHROOM_CHAT_REPLY_RESERVE));
63402
+ assign("perChatCriticalMinPerWindow", envInt(env.SWITCHROOM_CHAT_CRITICAL_MIN_PER_MIN));
63348
63403
  assign("perTokenMaxPerWindow", envInt(env.SWITCHROOM_TOKEN_MAX_PER_SEC));
63349
63404
  assign("maxDeferMs", envInt(env.SWITCHROOM_EDIT_FUSE_MAX_DEFER_MS));
63405
+ assign("maxTightenLevel", envInt(env.SWITCHROOM_EDIT_FUSE_MAX_TIGHTEN_LEVEL));
63406
+ assign("tightenFactor", envFactor(env.SWITCHROOM_EDIT_FUSE_TIGHTEN_FACTOR));
63350
63407
  const stateDir = env.TELEGRAM_STATE_DIR;
63351
63408
  if (stateDir != null && stateDir !== "") {
63352
63409
  cfg.floodWaitRemainingMs = makeFloodWaitProbe(floodStatePath(stateDir));
@@ -63367,6 +63424,8 @@ function createEditFloodFuse(config = {}) {
63367
63424
  const perChatTotalMax = config.perChatTotalMaxPerWindow ?? D.perChatTotalMaxPerWindow;
63368
63425
  const perChatWindowMs = config.perChatWindowMs ?? D.perChatWindowMs;
63369
63426
  const perChatReplyReserve = Math.min(Math.max(0, config.perChatReplyReserve ?? D.perChatReplyReserve), Math.max(0, perChatTotalMax - 1));
63427
+ const replyReserveFraction = perChatTotalMax > 0 ? perChatReplyReserve / perChatTotalMax : 0;
63428
+ const perChatCriticalMin = Math.max(0, config.perChatCriticalMinPerWindow ?? D.perChatCriticalMinPerWindow);
63370
63429
  const perTokenMax = Math.max(1, config.perTokenMaxPerWindow ?? D.perTokenMaxPerWindow);
63371
63430
  const perTokenWindowMs = Math.max(1, config.perTokenWindowMs ?? D.perTokenWindowMs);
63372
63431
  const tokenKey = `g:${config.botScopeKey ?? "unknown"}`;
@@ -63412,8 +63471,10 @@ function createEditFloodFuse(config = {}) {
63412
63471
  tightenLevel--;
63413
63472
  tightenedUntil = tightenLevel > 0 ? tightenedUntil + tightenMs : 0;
63414
63473
  }
63415
- if (persistedFloodRemainingMs(now) > 0)
63416
- return maxTightenLevel;
63474
+ const remainingMs = persistedFloodRemainingMs(now);
63475
+ if (remainingMs > 0) {
63476
+ return Math.max(tightenLevel, Math.min(maxTightenLevel, tightenStepFor(remainingMs / 1000)));
63477
+ }
63417
63478
  return tightenLevel;
63418
63479
  }
63419
63480
  function isTightened(now) {
@@ -63425,6 +63486,24 @@ function createEditFloodFuse(config = {}) {
63425
63486
  return base;
63426
63487
  return Math.max(1, Math.floor(base * Math.pow(tightenFactor, level)));
63427
63488
  }
63489
+ function classCeiling(base, cls, now) {
63490
+ const eff = ceiling(base, now);
63491
+ if (cls !== "critical")
63492
+ return eff;
63493
+ return Math.max(eff, Math.min(base, perChatCriticalMin));
63494
+ }
63495
+ function cosmeticTotalMax(now) {
63496
+ const eff = ceiling(perChatTotalMax, now);
63497
+ if (replyReserveFraction <= 0)
63498
+ return eff;
63499
+ const reserve = Math.min(eff, Math.max(1, Math.round(eff * replyReserveFraction)));
63500
+ return Math.max(0, eff - reserve);
63501
+ }
63502
+ function totalMaxFor(cls) {
63503
+ if (cls === "cosmetic")
63504
+ return cosmeticTotalMax;
63505
+ return (now) => classCeiling(perChatTotalMax, cls, now);
63506
+ }
63428
63507
  function win(key) {
63429
63508
  let w = windows.get(key);
63430
63509
  if (w === undefined) {
@@ -63454,6 +63533,9 @@ function createEditFloodFuse(config = {}) {
63454
63533
  }
63455
63534
  function waitFor(w, now, windowMs, max) {
63456
63535
  prune(w, now, windowMs);
63536
+ if (max <= 0) {
63537
+ return w.ts.length > 0 ? Math.max(1, w.ts[0] + windowMs - now) : Math.max(1, windowMs);
63538
+ }
63457
63539
  if (w.ts.length < max)
63458
63540
  return 0;
63459
63541
  return Math.max(1, w.ts[0] + windowMs - now);
@@ -63502,12 +63584,12 @@ function createEditFloodFuse(config = {}) {
63502
63584
  if (i >= 0)
63503
63585
  w.ts.splice(i, 1);
63504
63586
  }
63505
- async function awaitRoom(key, windowMs, max, method, mode, cls, dropGuard, deadline, lateReleaseKey) {
63587
+ async function awaitRoom(key, windowMs, maxFor, method, mode, cls, dropGuard, deadline, lateReleaseKey) {
63506
63588
  const w = win(key);
63507
63589
  let counted = false;
63508
63590
  for (;; ) {
63509
63591
  const now = clock.now();
63510
- const wait = waitFor(w, now, windowMs, ceiling(max, now));
63592
+ const wait = waitFor(w, now, windowMs, maxFor(now));
63511
63593
  if (wait === 0) {
63512
63594
  w.ts.push(now);
63513
63595
  return now;
@@ -63570,15 +63652,16 @@ function createEditFloodFuse(config = {}) {
63570
63652
  evict(now);
63571
63653
  const deadline = now + maxDeferMs;
63572
63654
  const cls = currentOutboundClass() ?? (isChatAction ? "cosmetic" : defaultOutboundClass(isEdit));
63655
+ const tokenMaxFor = (t) => ceiling(perTokenMax, t);
63573
63656
  if (chat == null) {
63574
63657
  counters.chatless++;
63575
- await awaitRoom(tokenKey, perTokenWindowMs, perTokenMax, method, "release", cls, undefined, deadline);
63658
+ await awaitRoom(tokenKey, perTokenWindowMs, tokenMaxFor, method, "release", cls, undefined, deadline);
63576
63659
  return runObserved(next);
63577
63660
  }
63578
63661
  const totalKey = `t:${chat}`;
63579
- const totalMaxFor = (c) => c === "cosmetic" ? Math.max(1, perChatTotalMax - perChatReplyReserve) : perChatTotalMax;
63662
+ const chatTotalMax = totalMaxFor(cls);
63580
63663
  const passToken = async () => {
63581
- await awaitRoom(tokenKey, perTokenWindowMs, perTokenMax, method, "release", cls, undefined, deadline);
63664
+ await awaitRoom(tokenKey, perTokenWindowMs, tokenMaxFor, method, "release", cls, undefined, deadline);
63582
63665
  return runObserved(next);
63583
63666
  };
63584
63667
  if (isEdit && msg != null) {
@@ -63586,7 +63669,7 @@ function createEditFloodFuse(config = {}) {
63586
63669
  const mw = win(msgKey);
63587
63670
  mw.inflight++;
63588
63671
  try {
63589
- const msgSlot = await awaitRoom(msgKey, perMessageWindowMs, cls === "cosmetic" ? cosmeticPerMessageMax : perMessageMax, method, "supersede", cls, undefined, deadline);
63672
+ const msgSlot = await awaitRoom(msgKey, perMessageWindowMs, cls === "cosmetic" ? (t) => ceiling(cosmeticPerMessageMax, t) : (t) => classCeiling(perMessageMax, cls, t), method, "supersede", cls, undefined, deadline);
63590
63673
  if (msgSlot === null)
63591
63674
  return DROPPED_RESULT;
63592
63675
  const dropGuard = () => mw.inflight > 1;
@@ -63597,13 +63680,13 @@ function createEditFloodFuse(config = {}) {
63597
63680
  unreserve(k, at);
63598
63681
  };
63599
63682
  const chatKey2 = `ce:${chat}`;
63600
- const chatSlot = await awaitRoom(chatKey2, perChatWindowMs, cls === "cosmetic" ? cosmeticPerChatMax : perChatEditMax, method, "drop", cls, dropGuard, deadline, lateKey);
63683
+ const chatSlot = await awaitRoom(chatKey2, perChatWindowMs, cls === "cosmetic" ? (t) => ceiling(cosmeticPerChatMax, t) : (t) => classCeiling(perChatEditMax, cls, t), method, "drop", cls, dropGuard, deadline, lateKey);
63601
63684
  if (chatSlot === null) {
63602
63685
  giveBack();
63603
63686
  return DROPPED_RESULT;
63604
63687
  }
63605
63688
  reserved.push([chatKey2, chatSlot]);
63606
- const totalSlot = await awaitRoom(totalKey, perChatWindowMs, totalMaxFor(cls), method, cls === "cosmetic" ? "drop" : "release", cls, dropGuard, deadline, lateKey);
63689
+ const totalSlot = await awaitRoom(totalKey, perChatWindowMs, chatTotalMax, method, cls === "cosmetic" ? "drop" : "release", cls, dropGuard, deadline, lateKey);
63607
63690
  if (totalSlot === null) {
63608
63691
  giveBack();
63609
63692
  return DROPPED_RESULT;
@@ -63614,19 +63697,19 @@ function createEditFloodFuse(config = {}) {
63614
63697
  }
63615
63698
  }
63616
63699
  if (isSend) {
63617
- await awaitRoom(`cs:${chat}`, perChatWindowMs, perChatSendMax, method, "release", cls, undefined, deadline);
63618
- await awaitRoom(totalKey, perChatWindowMs, totalMaxFor(cls), method, "release", cls, undefined, deadline);
63700
+ await awaitRoom(`cs:${chat}`, perChatWindowMs, (t) => classCeiling(perChatSendMax, cls, t), method, "release", cls, undefined, deadline);
63701
+ await awaitRoom(totalKey, perChatWindowMs, chatTotalMax, method, "release", cls, undefined, deadline);
63619
63702
  return passToken();
63620
63703
  }
63621
63704
  counters.meteredByDefault++;
63622
63705
  if (isChatAction) {
63623
63706
  const actionDeadline = Math.min(deadline, now + chatActionMaxDeferMs);
63624
- const slot = await awaitRoom(totalKey, perChatWindowMs, totalMaxFor(cls), method, "drop", cls, undefined, actionDeadline);
63707
+ const slot = await awaitRoom(totalKey, perChatWindowMs, chatTotalMax, method, "drop", cls, undefined, actionDeadline);
63625
63708
  if (slot === null)
63626
63709
  return DROPPED_RESULT;
63627
63710
  return passToken();
63628
63711
  }
63629
- await awaitRoom(totalKey, perChatWindowMs, totalMaxFor(cls), method, "release", cls, undefined, deadline);
63712
+ await awaitRoom(totalKey, perChatWindowMs, chatTotalMax, method, "release", cls, undefined, deadline);
63630
63713
  return passToken();
63631
63714
  }
63632
63715
  async function runObserved(next) {
@@ -63657,6 +63740,8 @@ function createEditFloodFuse(config = {}) {
63657
63740
  perMessageCeiling: ceiling(perMessageMax, now),
63658
63741
  cosmeticPerMessageCeiling: ceiling(cosmeticPerMessageMax, now),
63659
63742
  cosmeticPerChatCeiling: ceiling(cosmeticPerChatMax, now),
63743
+ cosmeticPerChatTotalCeiling: cosmeticTotalMax(now),
63744
+ criticalPerChatTotalCeiling: classCeiling(perChatTotalMax, "critical", now),
63660
63745
  meteredByDefault: counters.meteredByDefault,
63661
63746
  chatless: counters.chatless,
63662
63747
  perTokenCeiling: ceiling(perTokenMax, now),
@@ -70869,6 +70954,7 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
70869
70954
  "credentials-invalid",
70870
70955
  "credit-exhausted",
70871
70956
  "provider-credit-exhausted",
70957
+ "mcp-dependency-blocked",
70872
70958
  "proxy-misconfig"
70873
70959
  ]);
70874
70960
 
@@ -73898,6 +73984,19 @@ function renderOperatorEvent(ev) {
73898
73984
  }
73899
73985
  };
73900
73986
  }
73987
+ case "mcp-dependency-blocked":
73988
+ return {
73989
+ text: [
73990
+ `\uD83D\uDD0C **Paid dependency blocked**`,
73991
+ stripRawErrorBytes(ev.detail)
73992
+ ].filter(Boolean).join(`
73993
+ `),
73994
+ keyboard: {
73995
+ inline_keyboard: [
73996
+ [{ text: "\u274c Dismiss", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
73997
+ ]
73998
+ }
73999
+ };
73901
74000
  case "quota-exhausted":
73902
74001
  return {
73903
74002
  text: [
@@ -74049,6 +74148,7 @@ var OPERATOR_ACTIONABLE_KINDS2 = new Set([
74049
74148
  "credentials-invalid",
74050
74149
  "credit-exhausted",
74051
74150
  "provider-credit-exhausted",
74151
+ "mcp-dependency-blocked",
74052
74152
  "proxy-misconfig"
74053
74153
  ]);
74054
74154
  function isOperatorActionableKind(kind) {
@@ -74067,6 +74167,265 @@ function renderUserFacingFailureNotice() {
74067
74167
  return "\u26a0\ufe0f Sorry \u2014 I couldn't complete that just now. It's a problem on our side, not anything you did. Please try again shortly.";
74068
74168
  }
74069
74169
 
74170
+ // mcp-credential-failure.ts
74171
+ var MCP_SERVER_VAULT_KEYS = {
74172
+ eraser: { label: "Eraser", vaultKey: "eraser/api-key", consoleUrl: "https://app.eraser.io/settings/api" },
74173
+ brevo: { label: "Brevo", vaultKey: "brevo/api-key", consoleUrl: "https://app.brevo.com/settings/keys/api" },
74174
+ postiz: { label: "Postiz", vaultKey: "postiz/api-key", consoleUrl: "https://platform.postiz.com/settings" },
74175
+ "meta-ads": { label: "Meta Ads", vaultKey: "meta-ads/access-token", consoleUrl: "https://business.facebook.com/settings" },
74176
+ "google-ads": { label: "Google Ads", vaultKey: "google-ads/developer-token", consoleUrl: "https://ads.google.com/aw/apicenter" },
74177
+ cloudflare: { label: "Cloudflare", vaultKey: "cloudflare/api-token", consoleUrl: "https://dash.cloudflare.com/profile/api-tokens" },
74178
+ context7: { label: "Context7", vaultKey: "context7/api-key", consoleUrl: "https://context7.com/dashboard" }
74179
+ };
74180
+ function parseMcpServerFromToolName(toolName) {
74181
+ if (typeof toolName !== "string")
74182
+ return null;
74183
+ if (!toolName.startsWith("mcp__"))
74184
+ return null;
74185
+ const rest = toolName.slice("mcp__".length);
74186
+ const end = rest.indexOf("__");
74187
+ const server = end === -1 ? rest : rest.slice(0, end);
74188
+ return server.length > 0 ? server : null;
74189
+ }
74190
+ function describeMcpServer(server) {
74191
+ const direct = MCP_SERVER_VAULT_KEYS[server];
74192
+ if (direct != null)
74193
+ return direct;
74194
+ const entry = PROVIDER_CREDIT_REGISTRY.find((e) => e.id === server);
74195
+ if (entry != null) {
74196
+ return { label: entry.label, vaultKey: entry.vaultKey, consoleUrl: entry.consoleUrl };
74197
+ }
74198
+ return null;
74199
+ }
74200
+ function isAlertingClass(c) {
74201
+ return c !== "ordinary";
74202
+ }
74203
+ var CLASS_RULES = [
74204
+ {
74205
+ cls: "credential",
74206
+ statuses: [401, 403],
74207
+ signals: [
74208
+ "invalid api key",
74209
+ "invalid_api_key",
74210
+ "incorrect api key",
74211
+ "invalid authentication",
74212
+ "authentication_error",
74213
+ "authentication failed",
74214
+ "unauthorized",
74215
+ "unauthorised",
74216
+ "forbidden",
74217
+ "api key not found",
74218
+ "no api key provided",
74219
+ "missing api key",
74220
+ "api key expired",
74221
+ "key has been disabled",
74222
+ "key has been revoked",
74223
+ "key is disabled",
74224
+ "account has been blocked",
74225
+ "account is blocked",
74226
+ "account suspended",
74227
+ "permission_denied",
74228
+ "token expired",
74229
+ "invalid token"
74230
+ ]
74231
+ },
74232
+ {
74233
+ cls: "quota",
74234
+ statuses: [],
74235
+ signals: [
74236
+ "quota exceeded",
74237
+ "quota_exceeded",
74238
+ "over quota",
74239
+ "usage limit reached",
74240
+ "usage limit exceeded",
74241
+ "monthly limit",
74242
+ "monthly quota",
74243
+ "plan limit",
74244
+ "plan_limit",
74245
+ "limit reached for your plan",
74246
+ "upgrade your plan",
74247
+ "spending limit"
74248
+ ]
74249
+ }
74250
+ ];
74251
+ var TRANSIENT_SIGNALS = [
74252
+ "rate limit",
74253
+ "rate_limit",
74254
+ "too many requests",
74255
+ "retry after",
74256
+ "retry-after",
74257
+ "slow down",
74258
+ "timeout",
74259
+ "timed out",
74260
+ "etimedout",
74261
+ "econnreset",
74262
+ "econnrefused",
74263
+ "socket hang up",
74264
+ "network error",
74265
+ "service unavailable",
74266
+ "bad gateway",
74267
+ "temporarily unavailable"
74268
+ ];
74269
+ var MAX_SCAN_CHARS2 = 16384;
74270
+ function sample2(text4) {
74271
+ if (typeof text4 !== "string" || text4.length === 0)
74272
+ return "";
74273
+ return (text4.length > MAX_SCAN_CHARS2 ? text4.slice(0, MAX_SCAN_CHARS2) : text4).toLowerCase();
74274
+ }
74275
+ function extractHttpStatus(text4) {
74276
+ const lower = sample2(text4);
74277
+ if (lower.length === 0)
74278
+ return null;
74279
+ const m = /\b(?:http[ _-]?status|status[ _-]?code|statuscode|status|http|code|error)\b\D{0,4}\b([1-5]\d\d)\b/.exec(lower) ?? /\b([1-5]\d\d)\s+(?:unauthorized|unauthorised|forbidden|payment required|too many requests)\b/.exec(lower);
74280
+ if (m == null)
74281
+ return null;
74282
+ const n = Number(m[1]);
74283
+ return Number.isFinite(n) ? n : null;
74284
+ }
74285
+ function classifyMcpFailure(text4, status) {
74286
+ const lower = sample2(text4);
74287
+ const httpStatus = typeof status === "number" ? status : extractHttpStatus(text4);
74288
+ if (isProviderCreditStatus(httpStatus) || hasCreditExhaustionWording(lower))
74289
+ return "credit";
74290
+ for (const rule of CLASS_RULES) {
74291
+ if (rule.signals.some((s) => lower.includes(s)))
74292
+ return rule.cls;
74293
+ }
74294
+ if (TRANSIENT_SIGNALS.some((s) => lower.includes(s)))
74295
+ return "ordinary";
74296
+ for (const rule of CLASS_RULES) {
74297
+ if (httpStatus != null && rule.statuses.includes(httpStatus))
74298
+ return rule.cls;
74299
+ }
74300
+ return "ordinary";
74301
+ }
74302
+ var RENOTIFY_MS = 6 * 60 * 60 * 1000;
74303
+
74304
+ class McpFailureLedger {
74305
+ renotifyMs;
74306
+ rows = new Map;
74307
+ constructor(renotifyMs = RENOTIFY_MS) {
74308
+ this.renotifyMs = renotifyMs;
74309
+ }
74310
+ note(input) {
74311
+ if (!isAlertingClass(input.cls))
74312
+ return null;
74313
+ const cls = input.cls;
74314
+ const key = `${input.server}::${cls}`;
74315
+ const row = this.rows.get(key) ?? { agents: new Set, occurrences: 0, lastAlertAt: -Infinity };
74316
+ row.agents.add(input.agent);
74317
+ row.occurrences += 1;
74318
+ this.rows.set(key, row);
74319
+ const due = input.now - row.lastAlertAt >= this.renotifyMs;
74320
+ if (!due)
74321
+ return null;
74322
+ const renotify = Number.isFinite(row.lastAlertAt);
74323
+ const meta = describeMcpServer(input.server);
74324
+ const alert = {
74325
+ server: input.server,
74326
+ label: meta?.label ?? input.server,
74327
+ vaultKey: meta?.vaultKey ?? `${input.server}/api-key`,
74328
+ consoleUrl: meta?.consoleUrl ?? "",
74329
+ cls,
74330
+ agents: [...row.agents].sort(),
74331
+ occurrences: row.occurrences,
74332
+ renotify
74333
+ };
74334
+ row.lastAlertAt = input.now;
74335
+ row.agents = new Set;
74336
+ row.occurrences = 0;
74337
+ return alert;
74338
+ }
74339
+ size() {
74340
+ return this.rows.size;
74341
+ }
74342
+ }
74343
+ var PENDING_TOOL_NAMES_MAX = 512;
74344
+
74345
+ class McpFailureWatcher {
74346
+ pending = new Map;
74347
+ ledger;
74348
+ constructor(renotifyMs = RENOTIFY_MS) {
74349
+ this.ledger = new McpFailureLedger(renotifyMs);
74350
+ }
74351
+ onToolUse(toolUseId, toolName) {
74352
+ if (typeof toolUseId !== "string" || toolUseId.length === 0)
74353
+ return;
74354
+ if (parseMcpServerFromToolName(toolName) == null)
74355
+ return;
74356
+ if (this.pending.size >= PENDING_TOOL_NAMES_MAX) {
74357
+ const oldest = this.pending.keys().next();
74358
+ if (!oldest.done)
74359
+ this.pending.delete(oldest.value);
74360
+ }
74361
+ this.pending.set(toolUseId, toolName);
74362
+ }
74363
+ onToolResult(input) {
74364
+ const id = typeof input.toolUseId === "string" ? input.toolUseId : "";
74365
+ const toolName = id.length > 0 ? this.pending.get(id) : undefined;
74366
+ if (id.length > 0)
74367
+ this.pending.delete(id);
74368
+ if (input.isError !== true)
74369
+ return null;
74370
+ if (toolName == null)
74371
+ return null;
74372
+ const server = parseMcpServerFromToolName(toolName);
74373
+ if (server == null)
74374
+ return null;
74375
+ const cls = classifyMcpFailure(input.errorText);
74376
+ return this.ledger.note({ server, agent: input.agent, cls, now: input.now });
74377
+ }
74378
+ }
74379
+ var CLASS_HEADLINE = {
74380
+ credit: "is out of credit",
74381
+ credential: "key is being rejected",
74382
+ quota: "has hit its usage quota"
74383
+ };
74384
+ var CLASS_ACTION = {
74385
+ credit: "Top up the balance",
74386
+ credential: "Re-issue the key and update the vault entry",
74387
+ quota: "Raise the plan limit or wait for the quota to reset"
74388
+ };
74389
+ function renderMcpFailureDetail(alert) {
74390
+ const agents = alert.agents.length > 0 ? alert.agents.map((a) => a.replace(/[^A-Za-z0-9._-]/g, "")).filter(Boolean).join(", ") : "unknown agent";
74391
+ const console_ = alert.consoleUrl.length > 0 ? ` \u2192 ${alert.consoleUrl}` : "";
74392
+ const repeat = alert.renotify ? " (still failing)" : "";
74393
+ const times = alert.occurrences === 1 ? "1 failure" : `${alert.occurrences} failures`;
74394
+ return `${alert.label} ${CLASS_HEADLINE[alert.cls]}${repeat} \u2014 ${times}, affecting: ${agents}. ` + `${CLASS_ACTION[alert.cls]}. Vault key: \`${alert.vaultKey}\`${console_}`;
74395
+ }
74396
+
74397
+ // gateway/mcp-failure-hook.ts
74398
+ function createMcpFailureHook(deps) {
74399
+ const watcher = new McpFailureWatcher;
74400
+ const clock = deps.now ?? (() => Date.now());
74401
+ return function noteMcpDependencyFailure(ev) {
74402
+ try {
74403
+ if (ev.kind === "tool_use") {
74404
+ watcher.onToolUse(ev.toolUseId, ev.toolName);
74405
+ return;
74406
+ }
74407
+ if (ev.kind !== "tool_result")
74408
+ return;
74409
+ const alert = watcher.onToolResult({
74410
+ toolUseId: ev.toolUseId,
74411
+ isError: ev.isError,
74412
+ errorText: ev.errorText,
74413
+ agent: deps.agent,
74414
+ now: clock()
74415
+ });
74416
+ if (alert == null)
74417
+ return;
74418
+ deps.emit({
74419
+ kind: "mcp-dependency-blocked",
74420
+ agent: deps.agent,
74421
+ detail: renderMcpFailureDetail(alert),
74422
+ suggestedActions: [],
74423
+ firstSeenAt: new Date(clock())
74424
+ });
74425
+ } catch {}
74426
+ };
74427
+ }
74428
+
74070
74429
  // pending-user-notice.ts
74071
74430
  var PENDING_USER_NOTICE_TTL_MS = 10 * 60000;
74072
74431
 
@@ -74120,8 +74479,8 @@ var accountScopedThrottleSignals = [
74120
74479
  function isAccountScopedThrottle(text4) {
74121
74480
  if (typeof text4 !== "string" || text4.length === 0)
74122
74481
  return false;
74123
- const sample2 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
74124
- const lower = sample2.toLowerCase();
74482
+ const sample3 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
74483
+ const lower = sample3.toLowerCase();
74125
74484
  return accountScopedThrottleSignals.some((s) => lower.includes(s));
74126
74485
  }
74127
74486
  function classify429Detail(text4) {
@@ -74458,8 +74817,8 @@ var litellmV3LimiterSignalPair2 = ["rate limit exceeded for ", "limit type:"];
74458
74817
  function isLitellmProxyLocal4292(text4) {
74459
74818
  if (typeof text4 !== "string" || text4.length === 0)
74460
74819
  return false;
74461
- const sample2 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
74462
- const lower = sample2.toLowerCase();
74820
+ const sample3 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
74821
+ const lower = sample3.toLowerCase();
74463
74822
  if (litellmProxyLocal429Signals2.some((s) => lower.includes(s)))
74464
74823
  return true;
74465
74824
  return litellmV3LimiterSignalPair2.every((s) => lower.includes(s));
@@ -74467,14 +74826,14 @@ function isLitellmProxyLocal4292(text4) {
74467
74826
  function detectModelUnavailable2(stderr) {
74468
74827
  if (typeof stderr !== "string" || stderr.length === 0)
74469
74828
  return null;
74470
- const sample2 = stderr.length > 16384 ? stderr.slice(0, 16384) : stderr;
74471
- const lower = sample2.toLowerCase();
74829
+ const sample3 = stderr.length > 16384 ? stderr.slice(0, 16384) : stderr;
74830
+ const lower = sample3.toLowerCase();
74472
74831
  if (transientUpstreamSignals2.some((s) => lower.includes(s))) {
74473
- const resetAt = parseResetTime2(sample2);
74832
+ const resetAt = parseResetTime2(sample3);
74474
74833
  return resetAt !== undefined ? { kind: "overload", resetAt, raw: stderr } : { kind: "overload", raw: stderr };
74475
74834
  }
74476
- if (isLitellmProxyLocal4292(sample2)) {
74477
- const resetAt = parseResetTime2(sample2);
74835
+ if (isLitellmProxyLocal4292(sample3)) {
74836
+ const resetAt = parseResetTime2(sample3);
74478
74837
  return resetAt !== undefined ? { kind: "overload", resetAt, raw: stderr } : { kind: "overload", raw: stderr };
74479
74838
  }
74480
74839
  const quotaSignals = [
@@ -74494,7 +74853,7 @@ function detectModelUnavailable2(stderr) {
74494
74853
  "session cap"
74495
74854
  ];
74496
74855
  if (quotaSignals.some((s) => lower.includes(s))) {
74497
- const resetAt = parseResetTime2(sample2);
74856
+ const resetAt = parseResetTime2(sample3);
74498
74857
  return resetAt !== undefined ? { kind: "quota_exhausted", resetAt, raw: stderr } : { kind: "quota_exhausted", raw: stderr };
74499
74858
  }
74500
74859
  const overloadSignals = [
@@ -74514,7 +74873,7 @@ function detectModelUnavailable2(stderr) {
74514
74873
  " 529 "
74515
74874
  ];
74516
74875
  if (overloadSignals.some((s) => lower.includes(s))) {
74517
- const resetAt = parseResetTime2(sample2);
74876
+ const resetAt = parseResetTime2(sample3);
74518
74877
  return resetAt !== undefined ? { kind: "overload", resetAt, raw: stderr } : { kind: "overload", raw: stderr };
74519
74878
  }
74520
74879
  const networkSignals = [
@@ -74715,8 +75074,8 @@ var accountScopedThrottleSignals2 = [
74715
75074
  function isAccountScopedThrottle2(text4) {
74716
75075
  if (typeof text4 !== "string" || text4.length === 0)
74717
75076
  return false;
74718
- const sample2 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
74719
- const lower = sample2.toLowerCase();
75077
+ const sample3 = text4.length > 16384 ? text4.slice(0, 16384) : text4;
75078
+ const lower = sample3.toLowerCase();
74720
75079
  return accountScopedThrottleSignals2.some((s) => lower.includes(s));
74721
75080
  }
74722
75081
  function classify429Detail2(text4) {
@@ -75795,8 +76154,8 @@ function resolveYear(parsed, todayY, todayIdx) {
75795
76154
  }
75796
76155
  return best;
75797
76156
  }
75798
- function matchCase(sample2, replacement) {
75799
- if (sample2.length > 0 && sample2[0] === sample2[0].toUpperCase() && sample2[0] !== sample2[0].toLowerCase()) {
76157
+ function matchCase(sample3, replacement) {
76158
+ if (sample3.length > 0 && sample3[0] === sample3[0].toUpperCase() && sample3[0] !== sample3[0].toLowerCase()) {
75800
76159
  return replacement.charAt(0).toUpperCase() + replacement.slice(1);
75801
76160
  }
75802
76161
  return replacement;
@@ -80873,24 +81232,62 @@ function createNarrativeLane(deps) {
80873
81232
  // inline-keyboard-callbacks.ts
80874
81233
  var AGENT_CALLBACK_PREFIX2 = "agent:";
80875
81234
  var AGENT_CALLBACK_DATA_MAX2 = 64 - AGENT_CALLBACK_PREFIX2.length;
81235
+ function extractCallbackChatId2(callbackQuery) {
81236
+ const msg = callbackQuery?.message;
81237
+ const chat = msg?.chat;
81238
+ const id = chat?.id;
81239
+ if (typeof id === "number" && Number.isFinite(id))
81240
+ return String(id);
81241
+ if (typeof id === "string" && id !== "")
81242
+ return id;
81243
+ return;
81244
+ }
81245
+ var DEAD_CARD_NOTICE2 = "\u26a0\ufe0f Your tap was applied, but this card could not be updated. " + "The buttons on it are STALE \u2014 tapping them again will not change anything. " + "Scroll down for the outcome, or ask the agent to re-send the card.";
81246
+ async function disarmDeadCard2(ctx, apiCall, scope, log) {
81247
+ try {
81248
+ await apiCall(() => ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }), { ...scope, verb: "editMessageReplyMarkup" });
81249
+ log("finalizeCallback: repaint failed but the keyboard was stripped \u2014 " + `the card shows stale text and is no longer tappable
81250
+ `);
81251
+ return;
81252
+ } catch (err) {
81253
+ log(`finalizeCallback: editMessageReplyMarkup fallback failed: ${err.message}
81254
+ `);
81255
+ }
81256
+ try {
81257
+ await apiCall(() => ctx.reply(DEAD_CARD_NOTICE2, { link_preview_options: { is_disabled: true } }), { ...scope, verb: "sendMessage" });
81258
+ } catch (err) {
81259
+ log(`finalizeCallback: dead-card notice failed: ${err.message}
81260
+ `);
81261
+ }
81262
+ }
80876
81263
  async function finalizeCallback2(ctx, opts) {
80877
81264
  const log = opts.log ?? ((line) => process.stderr.write(line));
80878
- ctx.answerCallbackQuery({
81265
+ const apiCall = opts.apiCall;
81266
+ const chatId = extractCallbackChatId2(ctx.callbackQuery);
81267
+ const scope = {
81268
+ ...chatId != null ? { chat_id: chatId } : {},
81269
+ priorityClass: "critical"
81270
+ };
81271
+ apiCall(() => ctx.answerCallbackQuery({
80879
81272
  text: opts.ackText,
80880
81273
  ...opts.alert ? { show_alert: true } : {}
80881
- }).catch((err) => {
81274
+ }), { ...scope, verb: "answerCallbackQuery" }).catch((err) => {
80882
81275
  log(`finalizeCallback: answerCallbackQuery failed: ${err.message}
80883
81276
  `);
80884
81277
  });
81278
+ let repainted = true;
80885
81279
  try {
80886
- await ctx.editMessageText(opts.literalText ? opts.newText : { markdown: opts.newText }, {
81280
+ await apiCall(() => ctx.editMessageText(opts.literalText ? opts.newText : { markdown: opts.newText }, {
80887
81281
  reply_markup: { inline_keyboard: [] },
80888
81282
  link_preview_options: { is_disabled: true }
80889
- });
81283
+ }), { ...scope, verb: "editMessageText" });
80890
81284
  } catch (err) {
81285
+ repainted = false;
80891
81286
  log(`finalizeCallback: editMessageText failed: ${err.message}
80892
81287
  `);
80893
81288
  }
81289
+ if (!repainted)
81290
+ await disarmDeadCard2(ctx, apiCall, scope, log);
80894
81291
  if (opts.synthInbound != null) {
80895
81292
  try {
80896
81293
  const r = opts.synthInbound();
@@ -84017,7 +84414,13 @@ var HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2;
84017
84414
  var HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low";
84018
84415
  var HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4;
84019
84416
  var HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1;
84020
- var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1;
84417
+ function hindsightConsolidationLlmMaxConcurrentDefault(globalMaxConcurrent = HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT, retainMaxConcurrent = HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT) {
84418
+ const globalCap = Number.isFinite(globalMaxConcurrent) && globalMaxConcurrent >= 1 ? Math.floor(globalMaxConcurrent) : HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT;
84419
+ const retainCap = Number.isFinite(retainMaxConcurrent) && retainMaxConcurrent >= 0 ? Math.floor(retainMaxConcurrent) : HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT;
84420
+ const headroomBound = Math.max(1, globalCap - 1);
84421
+ return Math.min(headroomBound, Math.max(1, globalCap - retainCap - 1));
84422
+ }
84423
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = hindsightConsolidationLlmMaxConcurrentDefault();
84021
84424
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true";
84022
84425
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128;
84023
84426
  var HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true";
@@ -84032,6 +84435,8 @@ var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
84032
84435
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
84033
84436
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
84034
84437
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
84438
+ var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
84439
+ var HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30;
84035
84440
  var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
84036
84441
  [
84037
84442
  "HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
@@ -84085,6 +84490,14 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
84085
84490
  [
84086
84491
  "HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND",
84087
84492
  String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND)
84493
+ ],
84494
+ [
84495
+ "HINDSIGHT_API_RECENCY_DECAY_FUNCTION",
84496
+ HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION
84497
+ ],
84498
+ [
84499
+ "HINDSIGHT_API_RECENCY_DECAY_HALFLIFE_DAYS",
84500
+ String(HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS)
84088
84501
  ]
84089
84502
  ];
84090
84503
  var HINDSIGHT_PERF_DEFAULTS_GPU = [
@@ -84109,7 +84522,9 @@ var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
84109
84522
  ];
84110
84523
  var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
84111
84524
  "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
84112
- "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP"
84525
+ "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
84526
+ "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
84527
+ "HINDSIGHT_API_WORKER_MAX_SLOTS"
84113
84528
  ]);
84114
84529
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
84115
84530
  ...[
@@ -84175,6 +84590,16 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
84175
84590
  ` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
84176
84591
  ` + ` system's self-reports are not memories.
84177
84592
  ` + `- Restatements of the user's current request or the task in progress.
84593
+ ` + `- Volatile state written as a timeless assertion. A version, count, size,
84594
+ ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
84595
+ ` + ` claim is true only at the instant it was said. Concretely, never produce a
84596
+ ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
84597
+ ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
84598
+ ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
84599
+ ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
84600
+ ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
84601
+ ` + ` cannot date it, drop it. An undated one is recalled forever as though it
84602
+ ` + ` were still true, which is worse than not remembering it at all.
84178
84603
  ` + `- Transient state (unread counts, build status, what is running right now) unless
84179
84604
  ` + ` the fact is explicitly dated, in which case record it as a dated observation.
84180
84605
  ` + `- Greetings, acknowledgements, and routine operational chatter.
@@ -84196,18 +84621,108 @@ var SUPERSEDED_RETAIN_MISSIONS = [
84196
84621
  ` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
84197
84622
  ` + `- Greetings, acknowledgements, and routine operational chatter.
84198
84623
 
84199
- ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list."
84624
+ ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.",
84625
+ `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
84626
+ ` + `
84627
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
84628
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
84629
+ ` + `the location where some output was written? If yes, drop it \u2014 it is transcript
84630
+ ` + `exhaust, not memory.
84631
+ ` + `
84632
+ ` + `NEVER extract:
84633
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
84634
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
84635
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
84636
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
84637
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
84638
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
84639
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
84640
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
84641
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
84642
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
84643
+ ` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
84644
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
84645
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
84646
+ ` + ` reset assistant state").
84647
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
84648
+ ` + ` system's self-reports are not memories.
84649
+ ` + `- Restatements of the user's current request or the task in progress.
84650
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
84651
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
84652
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
84653
+ ` + `
84654
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
84655
+ ` + "nothing durable remains, return an empty facts list."
84656
+ ];
84657
+ var DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
84658
+ ` + `
84659
+ ` + `The test is durability, not notability: an observation must still be worth reading weeks from now. A single dated event belongs in an observation only when it establishes or changes a standing fact.
84660
+ ` + `
84661
+ ` + `Do NOT synthesise observations from:
84662
+ ` + `- Transcript exhaust \u2014 tool calls and their results, file paths, temp or scratchpad directories, where some output was written, or narration of what an assistant did.
84663
+ ` + `- Identifiers with no standing meaning: session, agent, request, batch or command IDs, UUIDs, hashes, error codes.
84664
+ ` + `- In-flight process narration \u2014 a task started, paused, or still running. Record the outcome once it lands, not the running state.
84665
+ ` + `- The memory system's own errors, retries, backlogs, or internal state.
84666
+ ` + `- Transient state (what is running right now, unread counts, build status) unless the fact is explicitly dated, in which case record it as dated.
84667
+ ` + `
84668
+ ` + "If the new facts contain nothing durable, record nothing rather than synthesising a weak observation.";
84669
+ var SUPERSEDED_OBSERVATIONS_MISSIONS = [
84670
+ "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
84200
84671
  ];
84201
84672
  var PROFILE_MEMORY_DEFAULTS = {
84202
84673
  "health-coach": {
84203
84674
  disposition: { skepticism: 2, literalism: 2, empathy: 5 },
84204
- observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
84675
+ observations_mission: `You consolidate the memory of a health and fitness coach working with one person. This bank records how that person actually lives and trains.
84676
+ ` + `
84677
+ ` + `Synthesise into durable observations:
84678
+ ` + `- Goals, targets, and the plan currently in force, with the reasoning behind each.
84679
+ ` + `- Training, nutrition, sleep, and alcohol patterns as they hold over weeks \u2014 what the person reliably does, not what they did once.
84680
+ ` + `- Constraints that shape the plan: injuries, medical guidance, schedule, equipment, foods and sessions they refuse.
84681
+ ` + `- Motivations, and what actually helps or backfires when they slip.
84682
+ ` + `- Trends in the numbers: direction and range over time, not any single reading.
84683
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
84684
+ ` + `
84685
+ ` + `A single day's log, weight reading, or session is evidence, not an observation. Record one only when it establishes or changes a standing pattern, target, or constraint \u2014 then fold it into the observation for that pattern and say what changed and roughly when.
84686
+ ` + `
84687
+ ` + `Granularity: one observation per habit, target, constraint, or trend. Aggregate repeated daily evidence into the observation for that pattern rather than creating one per day, and keep training, nutrition, and sleep as separate observations.
84688
+ ` + `
84689
+ ` + "Word observations as the person's own pattern and framing, never as a verdict on them."
84205
84690
  },
84206
84691
  "executive-assistant": {
84207
- disposition: { skepticism: 4, literalism: 4, empathy: 3 }
84692
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 },
84693
+ observations_mission: `You consolidate the memory of an executive assistant working for one person. This bank records that person's commitments, people, and standing arrangements.
84694
+ ` + `
84695
+ ` + `Synthesise into durable observations:
84696
+ ` + `- Standing rules and preferences: how they want things scheduled, written, and filed, and when they want to be interrupted.
84697
+ ` + `- People and organisations, and the relationship: who they are, what they are involved in, how to reach them.
84698
+ ` + `- Recurring commitments and routines, and the constraints around them.
84699
+ ` + `- Obligations and their state: what was promised to whom, the deadline, and what is still outstanding.
84700
+ ` + `- Decisions made and decisions deferred, with the reasoning and the trade accepted.
84701
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
84702
+ ` + `
84703
+ ` + `Live commitment state IS durable knowledge here, not ephemeral chatter. An outstanding obligation, a travel window, an unanswered request, a "currently X" arrangement \u2014 these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
84704
+ ` + `
84705
+ ` + `Granularity: one observation per person, arrangement, or obligation. Aggregate repeated mentions into that observation rather than creating siblings; never merge two different people or two different commitments into one.
84706
+ ` + `
84707
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, message or event identifiers, or narration of what the assistant did."
84208
84708
  },
84209
84709
  coding: {
84210
- disposition: { skepticism: 4, literalism: 5, empathy: 2 }
84710
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 },
84711
+ observations_mission: `You consolidate the memory of a software-engineering agent. This bank records real work on real codebases.
84712
+ ` + `
84713
+ ` + `Synthesise into durable observations:
84714
+ ` + `- Architecture and design decisions, each with its rationale and the trade accepted.
84715
+ ` + `- Root causes, with the evidence chain, and negative results \u2014 what was ruled out matters as much as what was found.
84716
+ ` + `- How the repository works: build, test, and lint commands, conventions, CI gates, and where things live.
84717
+ ` + `- Outcomes of code work: issue and PR numbers, what changed, whether it merged, what review found.
84718
+ ` + `- The user's standing rules, preferences, and corrections to this agent's behaviour.
84719
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
84720
+ ` + `
84721
+ ` + `Repository and service state IS durable knowledge here, not ephemeral chatter. Versions, a failing gate, an open PR, a measured number, a "currently X" claim \u2014 these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
84722
+ ` + `
84723
+ ` + `Granularity: one observation per distinct decision, cause, convention, or work item. Aggregate repeated evidence about the same one into that observation rather than creating siblings; never merge two separate decisions into a single summary.
84724
+ ` + `
84725
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, scratch paths, session or request identifiers, or narration of what the agent did. Prefer specific and falsifiable \u2014 naming the file, the number, the commit, or the decision beats summarising the topic."
84211
84726
  }
84212
84727
  };
84213
84728
  // ../src/agents/reconcile-default-skills.ts
@@ -97654,10 +98169,10 @@ function startOutboxSweep(deps) {
97654
98169
  }
97655
98170
 
97656
98171
  // ../src/build-info.ts
97657
- var VERSION2 = "0.19.27";
97658
- var COMMIT_SHA = "42779242";
97659
- var COMMIT_DATE = "2026-07-27T23:25:46Z";
97660
- var LATEST_PR = 3877;
98172
+ var VERSION2 = "0.19.28";
98173
+ var COMMIT_SHA = "d9a5f4ae";
98174
+ var COMMIT_DATE = "2026-07-28T03:46:02Z";
98175
+ var LATEST_PR = 3906;
97661
98176
  var COMMITS_AHEAD_OF_TAG = 0;
97662
98177
 
97663
98178
  // gateway/boot-version.ts
@@ -105584,7 +106099,9 @@ function surfaceConsolidationLegibility(rec, target) {
105584
106099
  `);
105585
106100
  });
105586
106101
  }
106102
+ var noteMcpDependencyFailure = createMcpFailureHook({ agent: AGENT_NAME, emit: emitGatewayOperatorEvent });
105587
106103
  function handleSessionEvent2(ev) {
106104
+ noteMcpDependencyFailure(ev);
105588
106105
  handleSessionEvent(gatewayStreamRenderDeps(), ev);
105589
106106
  }
105590
106107
  function gatewayStreamRenderDeps() {
@@ -109966,6 +110483,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
109966
110483
  const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg2(sourceMsg.text) : "";
109967
110484
  const interimLabel = `\u23F3 **Rule applied for this session** \u2014 ${escapeHtmlForTg2(agentName3)} can ${escapeHtmlForTg2(grantPhrase)} ` + `without asking for now; saving durably in background\u2026`;
109968
110485
  await finalizeCallback2(ctx, {
110486
+ apiCall: robustApiCall,
109969
110487
  ackText: "Rule applied for this session; saving durably in background\u2026".slice(0, 200),
109970
110488
  newText: baseText2 ? `${baseText2}
109971
110489
 
@@ -110131,6 +110649,7 @@ ${editLabel}` : editLabel), {
110131
110649
 
110132
110650
  ${resumeLine}` : htmlLabel;
110133
110651
  await finalizeCallback2(ctx, {
110652
+ apiCall: robustApiCall,
110134
110653
  ackText: ackText3.slice(0, 200),
110135
110654
  newText: baseText ? `${baseText}
110136
110655