switchroom 0.19.35 → 0.19.37

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 (35) hide show
  1. package/dist/cli/skill-validate-pretool.mjs +15 -2
  2. package/dist/cli/switchroom.js +678 -444
  3. package/dist/host-control/main.js +149 -3
  4. package/package.json +4 -2
  5. package/profiles/_base/start.sh.hbs +37 -12
  6. package/telegram-plugin/bridge/bridge.ts +4 -4
  7. package/telegram-plugin/dist/bridge/bridge.js +4 -4
  8. package/telegram-plugin/dist/gateway/gateway.js +317 -95
  9. package/telegram-plugin/dist/server.js +4 -4
  10. package/telegram-plugin/format.ts +100 -29
  11. package/telegram-plugin/gateway/gateway.ts +22 -29
  12. package/telegram-plugin/gateway/ipc-server.ts +18 -15
  13. package/telegram-plugin/gateway/model-command.ts +34 -0
  14. package/telegram-plugin/gateway/outbound-send-path.ts +12 -1
  15. package/telegram-plugin/operator-events.ts +40 -16
  16. package/telegram-plugin/render/unsupported-token-guard.ts +10 -6
  17. package/telegram-plugin/secret-detect/db-uri.ts +90 -0
  18. package/telegram-plugin/secret-detect/index.ts +24 -1
  19. package/telegram-plugin/secret-detect/inert-values.ts +147 -0
  20. package/telegram-plugin/secret-detect/kv-scanner.ts +108 -0
  21. package/telegram-plugin/secret-detect/patterns.ts +24 -4
  22. package/telegram-plugin/tests/format-consistency.test.ts +111 -0
  23. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +40 -2
  24. package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +20 -11
  25. package/telegram-plugin/tests/mcp-instructions-budget.test.ts +8 -1
  26. package/telegram-plugin/tests/outbound-send-path.test.ts +1 -1
  27. package/telegram-plugin/tests/render/unsupported-token-guard.test.ts +36 -5
  28. package/telegram-plugin/tests/secret-detect-cross-engine.test.ts +263 -0
  29. package/telegram-plugin/tests/secret-detect-write-path.test.ts +403 -0
  30. package/telegram-plugin/tests/turn-flush-safety.test.ts +2 -2
  31. package/vendor/hindsight-memory/scripts/lib/client.py +58 -0
  32. package/vendor/hindsight-memory/scripts/lib/secret_patterns.json +431 -0
  33. package/vendor/hindsight-memory/scripts/lib/secret_redact.py +563 -0
  34. package/vendor/hindsight-memory/scripts/lib/secret_redaction_vectors.json +397 -0
  35. package/vendor/hindsight-memory/scripts/tests/test_secret_redact.py +522 -0
