switchroom 0.19.35 → 0.19.36
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.
- package/dist/cli/skill-validate-pretool.mjs +15 -2
- package/dist/cli/switchroom.js +672 -441
- package/dist/host-control/main.js +149 -3
- package/package.json +4 -2
- package/profiles/_base/start.sh.hbs +37 -12
- package/telegram-plugin/dist/gateway/gateway.js +311 -93
- package/telegram-plugin/format.ts +70 -15
- package/telegram-plugin/gateway/gateway.ts +22 -29
- package/telegram-plugin/gateway/ipc-server.ts +18 -15
- package/telegram-plugin/gateway/model-command.ts +34 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +12 -1
- package/telegram-plugin/operator-events.ts +40 -16
- package/telegram-plugin/secret-detect/db-uri.ts +90 -0
- package/telegram-plugin/secret-detect/index.ts +24 -1
- package/telegram-plugin/secret-detect/inert-values.ts +147 -0
- package/telegram-plugin/secret-detect/kv-scanner.ts +108 -0
- package/telegram-plugin/secret-detect/patterns.ts +24 -4
- package/telegram-plugin/tests/format-consistency.test.ts +93 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +40 -2
- package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +20 -11
- package/telegram-plugin/tests/outbound-send-path.test.ts +1 -1
- package/telegram-plugin/tests/secret-detect-cross-engine.test.ts +263 -0
- package/telegram-plugin/tests/secret-detect-write-path.test.ts +403 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +2 -2
- package/vendor/hindsight-memory/scripts/lib/client.py +58 -0
- package/vendor/hindsight-memory/scripts/lib/secret_patterns.json +431 -0
- package/vendor/hindsight-memory/scripts/lib/secret_redact.py +563 -0
- package/vendor/hindsight-memory/scripts/lib/secret_redaction_vectors.json +397 -0
- package/vendor/hindsight-memory/scripts/tests/test_secret_redact.py +522 -0
|
@@ -6936,10 +6936,18 @@ function listItemContent(line) {
|
|
|
6936
6936
|
function isFullyBolded(fragment) {
|
|
6937
6937
|
return /^\*\*[^*]+\*\*[.,:;!?]?$/.test(fragment.trim());
|
|
6938
6938
|
}
|
|
6939
|
+
function isPseudoHeadingBlock(block) {
|
|
6940
|
+
const lines = block.split(`
|
|
6941
|
+
`).filter((l) => l.trim() !== "");
|
|
6942
|
+
if (lines.length !== 1)
|
|
6943
|
+
return false;
|
|
6944
|
+
const t = lines[0].trim();
|
|
6945
|
+
return t.length <= PSEUDO_HEADING_MAX_CHARS && isFullyBolded(t);
|
|
6946
|
+
}
|
|
6939
6947
|
function unbold(fragment) {
|
|
6940
6948
|
return fragment.replace(/\*\*([^*]+)\*\*/g, "$1");
|
|
6941
6949
|
}
|
|
6942
|
-
function stripExcessBold(text) {
|
|
6950
|
+
function stripExcessBold(text, onStrip) {
|
|
6943
6951
|
if (!text.includes("**"))
|
|
6944
6952
|
return text;
|
|
6945
6953
|
const nonce = Math.random().toString(36).slice(2);
|
|
@@ -6950,10 +6958,15 @@ function stripExcessBold(text) {
|
|
|
6950
6958
|
let boldChars = 0;
|
|
6951
6959
|
for (const m of visible.matchAll(/\*\*([^*]+)\*\*/g))
|
|
6952
6960
|
boldChars += m[1].length;
|
|
6953
|
-
|
|
6954
|
-
return restore(unbold(masked));
|
|
6955
|
-
}
|
|
6961
|
+
const ratio = boldChars / visible.length;
|
|
6956
6962
|
const blocks = masked.split(/\n{2,}/);
|
|
6963
|
+
if (ratio > 0.3) {
|
|
6964
|
+
const rebuilt2 = blocks.map((block) => isPseudoHeadingBlock(block) ? block : unbold(block));
|
|
6965
|
+
const out2 = rejoinBlocks(masked, rebuilt2);
|
|
6966
|
+
if (out2 !== masked)
|
|
6967
|
+
onStrip?.({ rule: "global", ratio });
|
|
6968
|
+
return restore(out2);
|
|
6969
|
+
}
|
|
6957
6970
|
const rebuilt = blocks.map((block) => {
|
|
6958
6971
|
const lines = block.split(`
|
|
6959
6972
|
`).filter((l) => l.trim() !== "");
|
|
@@ -6970,17 +6983,23 @@ function stripExcessBold(text) {
|
|
|
6970
6983
|
return block;
|
|
6971
6984
|
if (!lines.every((l) => isFullyBolded(l)))
|
|
6972
6985
|
return block;
|
|
6973
|
-
if (
|
|
6986
|
+
if (isPseudoHeadingBlock(block))
|
|
6974
6987
|
return block;
|
|
6975
6988
|
return unbold(block);
|
|
6976
6989
|
});
|
|
6990
|
+
const out = rejoinBlocks(masked, rebuilt);
|
|
6991
|
+
if (out !== masked)
|
|
6992
|
+
onStrip?.({ rule: "per-block", ratio });
|
|
6993
|
+
return restore(out);
|
|
6994
|
+
}
|
|
6995
|
+
function rejoinBlocks(masked, rebuilt) {
|
|
6977
6996
|
const seps = masked.match(/\n{2,}/g) ?? [];
|
|
6978
6997
|
let out = rebuilt[0] ?? "";
|
|
6979
6998
|
for (let i = 1;i < rebuilt.length; i++)
|
|
6980
6999
|
out += (seps[i - 1] ?? `
|
|
6981
7000
|
|
|
6982
7001
|
`) + rebuilt[i];
|
|
6983
|
-
return
|
|
7002
|
+
return out;
|
|
6984
7003
|
}
|
|
6985
7004
|
function isListItemLine(line) {
|
|
6986
7005
|
const t = line.trimStart();
|
|
@@ -7183,7 +7202,7 @@ function backOffOpenInline(text, cut) {
|
|
|
7183
7202
|
}
|
|
7184
7203
|
return earliest;
|
|
7185
7204
|
}
|
|
7186
|
-
var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", INLINE_SPAN_PATTERNS;
|
|
7205
|
+
var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", PSEUDO_HEADING_MAX_CHARS = 48, INLINE_SPAN_PATTERNS;
|
|
7187
7206
|
var init_format = __esm(() => {
|
|
7188
7207
|
INLINE_SPAN_PATTERNS = [
|
|
7189
7208
|
/`[^`\n]+`/g,
|
|
@@ -12660,16 +12679,22 @@ var init_patterns = __esm(() => {
|
|
|
12660
12679
|
},
|
|
12661
12680
|
{
|
|
12662
12681
|
rule_id: "bearer_auth_header",
|
|
12663
|
-
regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/
|
|
12682
|
+
regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
|
|
12664
12683
|
captureIndex: 1,
|
|
12665
12684
|
slugHint: "bearer_token"
|
|
12666
12685
|
},
|
|
12667
12686
|
{
|
|
12668
12687
|
rule_id: "bearer_loose",
|
|
12669
|
-
regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/
|
|
12688
|
+
regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
|
|
12670
12689
|
captureIndex: 1,
|
|
12671
12690
|
slugHint: "bearer_token"
|
|
12672
12691
|
},
|
|
12692
|
+
{
|
|
12693
|
+
rule_id: "basic_auth_header",
|
|
12694
|
+
regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
|
|
12695
|
+
captureIndex: 1,
|
|
12696
|
+
slugHint: "basic_auth"
|
|
12697
|
+
},
|
|
12673
12698
|
{
|
|
12674
12699
|
rule_id: "pem_private_key",
|
|
12675
12700
|
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
@@ -12729,7 +12754,104 @@ function shannonEntropy(s) {
|
|
|
12729
12754
|
return h;
|
|
12730
12755
|
}
|
|
12731
12756
|
|
|
12757
|
+
// secret-detect/inert-values.ts
|
|
12758
|
+
function runIsCredentialShaped(run) {
|
|
12759
|
+
let classes = 0;
|
|
12760
|
+
if (/[a-z]/.test(run))
|
|
12761
|
+
classes++;
|
|
12762
|
+
if (/[A-Z]/.test(run))
|
|
12763
|
+
classes++;
|
|
12764
|
+
if (/[0-9]/.test(run))
|
|
12765
|
+
classes++;
|
|
12766
|
+
return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
|
|
12767
|
+
}
|
|
12768
|
+
function hasCredentialShapedRun(value) {
|
|
12769
|
+
for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
|
|
12770
|
+
if (runIsCredentialShaped(run))
|
|
12771
|
+
return true;
|
|
12772
|
+
}
|
|
12773
|
+
return false;
|
|
12774
|
+
}
|
|
12775
|
+
function isInertValue(value) {
|
|
12776
|
+
if (!INERT_VALUE_RE.some((re) => re.test(value)))
|
|
12777
|
+
return false;
|
|
12778
|
+
return !hasCredentialShapedRun(value);
|
|
12779
|
+
}
|
|
12780
|
+
function stripTrailingPunctuation(value) {
|
|
12781
|
+
return value.replace(TRAILING_PUNCT_RE, "");
|
|
12782
|
+
}
|
|
12783
|
+
var INERT_VALUE_RE, CREDENTIAL_RUN_RE, MIXED_CLASS_RUN_MIN = 12, SINGLE_CLASS_RUN_MIN = 16, INERT_GATED_RULES, TRAILING_PUNCT_RE;
|
|
12784
|
+
var init_inert_values = __esm(() => {
|
|
12785
|
+
INERT_VALUE_RE = [
|
|
12786
|
+
/^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
|
|
12787
|
+
/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
|
|
12788
|
+
/^\{\{[^\n\r\u2028\u2029]*\}\}$/,
|
|
12789
|
+
/^<[^<>\n\r\u2028\u2029]*>$/,
|
|
12790
|
+
/^%[A-Za-z_][A-Za-z0-9_]*%$/,
|
|
12791
|
+
/^vault:[A-Za-z0-9_./-]*$/i,
|
|
12792
|
+
/^[*x\u2022.]+$/i,
|
|
12793
|
+
/^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
|
|
12794
|
+
/^(?: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,
|
|
12795
|
+
/^[a-z]+(?: [a-z]+){2,}$/
|
|
12796
|
+
];
|
|
12797
|
+
CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
|
|
12798
|
+
INERT_GATED_RULES = new Set([
|
|
12799
|
+
"env_key_value",
|
|
12800
|
+
"json_secret_field",
|
|
12801
|
+
"cli_flag"
|
|
12802
|
+
]);
|
|
12803
|
+
TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
|
|
12804
|
+
});
|
|
12805
|
+
|
|
12732
12806
|
// secret-detect/kv-scanner.ts
|
|
12807
|
+
function charClassCount(value) {
|
|
12808
|
+
let n = 0;
|
|
12809
|
+
if (/[a-z]/.test(value))
|
|
12810
|
+
n++;
|
|
12811
|
+
if (/[A-Z]/.test(value))
|
|
12812
|
+
n++;
|
|
12813
|
+
if (/[0-9]/.test(value))
|
|
12814
|
+
n++;
|
|
12815
|
+
if (/[^A-Za-z0-9]/.test(value))
|
|
12816
|
+
n++;
|
|
12817
|
+
return n;
|
|
12818
|
+
}
|
|
12819
|
+
function looksLikeMemorablePassword(value) {
|
|
12820
|
+
if (isInertValue(value))
|
|
12821
|
+
return false;
|
|
12822
|
+
const core = stripTrailingPunctuation(value);
|
|
12823
|
+
if (core.length < 8 || core.length > 64)
|
|
12824
|
+
return false;
|
|
12825
|
+
if (isInertValue(core))
|
|
12826
|
+
return false;
|
|
12827
|
+
if (new Set(core).size < 4)
|
|
12828
|
+
return false;
|
|
12829
|
+
return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
|
|
12830
|
+
}
|
|
12831
|
+
function scanMemorablePasswords(text) {
|
|
12832
|
+
const hits = [];
|
|
12833
|
+
MEMORABLE_PW_RE.lastIndex = 0;
|
|
12834
|
+
let m;
|
|
12835
|
+
while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
|
|
12836
|
+
const keyName = m[1];
|
|
12837
|
+
const value = m[3];
|
|
12838
|
+
if (!value || !looksLikeMemorablePassword(value))
|
|
12839
|
+
continue;
|
|
12840
|
+
const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
|
|
12841
|
+
if (valueOffsetInMatch < 0)
|
|
12842
|
+
continue;
|
|
12843
|
+
const start = m.index + valueOffsetInMatch;
|
|
12844
|
+
hits.push({
|
|
12845
|
+
rule_id: MEMORABLE_PW_RULE_ID,
|
|
12846
|
+
start,
|
|
12847
|
+
end: start + value.length,
|
|
12848
|
+
matched_text: value,
|
|
12849
|
+
key_name: keyName,
|
|
12850
|
+
confidence: "ambiguous"
|
|
12851
|
+
});
|
|
12852
|
+
}
|
|
12853
|
+
return hits;
|
|
12854
|
+
}
|
|
12733
12855
|
function scanKeyValue(text) {
|
|
12734
12856
|
const hits = [];
|
|
12735
12857
|
KV_RE.lastIndex = 0;
|
|
@@ -12738,6 +12860,8 @@ function scanKeyValue(text) {
|
|
|
12738
12860
|
const [, keyName, value] = m;
|
|
12739
12861
|
if (!value)
|
|
12740
12862
|
continue;
|
|
12863
|
+
if (isInertValue(value))
|
|
12864
|
+
continue;
|
|
12741
12865
|
const h = shannonEntropy(value);
|
|
12742
12866
|
if (h < KV_ENTROPY_THRESHOLD)
|
|
12743
12867
|
continue;
|
|
@@ -12757,9 +12881,44 @@ function scanKeyValue(text) {
|
|
|
12757
12881
|
}
|
|
12758
12882
|
return hits;
|
|
12759
12883
|
}
|
|
12760
|
-
var KV_RE, KV_ENTROPY_THRESHOLD = 4;
|
|
12884
|
+
var KV_RE, KV_ENTROPY_THRESHOLD = 4, MEMORABLE_PW_RE, MEMORABLE_PW_RULE_ID = "memorable_password", MEMORABLE_PW_MIN_CLASSES = 2;
|
|
12761
12885
|
var init_kv_scanner = __esm(() => {
|
|
12886
|
+
init_inert_values();
|
|
12762
12887
|
KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
|
|
12888
|
+
MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
|
|
12889
|
+
});
|
|
12890
|
+
|
|
12891
|
+
// secret-detect/db-uri.ts
|
|
12892
|
+
function isInertPassword(value) {
|
|
12893
|
+
if (value.startsWith("[REDACTED"))
|
|
12894
|
+
return true;
|
|
12895
|
+
return /^[*x]+$/i.test(value);
|
|
12896
|
+
}
|
|
12897
|
+
function scanDbUris(text) {
|
|
12898
|
+
const hits = [];
|
|
12899
|
+
DB_URI_RE.lastIndex = 0;
|
|
12900
|
+
let m;
|
|
12901
|
+
while ((m = DB_URI_RE.exec(text)) !== null) {
|
|
12902
|
+
const scheme = m[1];
|
|
12903
|
+
const user = m[2];
|
|
12904
|
+
const password = m[3];
|
|
12905
|
+
if (isInertPassword(password))
|
|
12906
|
+
continue;
|
|
12907
|
+
const start = m.index + scheme.length + 3 + user.length + 1;
|
|
12908
|
+
hits.push({
|
|
12909
|
+
rule_id: DB_URI_RULE_ID,
|
|
12910
|
+
start,
|
|
12911
|
+
end: start + password.length,
|
|
12912
|
+
matched_text: password,
|
|
12913
|
+
key_name: `${scheme}_password`,
|
|
12914
|
+
confidence: "ambiguous"
|
|
12915
|
+
});
|
|
12916
|
+
}
|
|
12917
|
+
return hits;
|
|
12918
|
+
}
|
|
12919
|
+
var DB_URI_RE, DB_URI_RULE_ID = "db_uri_password";
|
|
12920
|
+
var init_db_uri = __esm(() => {
|
|
12921
|
+
DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
|
|
12763
12922
|
});
|
|
12764
12923
|
|
|
12765
12924
|
// secret-detect/generic-entropy.ts
|
|
@@ -12953,6 +13112,8 @@ function detectSecrets(text) {
|
|
|
12953
13112
|
const globalStart = win.offset + matchStart;
|
|
12954
13113
|
const globalEnd = globalStart + cap.length;
|
|
12955
13114
|
const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
|
|
13115
|
+
if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
|
|
13116
|
+
continue;
|
|
12956
13117
|
if (p.rule_id === "env_key_value") {
|
|
12957
13118
|
const ENV_KV_MIN_LEN = 12;
|
|
12958
13119
|
const ENV_KV_MIN_ENTROPY = 3.5;
|
|
@@ -12975,6 +13136,12 @@ function detectSecrets(text) {
|
|
|
12975
13136
|
for (const h of kvHits) {
|
|
12976
13137
|
raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
|
|
12977
13138
|
}
|
|
13139
|
+
for (const h of scanDbUris(win.text)) {
|
|
13140
|
+
raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
|
|
13141
|
+
}
|
|
13142
|
+
for (const h of scanMemorablePasswords(win.text)) {
|
|
13143
|
+
raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
|
|
13144
|
+
}
|
|
12978
13145
|
const genHits = scanGenericSecrets(win.text);
|
|
12979
13146
|
for (const h of genHits) {
|
|
12980
13147
|
raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
|
|
@@ -13024,6 +13191,8 @@ function dropOverlaps(hits) {
|
|
|
13024
13191
|
var init_secret_detect = __esm(() => {
|
|
13025
13192
|
init_patterns();
|
|
13026
13193
|
init_kv_scanner();
|
|
13194
|
+
init_db_uri();
|
|
13195
|
+
init_inert_values();
|
|
13027
13196
|
init_generic_entropy();
|
|
13028
13197
|
init_chunker();
|
|
13029
13198
|
init_suppressor();
|
|
@@ -71039,6 +71208,23 @@ function describeProviderCreditRemedy(provider) {
|
|
|
71039
71208
|
}
|
|
71040
71209
|
|
|
71041
71210
|
// operator-events.ts
|
|
71211
|
+
var OPERATOR_EVENT_KINDS = [
|
|
71212
|
+
"credentials-expired",
|
|
71213
|
+
"credentials-invalid",
|
|
71214
|
+
"proxy-misconfig",
|
|
71215
|
+
"credit-exhausted",
|
|
71216
|
+
"provider-credit-exhausted",
|
|
71217
|
+
"mcp-dependency-blocked",
|
|
71218
|
+
"quota-exhausted",
|
|
71219
|
+
"rate-limited",
|
|
71220
|
+
"agent-crashed",
|
|
71221
|
+
"agent-restarted-unexpectedly",
|
|
71222
|
+
"unknown-4xx",
|
|
71223
|
+
"unknown-5xx",
|
|
71224
|
+
"config-warning",
|
|
71225
|
+
"always-allow-persist-failed",
|
|
71226
|
+
"mental-model-persist-failed"
|
|
71227
|
+
];
|
|
71042
71228
|
function classifyClaudeError(raw) {
|
|
71043
71229
|
try {
|
|
71044
71230
|
return classifyInner(raw);
|
|
@@ -77648,7 +77834,12 @@ function normalizeOutboundBody(rawText, site, redact2, opts = {}) {
|
|
|
77648
77834
|
text4 = normalizeParagraphBreaks(text4);
|
|
77649
77835
|
text4 = redact2(text4, site);
|
|
77650
77836
|
if (!literalText) {
|
|
77651
|
-
let formatted = stripExcessBold(normalizePunctuation(text4))
|
|
77837
|
+
let formatted = stripExcessBold(normalizePunctuation(text4), (d) => {
|
|
77838
|
+
try {
|
|
77839
|
+
process.stderr.write(`telegram gateway: strip-excess-bold: fired site=${site} rule=${d.rule} ratio=${d.ratio.toFixed(3)}
|
|
77840
|
+
`);
|
|
77841
|
+
} catch {}
|
|
77842
|
+
});
|
|
77652
77843
|
if (addSpacers)
|
|
77653
77844
|
formatted = addParagraphSpacers(formatted);
|
|
77654
77845
|
text4 = formatted;
|
|
@@ -79094,7 +79285,12 @@ function normalizeOutboundBody2(rawText, site, redact2, opts = {}) {
|
|
|
79094
79285
|
text4 = normalizeParagraphBreaks(text4);
|
|
79095
79286
|
text4 = redact2(text4, site);
|
|
79096
79287
|
if (!literalText) {
|
|
79097
|
-
let formatted = stripExcessBold(normalizePunctuation(text4))
|
|
79288
|
+
let formatted = stripExcessBold(normalizePunctuation(text4), (d) => {
|
|
79289
|
+
try {
|
|
79290
|
+
process.stderr.write(`telegram gateway: strip-excess-bold: fired site=${site} rule=${d.rule} ratio=${d.ratio.toFixed(3)}
|
|
79291
|
+
`);
|
|
79292
|
+
} catch {}
|
|
79293
|
+
});
|
|
79098
79294
|
if (addSpacers)
|
|
79099
79295
|
formatted = addParagraphSpacers(formatted);
|
|
79100
79296
|
text4 = formatted;
|
|
@@ -83583,6 +83779,27 @@ var MODEL_ARG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._/\[\]-]{0,99}$/;
|
|
|
83583
83779
|
function isValidModelArg2(arg) {
|
|
83584
83780
|
return MODEL_ARG_RE2.test(arg);
|
|
83585
83781
|
}
|
|
83782
|
+
var DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS = 180000;
|
|
83783
|
+
var SESSION_MODEL_RESOLUTION_POLL_MS = 250;
|
|
83784
|
+
function resolveSessionModelResolutionTimeoutMs(raw) {
|
|
83785
|
+
if (raw == null || raw.trim() === "")
|
|
83786
|
+
return DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS;
|
|
83787
|
+
const value = Number(raw);
|
|
83788
|
+
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_SESSION_MODEL_RESOLUTION_TIMEOUT_MS;
|
|
83789
|
+
}
|
|
83790
|
+
async function waitForSessionModelResolution(input) {
|
|
83791
|
+
const pollMs = input.pollMs ?? SESSION_MODEL_RESOLUTION_POLL_MS;
|
|
83792
|
+
const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
83793
|
+
const startedAt = Date.now();
|
|
83794
|
+
for (;; ) {
|
|
83795
|
+
if (input.barrierExists())
|
|
83796
|
+
return true;
|
|
83797
|
+
const remaining = input.timeoutMs - (Date.now() - startedAt);
|
|
83798
|
+
if (remaining <= 0)
|
|
83799
|
+
return false;
|
|
83800
|
+
await sleep2(Math.min(pollMs, remaining));
|
|
83801
|
+
}
|
|
83802
|
+
}
|
|
83586
83803
|
function isSrModel2(name) {
|
|
83587
83804
|
return name.startsWith("sr-");
|
|
83588
83805
|
}
|
|
@@ -88847,17 +89064,7 @@ function recordWebhookEvent(rec, deps = {}) {
|
|
|
88847
89064
|
init_format();
|
|
88848
89065
|
import { renameSync as renameSync17, unlinkSync as unlinkSync20, chmodSync as chmodSync11 } from "fs";
|
|
88849
89066
|
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
|
-
]);
|
|
89067
|
+
var VALID_OPERATOR_KINDS = new Set(OPERATOR_EVENT_KINDS);
|
|
88861
89068
|
var AGENT_NAME_RE3 = /^[a-z0-9][a-z0-9_-]{0,50}$/;
|
|
88862
89069
|
var OPERATOR_EVENT_DETAIL_MAX = 1000;
|
|
88863
89070
|
function validateClientMessage(msg) {
|
|
@@ -99461,10 +99668,10 @@ function startOutboxSweep(deps) {
|
|
|
99461
99668
|
}
|
|
99462
99669
|
|
|
99463
99670
|
// ../src/build-info.ts
|
|
99464
|
-
var VERSION2 = "0.19.
|
|
99465
|
-
var COMMIT_SHA = "
|
|
99466
|
-
var COMMIT_DATE = "2026-07-
|
|
99467
|
-
var LATEST_PR =
|
|
99671
|
+
var VERSION2 = "0.19.36";
|
|
99672
|
+
var COMMIT_SHA = "ba505b15";
|
|
99673
|
+
var COMMIT_DATE = "2026-07-30T07:53:24Z";
|
|
99674
|
+
var LATEST_PR = 4019;
|
|
99468
99675
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
99469
99676
|
|
|
99470
99677
|
// gateway/boot-version.ts
|
|
@@ -112706,76 +112913,87 @@ async function startGateway() {
|
|
|
112706
112913
|
try {
|
|
112707
112914
|
const smAgentDir = resolveAgentDirFromEnv();
|
|
112708
112915
|
if (smAgentDir) {
|
|
112709
|
-
const
|
|
112710
|
-
|
|
112711
|
-
|
|
112712
|
-
|
|
112713
|
-
|
|
112714
|
-
|
|
112715
|
-
|
|
112716
|
-
|
|
112717
|
-
|
|
112718
|
-
|
|
112719
|
-
|
|
112720
|
-
|
|
112721
|
-
|
|
112722
|
-
|
|
112723
|
-
|
|
112724
|
-
|
|
112725
|
-
|
|
112726
|
-
|
|
112727
|
-
|
|
112728
|
-
|
|
112729
|
-
|
|
112730
|
-
|
|
112731
|
-
|
|
112732
|
-
|
|
112733
|
-
|
|
112734
|
-
|
|
112735
|
-
|
|
112736
|
-
|
|
112737
|
-
|
|
112738
|
-
|
|
112739
|
-
|
|
112740
|
-
|
|
112741
|
-
|
|
112742
|
-
|
|
112743
|
-
|
|
112916
|
+
const resolutionTimeoutMs = resolveSessionModelResolutionTimeoutMs(process.env.SWITCHROOM_SESSION_MODEL_RESOLUTION_TIMEOUT_MS);
|
|
112917
|
+
const resolved = await waitForSessionModelResolution({
|
|
112918
|
+
barrierExists: () => existsSync57(join68(smAgentDir, ".session-model-resolved")),
|
|
112919
|
+
timeoutMs: resolutionTimeoutMs
|
|
112920
|
+
});
|
|
112921
|
+
if (!resolved) {
|
|
112922
|
+
const target = modelSwitchReason == null ? "(none)" : parseModelSwitchTarget(modelSwitchReason) ?? "(unknown)";
|
|
112923
|
+
process.stderr.write(`telegram gateway: gw /model relaunch UNRESOLVED agent=${getMyAgentName()} target=${target} (barrier timeout after ${resolutionTimeoutMs}ms)
|
|
112924
|
+
`);
|
|
112925
|
+
} else {
|
|
112926
|
+
const activePath = join68(smAgentDir, ".active-session-model");
|
|
112927
|
+
if (existsSync57(activePath)) {
|
|
112928
|
+
try {
|
|
112929
|
+
const launched = readFileSync60(activePath, "utf8").trim();
|
|
112930
|
+
const configured = (() => {
|
|
112931
|
+
const d = switchroomExecJson(["agent", "list"]);
|
|
112932
|
+
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
112933
|
+
return resolveMainModel(raw ?? undefined);
|
|
112934
|
+
})();
|
|
112935
|
+
const isApplyBoot = launched.length > 0 && launched !== configured;
|
|
112936
|
+
sessionModelSource.setOverride(isApplyBoot ? launched : null, { verify: true });
|
|
112937
|
+
const modelBootCardDeps = {
|
|
112938
|
+
agent: getMyAgentName(),
|
|
112939
|
+
chat: modelSwitchMarkerChat,
|
|
112940
|
+
log: (line) => process.stderr.write(line),
|
|
112941
|
+
sendCard: (chatId, body, opts) => lockedBot.api.sendMessage(chatId, body, opts)
|
|
112942
|
+
};
|
|
112943
|
+
if (isApplyBoot) {
|
|
112944
|
+
sessionModelSource.setDivergenceHandler(buildServedModelDivergenceHandler(modelBootCardDeps));
|
|
112945
|
+
}
|
|
112946
|
+
const confirmation = modelSwitchReason != null ? classifyModelSwitchConfirmation({
|
|
112947
|
+
reason: modelSwitchReason,
|
|
112948
|
+
launched,
|
|
112949
|
+
configured
|
|
112950
|
+
}) : null;
|
|
112951
|
+
process.stderr.write(formatModelRelaunchDiagLog({
|
|
112952
|
+
agent: getMyAgentName(),
|
|
112953
|
+
launched,
|
|
112954
|
+
configured,
|
|
112744
112955
|
confirmation,
|
|
112745
|
-
|
|
112746
|
-
});
|
|
112747
|
-
|
|
112748
|
-
|
|
112749
|
-
|
|
112750
|
-
|
|
112751
|
-
|
|
112752
|
-
|
|
112753
|
-
|
|
112754
|
-
|
|
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;
|
|
112956
|
+
isApplyBoot
|
|
112957
|
+
}));
|
|
112958
|
+
if (confirmation != null) {
|
|
112959
|
+
deliverModelSwitchBootNotice({
|
|
112960
|
+
...modelBootCardDeps,
|
|
112961
|
+
confirmation,
|
|
112962
|
+
hasSessionModelAlert: existsSync57(join68(smAgentDir, ".session-model-alert"))
|
|
112963
|
+
});
|
|
112964
|
+
}
|
|
112965
|
+
} catch {}
|
|
112765
112966
|
}
|
|
112766
|
-
|
|
112767
|
-
|
|
112768
|
-
|
|
112769
|
-
|
|
112770
|
-
|
|
112771
|
-
|
|
112772
|
-
|
|
112773
|
-
|
|
112774
|
-
|
|
112775
|
-
|
|
112967
|
+
const activeEffortPath = join68(smAgentDir, ".active-session-effort");
|
|
112968
|
+
if (existsSync57(activeEffortPath)) {
|
|
112969
|
+
try {
|
|
112970
|
+
const launchedEffort = readFileSync60(activeEffortPath, "utf8").trim();
|
|
112971
|
+
const configuredEffort = getConfiguredEffortForPersist();
|
|
112972
|
+
sessionEffortOverride = launchedEffort.length > 0 && launchedEffort !== configuredEffort ? launchedEffort : null;
|
|
112973
|
+
} catch {}
|
|
112974
|
+
}
|
|
112975
|
+
const alertPath = join68(smAgentDir, ".session-model-alert");
|
|
112976
|
+
if (existsSync57(alertPath)) {
|
|
112977
|
+
let alertText = null;
|
|
112978
|
+
try {
|
|
112979
|
+
alertText = readFileSync60(alertPath, "utf8").trim();
|
|
112980
|
+
} catch {
|
|
112981
|
+
alertText = null;
|
|
112776
112982
|
}
|
|
112777
|
-
|
|
112983
|
+
try {
|
|
112984
|
+
unlinkSync30(alertPath);
|
|
112985
|
+
} catch {}
|
|
112986
|
+
if (alertText && alertText.length > 0) {
|
|
112987
|
+
const operators = loadAccess().allowFrom;
|
|
112988
|
+
for (const operator of operators) {
|
|
112989
|
+
if (!operator)
|
|
112990
|
+
continue;
|
|
112991
|
+
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)}
|
|
112992
|
+
`));
|
|
112993
|
+
}
|
|
112994
|
+
process.stderr.write(`telegram gateway: session-model: LiteLLM-down override drop \u2014 ${alertText}
|
|
112778
112995
|
`);
|
|
112996
|
+
}
|
|
112779
112997
|
}
|
|
112780
112998
|
}
|
|
112781
112999
|
}
|
|
@@ -761,6 +761,35 @@ function isFullyBolded(fragment: string): boolean {
|
|
|
761
761
|
return /^\*\*[^*]+\*\*[.,:;!?]?$/.test(fragment.trim())
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Max length (markers included) of a standalone fully-bolded line that is
|
|
766
|
+
* treated as a legitimate pseudo-heading (`**Section**`) rather than an
|
|
767
|
+
* over-bolded paragraph. Single source of truth for BOTH the per-block rule
|
|
768
|
+
* and the global-ratio heading exemption.
|
|
769
|
+
*/
|
|
770
|
+
const PSEUDO_HEADING_MAX_CHARS = 48
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* True when a blank-line-delimited block is a single-line pseudo-heading: one
|
|
774
|
+
* non-empty line that is a single fully-bolded span of ≤PSEUDO_HEADING_MAX_CHARS
|
|
775
|
+
* characters (`**Summary**`, `**Next steps:**`). Multi-line blocks are never
|
|
776
|
+
* headings, so this can never mislabel a bolded paragraph as exempt.
|
|
777
|
+
*/
|
|
778
|
+
function isPseudoHeadingBlock(block: string): boolean {
|
|
779
|
+
const lines = block.split('\n').filter((l) => l.trim() !== '')
|
|
780
|
+
if (lines.length !== 1) return false
|
|
781
|
+
const t = lines[0].trim()
|
|
782
|
+
return t.length <= PSEUDO_HEADING_MAX_CHARS && isFullyBolded(t)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Diagnostic emitted when the over-bold tripwire actually removes bold. */
|
|
786
|
+
export interface ExcessBoldStripDiagnostic {
|
|
787
|
+
/** Which rule fired: the whole-message ratio, or per-block flattening. */
|
|
788
|
+
rule: 'global' | 'per-block'
|
|
789
|
+
/** Measured bold ratio: bold chars / visible (non-code) chars. */
|
|
790
|
+
ratio: number
|
|
791
|
+
}
|
|
792
|
+
|
|
764
793
|
/** Strip `**bold**` markers from a fragment, keeping the text. */
|
|
765
794
|
function unbold(fragment: string): string {
|
|
766
795
|
return fragment.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
@@ -778,16 +807,24 @@ function unbold(fragment: string): string {
|
|
|
778
807
|
* Deliberately conservative:
|
|
779
808
|
* - Messages under 100 non-code characters are exempt (a short reply whose
|
|
780
809
|
* one key fact is bolded is exactly the house style).
|
|
781
|
-
* - A single-line fully-bolded paragraph of ≤
|
|
782
|
-
* pseudo-heading (the "**Section**" label the fleet style
|
|
783
|
-
* is NOT stripped by the per-block rule
|
|
784
|
-
* global ratio
|
|
810
|
+
* - A single-line fully-bolded paragraph of ≤PSEUDO_HEADING_MAX_CHARS chars
|
|
811
|
+
* is treated as a pseudo-heading (the "**Section**" label the fleet style
|
|
812
|
+
* encourages) and is NOT stripped — neither by the per-block rule NOR by
|
|
813
|
+
* the global-ratio rule (it still counts toward the global ratio, so a
|
|
814
|
+
* genuinely over-bolded message still trips, but its section headings
|
|
815
|
+
* survive instead of the whole reply going plain).
|
|
785
816
|
* - A list with any non-fully-bolded item is left alone.
|
|
786
817
|
*
|
|
787
818
|
* Code spans/fences are masked (maskCodeRegions) and never counted or
|
|
788
819
|
* modified. Idempotent: stripped output has no `**` spans left to trip on.
|
|
820
|
+
*
|
|
821
|
+
* `onStrip` is an optional diagnostic sink invoked exactly once, only when
|
|
822
|
+
* bold is actually removed, carrying which rule fired and the measured ratio.
|
|
789
823
|
*/
|
|
790
|
-
export function stripExcessBold(
|
|
824
|
+
export function stripExcessBold(
|
|
825
|
+
text: string,
|
|
826
|
+
onStrip?: (d: ExcessBoldStripDiagnostic) => void,
|
|
827
|
+
): string {
|
|
791
828
|
if (!text.includes('**')) return text
|
|
792
829
|
|
|
793
830
|
const nonce = Math.random().toString(36).slice(2)
|
|
@@ -800,14 +837,24 @@ export function stripExcessBold(text: string): string {
|
|
|
800
837
|
|
|
801
838
|
let boldChars = 0
|
|
802
839
|
for (const m of visible.matchAll(/\*\*([^*]+)\*\*/g)) boldChars += m[1].length
|
|
840
|
+
const ratio = boldChars / visible.length
|
|
841
|
+
|
|
842
|
+
// Blank-line-delimited blocks of the masked text (shared by both rules).
|
|
843
|
+
const blocks = masked.split(/\n{2,}/)
|
|
803
844
|
|
|
804
|
-
if (
|
|
805
|
-
// Clearly over-bolded — strip every bold span,
|
|
806
|
-
|
|
845
|
+
if (ratio > 0.3) {
|
|
846
|
+
// Clearly over-bolded — strip every bold span, keeping the text, EXCEPT
|
|
847
|
+
// standalone short pseudo-heading blocks (`**Section**`), which stay bold
|
|
848
|
+
// so a bold-dense digest keeps its section headings.
|
|
849
|
+
const rebuilt = blocks.map((block) =>
|
|
850
|
+
isPseudoHeadingBlock(block) ? block : unbold(block),
|
|
851
|
+
)
|
|
852
|
+
const out = rejoinBlocks(masked, rebuilt)
|
|
853
|
+
if (out !== masked) onStrip?.({ rule: 'global', ratio })
|
|
854
|
+
return restore(out)
|
|
807
855
|
}
|
|
808
856
|
|
|
809
|
-
// Per-block check on
|
|
810
|
-
const blocks = masked.split(/\n{2,}/)
|
|
857
|
+
// Per-block check on the same blocks.
|
|
811
858
|
const rebuilt = blocks.map((block) => {
|
|
812
859
|
const lines = block.split('\n').filter((l) => l.trim() !== '')
|
|
813
860
|
if (lines.length === 0 || !block.includes('**')) return block
|
|
@@ -824,17 +871,25 @@ export function stripExcessBold(text: string): string {
|
|
|
824
871
|
const isProseBlock = lines.every((l) => !isMarkerLine(l, placeholder))
|
|
825
872
|
if (!isProseBlock) return block
|
|
826
873
|
if (!lines.every((l) => isFullyBolded(l))) return block
|
|
827
|
-
if (
|
|
874
|
+
if (isPseudoHeadingBlock(block)) return block
|
|
828
875
|
return unbold(block)
|
|
829
876
|
})
|
|
830
877
|
|
|
831
|
-
|
|
832
|
-
|
|
878
|
+
const out = rejoinBlocks(masked, rebuilt)
|
|
879
|
+
if (out !== masked) onStrip?.({ rule: 'per-block', ratio })
|
|
880
|
+
return restore(out)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Rejoin blocks produced by `masked.split(/\n{2,}/)` after per-block mapping,
|
|
885
|
+
* restoring the ORIGINAL blank-gap shapes (split() drops the separators, so
|
|
886
|
+
* re-capture them from the masked source and interleave).
|
|
887
|
+
*/
|
|
888
|
+
function rejoinBlocks(masked: string, rebuilt: string[]): string {
|
|
833
889
|
const seps = masked.match(/\n{2,}/g) ?? []
|
|
834
890
|
let out = rebuilt[0] ?? ''
|
|
835
891
|
for (let i = 1; i < rebuilt.length; i++) out += (seps[i - 1] ?? '\n\n') + rebuilt[i]
|
|
836
|
-
|
|
837
|
-
return restore(out)
|
|
892
|
+
return out
|
|
838
893
|
}
|
|
839
894
|
|
|
840
895
|
// ---------------------------------------------------------------------------
|