@@ -6923,7 +6923,11 @@ function normalizePunctuation(text) {
6923
6923
  const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6924
6924
  const linkRestoreRe = new RegExp(`\x00RML${escNonce}_(\\d+)\x00`, "g");
6925
6925
  const restoreLinks = (s) => s.replace(linkRestoreRe, (_m, idx) => linkMasks[Number(idx)] ?? _m);
6926
- let out = maskedAutolinks.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
6926
+ const rewriteDashes = (line) => line.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
6927
+ const isQuotedLine = (line) => isBlockquoteLine(line) || line.trimStart().startsWith("**>");
6928
+ let out = maskedAutolinks.split(`
6929
+ `).map((line) => isQuotedLine(line) ? line : rewriteDashes(line)).join(`
6930
+ `);
6927
6931
  out = restoreLinks(out);
6928
6932
  out = out.split(`
6929
6933
  `).map((line) => line.replace(/^([ \t]*)[\u2022\u00b7][ \t]+/, "$1- ")).join(`
@@ -6936,10 +6940,18 @@ function listItemContent(line) {
6936
6940
  function isFullyBolded(fragment) {
6937
6941
  return /^\*\*[^*]+\*\*[.,:;!?]?$/.test(fragment.trim());
6938
6942
  }
6943
+ function isPseudoHeadingBlock(block) {
6944
+ const lines = block.split(`
6945
+ `).filter((l) => l.trim() !== "");
6946
+ if (lines.length !== 1)
6947
+ return false;
6948
+ const t = lines[0].trim();
6949
+ return t.length <= PSEUDO_HEADING_MAX_CHARS && isFullyBolded(t);
6950
+ }
6939
6951
  function unbold(fragment) {
6940
6952
  return fragment.replace(/\*\*([^*]+)\*\*/g, "$1");
6941
6953
  }
6942
- function stripExcessBold(text) {
6954
+ function stripExcessBold(text, onStrip) {
6943
6955
  if (!text.includes("**"))
6944
6956
  return text;
6945
6957
  const nonce = Math.random().toString(36).slice(2);
@@ -6950,10 +6962,15 @@ function stripExcessBold(text) {
6950
6962
  let boldChars = 0;
6951
6963
  for (const m of visible.matchAll(/\*\*([^*]+)\*\*/g))
6952
6964
  boldChars += m[1].length;
6953
- if (boldChars / visible.length > 0.3) {
6954
- return restore(unbold(masked));
6955
- }
6965
+ const ratio = boldChars / visible.length;
6956
6966
  const blocks = masked.split(/\n{2,}/);
6967
+ if (ratio > 0.3) {
6968
+ const rebuilt2 = blocks.map((block) => isPseudoHeadingBlock(block) ? block : unbold(block));
6969
+ const out2 = rejoinBlocks(masked, rebuilt2);
6970
+ if (out2 !== masked)
6971
+ onStrip?.({ rule: "global", ratio });
6972
+ return restore(out2);
6973
+ }
6957
6974
  const rebuilt = blocks.map((block) => {
6958
6975
  const lines = block.split(`
6959
6976
  `).filter((l) => l.trim() !== "");
@@ -6970,17 +6987,23 @@ function stripExcessBold(text) {
6970
6987
  return block;
6971
6988
  if (!lines.every((l) => isFullyBolded(l)))
6972
6989
  return block;
6973
- if (lines.length === 1 && lines[0].trim().length <= 48)
6990
+ if (isPseudoHeadingBlock(block))
6974
6991
  return block;
6975
6992
  return unbold(block);
6976
6993
  });
6994
+ const out = rejoinBlocks(masked, rebuilt);
6995
+ if (out !== masked)
6996
+ onStrip?.({ rule: "per-block", ratio });
6997
+ return restore(out);
6998
+ }
6999
+ function rejoinBlocks(masked, rebuilt) {
6977
7000
  const seps = masked.match(/\n{2,}/g) ?? [];
6978
7001
  let out = rebuilt[0] ?? "";
6979
7002
  for (let i = 1;i < rebuilt.length; i++)
6980
7003
  out += (seps[i - 1] ?? `
6981
7004
 
6982
7005
  `) + rebuilt[i];
6983
- return restore(out);
7006
+ return out;
6984
7007
  }
6985
7008
  function isListItemLine(line) {
6986
7009
  const t = line.trimStart();
@@ -7183,7 +7206,7 @@ function backOffOpenInline(text, cut) {
7183
7206
  }
7184
7207
  return earliest;
7185
7208
  }
7186
- var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", INLINE_SPAN_PATTERNS;
7209
+ var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", PSEUDO_HEADING_MAX_CHARS = 48, INLINE_SPAN_PATTERNS;
7187
7210
  var init_format = __esm(() => {
7188
7211
  INLINE_SPAN_PATTERNS = [
7189
7212
  /`[^`\n]+`/g,
@@ -12660,16 +12683,22 @@ var init_patterns = __esm(() => {
12660
12683
  },
12661
12684
  {
12662
12685
  rule_id: "bearer_auth_header",
12663
- regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/g,
12686
+ regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
12664
12687
  captureIndex: 1,
12665
12688
  slugHint: "bearer_token"
12666
12689
  },
12667
12690
  {
12668
12691
  rule_id: "bearer_loose",
12669
- regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/g,
12692
+ regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
12670
12693
  captureIndex: 1,
12671
12694
  slugHint: "bearer_token"
12672
12695
  },
12696
+ {
12697
+ rule_id: "basic_auth_header",
12698
+ regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
12699
+ captureIndex: 1,
12700
+ slugHint: "basic_auth"
12701
+ },
12673
12702
  {
12674
12703
  rule_id: "pem_private_key",
12675
12704
  regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
@@ -12729,7 +12758,104 @@ function shannonEntropy(s) {
12729
12758
  return h;
12730
12759
  }
12731
12760
 
12761
+ // secret-detect/inert-values.ts
12762
+ function runIsCredentialShaped(run) {
12763
+ let classes = 0;
12764
+ if (/[a-z]/.test(run))
12765
+ classes++;
12766
+ if (/[A-Z]/.test(run))
12767
+ classes++;
12768
+ if (/[0-9]/.test(run))
12769
+ classes++;
12770
+ return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
12771
+ }
12772
+ function hasCredentialShapedRun(value) {
12773
+ for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
12774
+ if (runIsCredentialShaped(run))
12775
+ return true;
12776
+ }
12777
+ return false;
12778
+ }
12779
+ function isInertValue(value) {
12780
+ if (!INERT_VALUE_RE.some((re) => re.test(value)))
12781
+ return false;
12782
+ return !hasCredentialShapedRun(value);
12783
+ }
12784
+ function stripTrailingPunctuation(value) {
12785
+ return value.replace(TRAILING_PUNCT_RE, "");
12786
+ }
12787
+ var INERT_VALUE_RE, CREDENTIAL_RUN_RE, MIXED_CLASS_RUN_MIN = 12, SINGLE_CLASS_RUN_MIN = 16, INERT_GATED_RULES, TRAILING_PUNCT_RE;
12788
+ var init_inert_values = __esm(() => {
12789
+ INERT_VALUE_RE = [
12790
+ /^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
12791
+ /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
12792
+ /^\{\{[^\n\r\u2028\u2029]*\}\}$/,
12793
+ /^<[^<>\n\r\u2028\u2029]*>$/,
12794
+ /^%[A-Za-z_][A-Za-z0-9_]*%$/,
12795
+ /^vault:[A-Za-z0-9_./-]*$/i,
12796
+ /^[*x\u2022.]+$/i,
12797
+ /^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
12798
+ /^(?:changeme|change-me|replaceme|replace-me|placeholder|todo|tbd|yourkey|your-key|yourpassword|your-password|yoursecret|your-secret|yourtoken|your-token)(?:[-_][A-Za-z0-9-]+)?$/i,
12799
+ /^[a-z]+(?: [a-z]+){2,}$/
12800
+ ];
12801
+ CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
12802
+ INERT_GATED_RULES = new Set([
12803
+ "env_key_value",
12804
+ "json_secret_field",
12805
+ "cli_flag"
12806
+ ]);
12807
+ TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
12808
+ });
12809
+
12732
12810
  // secret-detect/kv-scanner.ts
12811
+ function charClassCount(value) {
12812
+ let n = 0;
12813
+ if (/[a-z]/.test(value))
12814
+ n++;
12815
+ if (/[A-Z]/.test(value))
12816
+ n++;
12817
+ if (/[0-9]/.test(value))
12818
+ n++;
12819
+ if (/[^A-Za-z0-9]/.test(value))
12820
+ n++;
12821
+ return n;
12822
+ }
12823
+ function looksLikeMemorablePassword(value) {
12824
+ if (isInertValue(value))
12825
+ return false;
12826
+ const core = stripTrailingPunctuation(value);
12827
+ if (core.length < 8 || core.length > 64)
12828
+ return false;
12829
+ if (isInertValue(core))
12830
+ return false;
12831
+ if (new Set(core).size < 4)
12832
+ return false;
12833
+ return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
12834
+ }
12835
+ function scanMemorablePasswords(text) {
12836
+ const hits = [];
12837
+ MEMORABLE_PW_RE.lastIndex = 0;
12838
+ let m;
12839
+ while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
12840
+ const keyName = m[1];
12841
+ const value = m[3];
12842
+ if (!value || !looksLikeMemorablePassword(value))
12843
+ continue;
12844
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
12845
+ if (valueOffsetInMatch < 0)
12846
+ continue;
12847
+ const start = m.index + valueOffsetInMatch;
12848
+ hits.push({
12849
+ rule_id: MEMORABLE_PW_RULE_ID,
12850
+ start,
12851
+ end: start + value.length,
12852
+ matched_text: value,
12853
+ key_name: keyName,
12854
+ confidence: "ambiguous"
12855
+ });
12856
+ }
12857
+ return hits;
12858
+ }
12733
12859
  function scanKeyValue(text) {
12734
12860
  const hits = [];
12735
12861
  KV_RE.lastIndex = 0;
@@ -12738,6 +12864,8 @@ function scanKeyValue(text) {
12738
12864
  const [, keyName, value] = m;
12739
12865
  if (!value)
12740
12866
  continue;
12867
+ if (isInertValue(value))
12868
+ continue;
12741
12869
  const h = shannonEntropy(value);
12742
12870
  if (h < KV_ENTROPY_THRESHOLD)
12743
12871
  continue;
@@ -12757,9 +12885,44 @@ function scanKeyValue(text) {
12757
12885
  }
12758
12886
  return hits;
12759
12887
  }
12760
- var KV_RE, KV_ENTROPY_THRESHOLD = 4;
12888
+ var KV_RE, KV_ENTROPY_THRESHOLD = 4, MEMORABLE_PW_RE, MEMORABLE_PW_RULE_ID = "memorable_password", MEMORABLE_PW_MIN_CLASSES = 2;
12761
12889
  var init_kv_scanner = __esm(() => {
12890
+ init_inert_values();
12762
12891
  KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
12892
+ MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
12893
+ });
12894
+
12895
+ // secret-detect/db-uri.ts
12896
+ function isInertPassword(value) {
12897
+ if (value.startsWith("[REDACTED"))
12898
+ return true;
12899
+ return /^[*x]+$/i.test(value);
12900
+ }
12901
+ function scanDbUris(text) {
12902
+ const hits = [];
12903
+ DB_URI_RE.lastIndex = 0;
12904
+ let m;
12905
+ while ((m = DB_URI_RE.exec(text)) !== null) {
12906
+ const scheme = m[1];
12907
+ const user = m[2];
12908
+ const password = m[3];
12909
+ if (isInertPassword(password))
12910
+ continue;
12911
+ const start = m.index + scheme.length + 3 + user.length + 1;
12912
+ hits.push({
12913
+ rule_id: DB_URI_RULE_ID,
12914
+ start,
12915
+ end: start + password.length,
12916
+ matched_text: password,
12917
+ key_name: `${scheme}_password`,
12918
+ confidence: "ambiguous"
12919
+ });
12920
+ }
12921
+ return hits;
12922
+ }
12923
+ var DB_URI_RE, DB_URI_RULE_ID = "db_uri_password";
12924
+ var init_db_uri = __esm(() => {
12925
+ DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
12763
12926
  });
12764
12927
 
12765
12928
  // secret-detect/generic-entropy.ts
@@ -12953,6 +13116,8 @@ function detectSecrets(text) {
12953
13116
  const globalStart = win.offset + matchStart;
12954
13117
  const globalEnd = globalStart + cap.length;
12955
13118
  const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
13119
+ if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
13120
+ continue;
12956
13121
  if (p.rule_id === "env_key_value") {
12957
13122
  const ENV_KV_MIN_LEN = 12;
12958
13123
  const ENV_KV_MIN_ENTROPY = 3.5;
@@ -12975,6 +13140,12 @@ function detectSecrets(text) {
12975
13140
  for (const h of kvHits) {
12976
13141
  raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
12977
13142
  }
13143
+ for (const h of scanDbUris(win.text)) {
13144
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
13145
+ }
13146
+ for (const h of scanMemorablePasswords(win.text)) {
13147
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
13148
+ }
12978
13149
  const genHits = scanGenericSecrets(win.text);
12979
13150
  for (const h of genHits) {
12980
13151
  raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
@@ -13024,6 +13195,8 @@ function dropOverlaps(hits) {
13024
13195
  var init_secret_detect = __esm(() => {
13025
13196
  init_patterns();
13026
13197
  init_kv_scanner();
13198
+ init_db_uri();
13199
+ init_inert_values();
13027
13200
  init_generic_entropy();
13028
13201
  init_chunker();
13029
13202
  init_suppressor();
@@ -13438,7 +13611,7 @@ function guardUnsupportedTokens(text) {
13438
13611
  var CARET_PAIR, FOOTNOTE_MARKER, DETAILS_BLOCK, ORPHAN_TAGS;
13439
13612
  var init_unsupported_token_guard = __esm(() => {
13440
13613
  CARET_PAIR = /\^([A-Za-z0-9]+)\^/g;
13441
- FOOTNOTE_MARKER = /\[\^\d+\](?!:)/g;
13614
+ FOOTNOTE_MARKER = /\[\^[A-Za-z0-9]{1,10}\](?!:)/g;
13442
13615
  DETAILS_BLOCK = /<details[^>]*>\s*(?:<summary[^>]*>([\s\S]*?)<\/summary>)?([\s\S]*?)<\/details>/gi;
13443
13616
  ORPHAN_TAGS = /<\/?(?:details|summary)[^>]*>/gi;
13444
13617
  });
@@ -71039,6 +71212,23 @@ function describeProviderCreditRemedy(provider) {
71039
71212
  }
71040
71213
 
71041
71214
  // operator-events.ts
71215
+ var OPERATOR_EVENT_KINDS = [
71216
+ "credentials-expired",
71217
+ "credentials-invalid",
71218
+ "proxy-misconfig",
71219
+ "credit-exhausted",
71220
+ "provider-credit-exhausted",
71221
+ "mcp-dependency-blocked",
71222
+ "quota-exhausted",
71223
+ "rate-limited",
71224
+ "agent-crashed",
71225
+ "agent-restarted-unexpectedly",
71226
+ "unknown-4xx",
71227
+ "unknown-5xx",
71228
+ "config-warning",
71229
+ "always-allow-persist-failed",
71230
+ "mental-model-persist-failed"
71231
+ ];
71042
71232
  function classifyClaudeError(raw) {
71043
71233
  try {
71044
71234
  return classifyInner(raw);
@@ -77648,7 +77838,12 @@ function normalizeOutboundBody(rawText, site, redact2, opts = {}) {
77648
77838
  text4 = normalizeParagraphBreaks(text4);
77649
77839
  text4 = redact2(text4, site);
77650
77840
  if (!literalText) {
77651
- let formatted = stripExcessBold(normalizePunctuation(text4));
77841
+ let formatted = stripExcessBold(normalizePunctuation(text4), (d) => {
77842
+ try {
77843
+ process.stderr.write(`telegram gateway: strip-excess-bold: fired site=${site} rule=${d.rule} ratio=${d.ratio.toFixed(3)}
77844
+ `);
77845
+ } catch {}
77846
+ });
77652
77847
  if (addSpacers)
77653
77848
  formatted = addParagraphSpacers(formatted);
77654
77849
  text4 = formatted;
@@ -79094,7 +79289,12 @@ function normalizeOutboundBody2(rawText, site, redact2, opts = {}) {
79094
79289
  text4 = normalizeParagraphBreaks(text4);
79095
79290
  text4 = redact2(text4, site);
79096
79291
  if (!literalText) {
79097
- let formatted = stripExcessBold(normalizePunctuation(text4));
79292
+ let formatted = stripExcessBold(normalizePunctuation(text4), (d) => {
79293
+ try {
79294
+ process.stderr.write(`telegram gateway: strip-excess-bold: fired site=${site} rule=${d.rule} ratio=${d.ratio.toFixed(3)}
79295
+ `);
79296
+ } catch {}
79297
+ });
79098
79298
  if (addSpacers)
79099
79299
  formatted = addParagraphSpacers(formatted);
79100
79300
  text4 = formatted;
@@ -83583,6 +83783,27 @@ var MODEL_ARG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._/\[\]-]{0,99}$/;
83583
83783
  function isValidModelArg2(arg) {
83584
83784
  return MODEL_ARG_RE2.test(arg);
83585
83785
  }
83786
+ var DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS = 180000;
83787
+ var SESSION_MODEL_RESOLUTION_POLL_MS = 250;
83788
+ function resolveSessionModelResolutionTimeoutMs(raw) {
83789
+ if (raw == null || raw.trim() === "")
83790
+ return DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS;
83791
+ const value = Number(raw);
83792
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS;
83793
+ }
83794
+ async function waitForSessionModelResolution(input) {
83795
+ const pollMs = input.pollMs ?? SESSION_MODEL_RESOLUTION_POLL_MS;
83796
+ const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
83797
+ const startedAt = Date.now();
83798
+ for (;; ) {
83799
+ if (input.barrierExists())
83800
+ return true;
83801
+ const remaining = input.timeoutMs - (Date.now() - startedAt);
83802
+ if (remaining <= 0)
83803
+ return false;
83804
+ await sleep2(Math.min(pollMs, remaining));
83805
+ }
83806
+ }
83586
83807
  function isSrModel2(name) {
83587
83808
  return name.startsWith("sr-");
83588
83809
  }
@@ -88847,17 +89068,7 @@ function recordWebhookEvent(rec, deps = {}) {
88847
89068
  init_format();
88848
89069
  import { renameSync as renameSync17, unlinkSync as unlinkSync20, chmodSync as chmodSync11 } from "fs";
88849
89070
  var MAX_BUFFER_SIZE = 1024 * 1024;
88850
- var VALID_OPERATOR_KINDS = new Set([
88851
- "credentials-expired",
88852
- "credentials-invalid",
88853
- "credit-exhausted",
88854
- "quota-exhausted",
88855
- "rate-limited",
88856
- "agent-crashed",
88857
- "agent-restarted-unexpectedly",
88858
- "unknown-4xx",
88859
- "unknown-5xx"
88860
- ]);
89071
+ var VALID_OPERATOR_KINDS = new Set(OPERATOR_EVENT_KINDS);
88861
89072
  var AGENT_NAME_RE3 = /^[a-z0-9][a-z0-9_-]{0,50}$/;
88862
89073
  var OPERATOR_EVENT_DETAIL_MAX = 1000;
88863
89074
  function validateClientMessage(msg) {
@@ -99461,10 +99672,10 @@ function startOutboxSweep(deps) {
99461
99672
  }
99462
99673
 
99463
99674
  // ../src/build-info.ts
99464
- var VERSION2 = "0.19.35";
99465
- var COMMIT_SHA = "76ae22ca";
99466
- var COMMIT_DATE = "2026-07-30T01:27:21Z";
99467
- var LATEST_PR = 4011;
99675
+ var VERSION2 = "0.19.37";
99676
+ var COMMIT_SHA = "5d2f9a17";
99677
+ var COMMIT_DATE = "2026-07-30T08:57:38Z";
99678
+ var LATEST_PR = 4023;
99468
99679
  var COMMITS_AHEAD_OF_TAG = 0;
99469
99680
 
99470
99681
  // gateway/boot-version.ts
@@ -112706,76 +112917,87 @@ async function startGateway() {
112706
112917
  try {
112707
112918
  const smAgentDir = resolveAgentDirFromEnv();
112708
112919
  if (smAgentDir) {
112709
- const activePath = join68(smAgentDir, ".active-session-model");
112710
- if (existsSync57(activePath)) {
112711
- try {
112712
- const launched = readFileSync60(activePath, "utf8").trim();
112713
- const configured = (() => {
112714
- const d = switchroomExecJson(["agent", "list"]);
112715
- const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
112716
- return resolveMainModel(raw ?? undefined);
112717
- })();
112718
- const isApplyBoot = launched.length > 0 && launched !== configured;
112719
- sessionModelSource.setOverride(isApplyBoot ? launched : null, { verify: true });
112720
- const modelBootCardDeps = {
112721
- agent: getMyAgentName(),
112722
- chat: modelSwitchMarkerChat,
112723
- log: (line) => process.stderr.write(line),
112724
- sendCard: (chatId, body, opts) => lockedBot.api.sendMessage(chatId, body, opts)
112725
- };
112726
- if (isApplyBoot) {
112727
- sessionModelSource.setDivergenceHandler(buildServedModelDivergenceHandler(modelBootCardDeps));
112728
- }
112729
- const confirmation = modelSwitchReason != null ? classifyModelSwitchConfirmation({
112730
- reason: modelSwitchReason,
112731
- launched,
112732
- configured
112733
- }) : null;
112734
- process.stderr.write(formatModelRelaunchDiagLog({
112735
- agent: getMyAgentName(),
112736
- launched,
112737
- configured,
112738
- confirmation,
112739
- isApplyBoot
112740
- }));
112741
- if (confirmation != null) {
112742
- deliverModelSwitchBootNotice({
112743
- ...modelBootCardDeps,
112920
+ const resolutionTimeoutMs = resolveSessionModelResolutionTimeoutMs(process.env.SWITCHROOM_SESSION_MODEL_RESOLUTION_TIMEOUT_MS);
112921
+ const resolved = await waitForSessionModelResolution({
112922
+ barrierExists: () => existsSync57(join68(smAgentDir, ".session-model-resolved")),
112923
+ timeoutMs: resolutionTimeoutMs
112924
+ });
112925
+ if (!resolved) {
112926
+ const target = modelSwitchReason == null ? "(none)" : parseModelSwitchTarget(modelSwitchReason) ?? "(unknown)";
112927
+ process.stderr.write(`telegram gateway: gw /model relaunch UNRESOLVED agent=${getMyAgentName()} target=${target} (barrier timeout after ${resolutionTimeoutMs}ms)
112928
+ `);
112929
+ } else {
112930
+ const activePath = join68(smAgentDir, ".active-session-model");
112931
+ if (existsSync57(activePath)) {
112932
+ try {
112933
+ const launched = readFileSync60(activePath, "utf8").trim();
112934
+ const configured = (() => {
112935
+ const d = switchroomExecJson(["agent", "list"]);
112936
+ const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
112937
+ return resolveMainModel(raw ?? undefined);
112938
+ })();
112939
+ const isApplyBoot = launched.length > 0 && launched !== configured;
112940
+ sessionModelSource.setOverride(isApplyBoot ? launched : null, { verify: true });
112941
+ const modelBootCardDeps = {
112942
+ agent: getMyAgentName(),
112943
+ chat: modelSwitchMarkerChat,
112944
+ log: (line) => process.stderr.write(line),
112945
+ sendCard: (chatId, body, opts) => lockedBot.api.sendMessage(chatId, body, opts)
112946
+ };
112947
+ if (isApplyBoot) {
112948
+ sessionModelSource.setDivergenceHandler(buildServedModelDivergenceHandler(modelBootCardDeps));
112949
+ }
112950
+ const confirmation = modelSwitchReason != null ? classifyModelSwitchConfirmation({
112951
+ reason: modelSwitchReason,
112952
+ launched,
112953
+ configured
112954
+ }) : null;
112955
+ process.stderr.write(formatModelRelaunchDiagLog({
112956
+ agent: getMyAgentName(),
112957
+ launched,
112958
+ configured,
112744
112959
  confirmation,
112745
- hasSessionModelAlert: existsSync57(join68(smAgentDir, ".session-model-alert"))
112746
- });
112747
- }
112748
- } catch {}
112749
- }
112750
- const activeEffortPath = join68(smAgentDir, ".active-session-effort");
112751
- if (existsSync57(activeEffortPath)) {
112752
- try {
112753
- const launchedEffort = readFileSync60(activeEffortPath, "utf8").trim();
112754
- const configuredEffort = getConfiguredEffortForPersist();
112755
- sessionEffortOverride = launchedEffort.length > 0 && launchedEffort !== configuredEffort ? launchedEffort : null;
112756
- } catch {}
112757
- }
112758
- const alertPath = join68(smAgentDir, ".session-model-alert");
112759
- if (existsSync57(alertPath)) {
112760
- let alertText = null;
112761
- try {
112762
- alertText = readFileSync60(alertPath, "utf8").trim();
112763
- } catch {
112764
- alertText = null;
112960
+ isApplyBoot
112961
+ }));
112962
+ if (confirmation != null) {
112963
+ deliverModelSwitchBootNotice({
112964
+ ...modelBootCardDeps,
112965
+ confirmation,
112966
+ hasSessionModelAlert: existsSync57(join68(smAgentDir, ".session-model-alert"))
112967
+ });
112968
+ }
112969
+ } catch {}
112765
112970
  }
112766
- try {
112767
- unlinkSync30(alertPath);
112768
- } catch {}
112769
- if (alertText && alertText.length > 0) {
112770
- const operators = loadAccess().allowFrom;
112771
- for (const operator of operators) {
112772
- if (!operator)
112773
- continue;
112774
- lockedBot.api.sendMessage(operator, `\u26A0\uFE0F ${alertText}`).catch((err) => process.stderr.write(`telegram gateway: session-model alert send failed for ${operator}: ${err?.message ?? String(err)}
112775
- `));
112971
+ const activeEffortPath = join68(smAgentDir, ".active-session-effort");
112972
+ if (existsSync57(activeEffortPath)) {
112973
+ try {
112974
+ const launchedEffort = readFileSync60(activeEffortPath, "utf8").trim();
112975
+ const configuredEffort = getConfiguredEffortForPersist();
112976
+ sessionEffortOverride = launchedEffort.length > 0 && launchedEffort !== configuredEffort ? launchedEffort : null;
112977
+ } catch {}
112978
+ }
112979
+ const alertPath = join68(smAgentDir, ".session-model-alert");
112980
+ if (existsSync57(alertPath)) {
112981
+ let alertText = null;
112982
+ try {
112983
+ alertText = readFileSync60(alertPath, "utf8").trim();
112984
+ } catch {
112985
+ alertText = null;
112776
112986
  }
112777
- process.stderr.write(`telegram gateway: session-model: LiteLLM-down override drop \u2014 ${alertText}
112987
+ try {
112988
+ unlinkSync30(alertPath);
112989
+ } catch {}
112990
+ if (alertText && alertText.length > 0) {
112991
+ const operators = loadAccess().allowFrom;
112992
+ for (const operator of operators) {
112993
+ if (!operator)
112994
+ continue;
112995
+ lockedBot.api.sendMessage(operator, `\u26A0\uFE0F ${alertText}`).catch((err) => process.stderr.write(`telegram gateway: session-model alert send failed for ${operator}: ${err?.message ?? String(err)}
112996
+ `));
112997
+ }
112998
+ process.stderr.write(`telegram gateway: session-model: LiteLLM-down override drop \u2014 ${alertText}
112778
112999
  `);
113000
+ }
112779
113001
  }
112780
113002
  }
112781
113003
  }
@@ -24827,7 +24827,7 @@ var init_bridge = __esm(async () => {
24827
24827
  TOOL_SCHEMAS = [
24828
24828
  {
24829
24829
  name: "reply",
24830
- description: "Reply on Telegram. Pass chat_id from the inbound message. By default the reply is a quote-reply to the latest inbound user message in this chat+thread \u2014 pass quote:false to opt out, or pass an explicit reply_to to thread under a specific earlier message. files (absolute paths) attach images or documents. inline_keyboard adds tappable buttons (URL or callback) under the message \u2014 single-tap actions beat asking the user to type YES. " + "FORUM TOPICS: a reply is auto-routed back to the topic the question came from, so do NOT pass message_thread_id on a normal reply \u2014 pass the inbound's origin_turn_id instead, so the answer lands in the right topic even if a message from another topic arrived while you were working. message_thread_id is only for deliberately posting into a topic that is not the one you were asked in. " + 'FORMAT: the default format is "html" \u2014 write natural markdown and it is auto-converted to Telegram HTML (bold, italic, code, links, code blocks). Pass format: "markdownv2" for MarkdownV2 with auto-escaping, or "text" for plain text sent verbatim.',
24830
+ description: "Reply on Telegram. Pass chat_id from the inbound message. By default the reply is a quote-reply to the latest inbound user message in this chat+thread \u2014 pass quote:false to opt out, or pass an explicit reply_to to thread under a specific earlier message. files (absolute paths) attach images or documents. inline_keyboard adds tappable buttons (URL or callback) under the message \u2014 single-tap actions beat asking the user to type YES. " + "FORUM TOPICS: a reply is auto-routed back to the topic the question came from, so do NOT pass message_thread_id on a normal reply \u2014 pass the inbound's origin_turn_id instead, so the answer lands in the right topic even if a message from another topic arrived while you were working. message_thread_id is only for deliberately posting into a topic that is not the one you were asked in. " + 'FORMAT: by default your text is rendered as rich GFM markdown (Bot API 10.1) \u2014 write natural markdown (bold, italic, code, links, code blocks, lists, tables) and it renders as-is. Pass format: "text" to send plain text verbatim with no rendering. "html" and "markdownv2" are accepted legacy aliases that route through the exact same single rich GFM path \u2014 there is no separate HTML engine and no auto-escaping.',
24831
24831
  inputSchema: {
24832
24832
  type: "object",
24833
24833
  properties: {
@@ -24838,7 +24838,7 @@ var init_bridge = __esm(async () => {
24838
24838
  message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message in the same chat if not specified." },
24839
24839
  origin_turn_id: { type: "string", description: "In a forum supergroup, pass back the origin_turn_id attribute from the <channel> message you are answering. It pins the reply to that message's topic even if another topic's turn started meanwhile. Omit in DMs / single-topic chats." },
24840
24840
  files: { type: "array", items: { type: "string" }, description: "Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each. Telegram rejects photos with extreme dimensions (aspect ratio over ~10:1, width+height over 10000px, or over 10MB) \u2014 very tall images like full-page screenshots are auto-rerouted as documents; crop or split them first if the user should see them inline as photos." },
24841
- format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." },
24841
+ format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. Default renders your text as rich GFM markdown (Bot API 10.1). 'text' sends plain text verbatim. 'html' and 'markdownv2' are legacy aliases that route through the same single rich GFM path \u2014 no separate HTML engine, no auto-escaping." },
24842
24842
  disable_web_page_preview: { type: "boolean", description: "Disable link preview thumbnails. Default: true." },
24843
24843
  protect_content: { type: "boolean", description: "When true, Telegram prevents the message from being forwarded or saved." },
24844
24844
  quote_text: { type: "string", description: "Surgical quote: specific text to highlight from the reply_to message. Requires reply_to." },
@@ -24912,7 +24912,7 @@ var init_bridge = __esm(async () => {
24912
24912
  chat_id: { type: "string" },
24913
24913
  message_id: { type: "string" },
24914
24914
  text: { type: "string" },
24915
- format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." }
24915
+ format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. Default renders your text as rich GFM markdown (Bot API 10.1). 'text' sends plain text verbatim. 'html' and 'markdownv2' are legacy aliases that route through the same single rich GFM path \u2014 no separate HTML engine, no auto-escaping." }
24916
24916
  },
24917
24917
  required: ["chat_id", "message_id", "text"]
24918
24918
  }
@@ -25069,7 +25069,7 @@ var init_bridge = __esm(async () => {
25069
25069
  type: "object",
25070
25070
  properties: {
25071
25071
  chat_id: { type: "string", description: "Chat that should receive the question. Pass from inbound meta." },
25072
- question: { type: "string", description: "The question text. Plain text or HTML. Keep it short \u2014 buttons render below." },
25072
+ question: { type: "string", description: "The question text. Plain text or GFM markdown (rendered through the same rich path as replies). Keep it short \u2014 buttons render below." },
25073
25073
  options: {
25074
25074
  type: "array",
25075
25075
  items: { type: "string" },