switchroom 0.19.34 → 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.
Files changed (35) hide show
  1. package/dist/auth-broker/index.js +7 -1
  2. package/dist/cli/skill-validate-pretool.mjs +15 -2
  3. package/dist/cli/switchroom.js +1005 -482
  4. package/dist/host-control/main.js +156 -4
  5. package/dist/vault/approvals/kernel-server.js +7 -1
  6. package/dist/vault/broker/server.js +7 -1
  7. package/package.json +4 -2
  8. package/profiles/_base/start.sh.hbs +37 -12
  9. package/telegram-plugin/dist/gateway/gateway.js +476 -116
  10. package/telegram-plugin/format.ts +70 -15
  11. package/telegram-plugin/gateway/gateway.ts +27 -31
  12. package/telegram-plugin/gateway/ipc-server.ts +18 -15
  13. package/telegram-plugin/gateway/model-command.ts +279 -23
  14. package/telegram-plugin/gateway/outbound-send-path.ts +12 -1
  15. package/telegram-plugin/operator-events.ts +40 -16
  16. package/telegram-plugin/secret-detect/db-uri.ts +90 -0
  17. package/telegram-plugin/secret-detect/index.ts +24 -1
  18. package/telegram-plugin/secret-detect/inert-values.ts +147 -0
  19. package/telegram-plugin/secret-detect/kv-scanner.ts +108 -0
  20. package/telegram-plugin/secret-detect/patterns.ts +24 -4
  21. package/telegram-plugin/tests/format-consistency.test.ts +93 -0
  22. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +40 -2
  23. package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +20 -11
  24. package/telegram-plugin/tests/model-command.test.ts +415 -4
  25. package/telegram-plugin/tests/outbound-send-path.test.ts +1 -1
  26. package/telegram-plugin/tests/secret-detect-cross-engine.test.ts +263 -0
  27. package/telegram-plugin/tests/secret-detect-write-path.test.ts +403 -0
  28. package/telegram-plugin/tests/turn-flush-safety.test.ts +2 -2
  29. package/vendor/hindsight-memory/scripts/lib/client.py +58 -0
  30. package/vendor/hindsight-memory/scripts/lib/secret_patterns.json +431 -0
  31. package/vendor/hindsight-memory/scripts/lib/secret_redact.py +563 -0
  32. package/vendor/hindsight-memory/scripts/lib/secret_redaction_vectors.json +397 -0
  33. package/vendor/hindsight-memory/scripts/subagent_retain.py +103 -7
  34. package/vendor/hindsight-memory/scripts/tests/test_secret_redact.py +522 -0
  35. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain_learnings.py +377 -0
@@ -21311,7 +21311,7 @@ function allocateAgentUid(name) {
21311
21311
  }
21312
21312
 
21313
21313
  // src/build-info.ts
21314
- var VERSION = "0.19.34";
21314
+ var VERSION = "0.19.36";
21315
21315
 
21316
21316
  // src/setup/hindsight-recall-tunables.ts
21317
21317
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -21323,6 +21323,9 @@ init_schema();
21323
21323
  // src/config/users.ts
21324
21324
  init_merge();
21325
21325
 
21326
+ // src/config/thinking-effort-risk.ts
21327
+ var RISKY_EFFORTS = new Set(["medium", "high", "xhigh", "max"]);
21328
+
21326
21329
  // src/agents/scaffold.ts
21327
21330
  init_merge();
21328
21331
  init_timezone();
@@ -21444,12 +21447,15 @@ function pgMib(mib) {
21444
21447
  }
21445
21448
  var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
21446
21449
  var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
21450
+ var HINDSIGHT_PG_FSYNC_ENV = "SWITCHROOM_HINDSIGHT_PG_FSYNC";
21451
+ var HINDSIGHT_PG_DEFAULT_FSYNC = "on";
21447
21452
  var HINDSIGHT_PG_DEFAULTS = [
21448
21453
  [
21449
21454
  HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
21450
21455
  pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
21451
21456
  ],
21452
- [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
21457
+ [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)],
21458
+ [HINDSIGHT_PG_FSYNC_ENV, HINDSIGHT_PG_DEFAULT_FSYNC]
21453
21459
  ];
21454
21460
  var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
21455
21461
 
@@ -27284,16 +27290,22 @@ var STRUCTURED_PATTERNS = [
27284
27290
  },
27285
27291
  {
27286
27292
  rule_id: "bearer_auth_header",
27287
- regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/g,
27293
+ regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
27288
27294
  captureIndex: 1,
27289
27295
  slugHint: "bearer_token"
27290
27296
  },
27291
27297
  {
27292
27298
  rule_id: "bearer_loose",
27293
- regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/g,
27299
+ regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
27294
27300
  captureIndex: 1,
27295
27301
  slugHint: "bearer_token"
27296
27302
  },
27303
+ {
27304
+ rule_id: "basic_auth_header",
27305
+ regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
27306
+ captureIndex: 1,
27307
+ slugHint: "basic_auth"
27308
+ },
27297
27309
  {
27298
27310
  rule_id: "pem_private_key",
27299
27311
  regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
@@ -27352,9 +27364,108 @@ function shannonEntropy(s) {
27352
27364
  return h;
27353
27365
  }
27354
27366
 
27367
+ // telegram-plugin/secret-detect/inert-values.ts
27368
+ var INERT_VALUE_RE = [
27369
+ /^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
27370
+ /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
27371
+ /^\{\{[^\n\r\u2028\u2029]*\}\}$/,
27372
+ /^<[^<>\n\r\u2028\u2029]*>$/,
27373
+ /^%[A-Za-z_][A-Za-z0-9_]*%$/,
27374
+ /^vault:[A-Za-z0-9_./-]*$/i,
27375
+ /^[*x•.]+$/i,
27376
+ /^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
27377
+ /^(?: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,
27378
+ /^[a-z]+(?: [a-z]+){2,}$/
27379
+ ];
27380
+ var CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
27381
+ var MIXED_CLASS_RUN_MIN = 12;
27382
+ var SINGLE_CLASS_RUN_MIN = 16;
27383
+ function runIsCredentialShaped(run) {
27384
+ let classes = 0;
27385
+ if (/[a-z]/.test(run))
27386
+ classes++;
27387
+ if (/[A-Z]/.test(run))
27388
+ classes++;
27389
+ if (/[0-9]/.test(run))
27390
+ classes++;
27391
+ return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
27392
+ }
27393
+ function hasCredentialShapedRun(value) {
27394
+ for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
27395
+ if (runIsCredentialShaped(run))
27396
+ return true;
27397
+ }
27398
+ return false;
27399
+ }
27400
+ function isInertValue(value) {
27401
+ if (!INERT_VALUE_RE.some((re) => re.test(value)))
27402
+ return false;
27403
+ return !hasCredentialShapedRun(value);
27404
+ }
27405
+ var INERT_GATED_RULES = new Set([
27406
+ "env_key_value",
27407
+ "json_secret_field",
27408
+ "cli_flag"
27409
+ ]);
27410
+ var TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
27411
+ function stripTrailingPunctuation(value) {
27412
+ return value.replace(TRAILING_PUNCT_RE, "");
27413
+ }
27414
+
27355
27415
  // telegram-plugin/secret-detect/kv-scanner.ts
27356
27416
  var KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
27357
27417
  var KV_ENTROPY_THRESHOLD = 4;
27418
+ var MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
27419
+ var MEMORABLE_PW_RULE_ID = "memorable_password";
27420
+ var MEMORABLE_PW_MIN_CLASSES = 2;
27421
+ function charClassCount(value) {
27422
+ let n = 0;
27423
+ if (/[a-z]/.test(value))
27424
+ n++;
27425
+ if (/[A-Z]/.test(value))
27426
+ n++;
27427
+ if (/[0-9]/.test(value))
27428
+ n++;
27429
+ if (/[^A-Za-z0-9]/.test(value))
27430
+ n++;
27431
+ return n;
27432
+ }
27433
+ function looksLikeMemorablePassword(value) {
27434
+ if (isInertValue(value))
27435
+ return false;
27436
+ const core = stripTrailingPunctuation(value);
27437
+ if (core.length < 8 || core.length > 64)
27438
+ return false;
27439
+ if (isInertValue(core))
27440
+ return false;
27441
+ if (new Set(core).size < 4)
27442
+ return false;
27443
+ return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
27444
+ }
27445
+ function scanMemorablePasswords(text) {
27446
+ const hits = [];
27447
+ MEMORABLE_PW_RE.lastIndex = 0;
27448
+ let m;
27449
+ while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
27450
+ const keyName = m[1];
27451
+ const value = m[3];
27452
+ if (!value || !looksLikeMemorablePassword(value))
27453
+ continue;
27454
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
27455
+ if (valueOffsetInMatch < 0)
27456
+ continue;
27457
+ const start = m.index + valueOffsetInMatch;
27458
+ hits.push({
27459
+ rule_id: MEMORABLE_PW_RULE_ID,
27460
+ start,
27461
+ end: start + value.length,
27462
+ matched_text: value,
27463
+ key_name: keyName,
27464
+ confidence: "ambiguous"
27465
+ });
27466
+ }
27467
+ return hits;
27468
+ }
27358
27469
  function scanKeyValue(text) {
27359
27470
  const hits = [];
27360
27471
  KV_RE.lastIndex = 0;
@@ -27363,6 +27474,8 @@ function scanKeyValue(text) {
27363
27474
  const [, keyName, value] = m;
27364
27475
  if (!value)
27365
27476
  continue;
27477
+ if (isInertValue(value))
27478
+ continue;
27366
27479
  const h = shannonEntropy(value);
27367
27480
  if (h < KV_ENTROPY_THRESHOLD)
27368
27481
  continue;
@@ -27383,6 +27496,37 @@ function scanKeyValue(text) {
27383
27496
  return hits;
27384
27497
  }
27385
27498
 
27499
+ // telegram-plugin/secret-detect/db-uri.ts
27500
+ var DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
27501
+ var DB_URI_RULE_ID = "db_uri_password";
27502
+ function isInertPassword(value) {
27503
+ if (value.startsWith("[REDACTED"))
27504
+ return true;
27505
+ return /^[*x]+$/i.test(value);
27506
+ }
27507
+ function scanDbUris(text) {
27508
+ const hits = [];
27509
+ DB_URI_RE.lastIndex = 0;
27510
+ let m;
27511
+ while ((m = DB_URI_RE.exec(text)) !== null) {
27512
+ const scheme = m[1];
27513
+ const user = m[2];
27514
+ const password = m[3];
27515
+ if (isInertPassword(password))
27516
+ continue;
27517
+ const start = m.index + scheme.length + 3 + user.length + 1;
27518
+ hits.push({
27519
+ rule_id: DB_URI_RULE_ID,
27520
+ start,
27521
+ end: start + password.length,
27522
+ matched_text: password,
27523
+ key_name: `${scheme}_password`,
27524
+ confidence: "ambiguous"
27525
+ });
27526
+ }
27527
+ return hits;
27528
+ }
27529
+
27386
27530
  // telegram-plugin/secret-detect/generic-entropy.ts
27387
27531
  var CANDIDATE_RE = /[A-Za-z0-9]{28,}/g;
27388
27532
  var GENERIC_MIN_DISTINCT = 18;
@@ -27556,6 +27700,8 @@ function detectSecrets(text) {
27556
27700
  const globalStart = win.offset + matchStart;
27557
27701
  const globalEnd = globalStart + cap.length;
27558
27702
  const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
27703
+ if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
27704
+ continue;
27559
27705
  if (p.rule_id === "env_key_value") {
27560
27706
  const ENV_KV_MIN_LEN = 12;
27561
27707
  const ENV_KV_MIN_ENTROPY = 3.5;
@@ -27578,6 +27724,12 @@ function detectSecrets(text) {
27578
27724
  for (const h of kvHits) {
27579
27725
  raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
27580
27726
  }
27727
+ for (const h of scanDbUris(win.text)) {
27728
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
27729
+ }
27730
+ for (const h of scanMemorablePasswords(win.text)) {
27731
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
27732
+ }
27581
27733
  const genHits = scanGenericSecrets(win.text);
27582
27734
  for (const h of genHits) {
27583
27735
  raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
@@ -19220,6 +19220,9 @@ init_schema();
19220
19220
  // src/config/users.ts
19221
19221
  init_merge();
19222
19222
 
19223
+ // src/config/thinking-effort-risk.ts
19224
+ var RISKY_EFFORTS = new Set(["medium", "high", "xhigh", "max"]);
19225
+
19223
19226
  // src/agents/scaffold.ts
19224
19227
  init_merge();
19225
19228
  init_timezone();
@@ -19341,12 +19344,15 @@ function pgMib(mib) {
19341
19344
  }
19342
19345
  var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
19343
19346
  var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
19347
+ var HINDSIGHT_PG_FSYNC_ENV = "SWITCHROOM_HINDSIGHT_PG_FSYNC";
19348
+ var HINDSIGHT_PG_DEFAULT_FSYNC = "on";
19344
19349
  var HINDSIGHT_PG_DEFAULTS = [
19345
19350
  [
19346
19351
  HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
19347
19352
  pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
19348
19353
  ],
19349
- [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
19354
+ [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)],
19355
+ [HINDSIGHT_PG_FSYNC_ENV, HINDSIGHT_PG_DEFAULT_FSYNC]
19350
19356
  ];
19351
19357
  var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
19352
19358
 
@@ -19461,6 +19461,9 @@ init_schema();
19461
19461
  // src/config/users.ts
19462
19462
  init_merge();
19463
19463
 
19464
+ // src/config/thinking-effort-risk.ts
19465
+ var RISKY_EFFORTS = new Set(["medium", "high", "xhigh", "max"]);
19466
+
19464
19467
  // src/agents/scaffold.ts
19465
19468
  init_merge();
19466
19469
  init_timezone();
@@ -19754,12 +19757,15 @@ function pgMib(mib) {
19754
19757
  }
19755
19758
  var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
19756
19759
  var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
19760
+ var HINDSIGHT_PG_FSYNC_ENV = "SWITCHROOM_HINDSIGHT_PG_FSYNC";
19761
+ var HINDSIGHT_PG_DEFAULT_FSYNC = "on";
19757
19762
  var HINDSIGHT_PG_DEFAULTS = [
19758
19763
  [
19759
19764
  HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
19760
19765
  pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
19761
19766
  ],
19762
- [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
19767
+ [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)],
19768
+ [HINDSIGHT_PG_FSYNC_ENV, HINDSIGHT_PG_DEFAULT_FSYNC]
19763
19769
  ];
19764
19770
  var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
19765
19771
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.34",
4
+ "version": "0.19.36",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,8 +28,10 @@
28
28
  "test:vitest": "vitest run",
29
29
  "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs",
31
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts",
32
32
  "lint:tsc": "tsc --noEmit",
33
+ "lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
34
+ "lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
33
35
  "lint:plugin-references": "node scripts/check-plugin-references.mjs",
34
36
  "lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
35
37
  "lint:bun-test-imports": "node scripts/check-bun-test-imports.mjs",
@@ -390,6 +390,10 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
390
390
  # ephemeral). Refresh-or-remove every boot so a stale snapshot can never
391
391
  # re-apply a consumed override on a later restart.
392
392
  rm -f "{{agentDir}}/.session-model.boot-snapshot" "{{agentDir}}/.session-model-boot-attempts.boot-snapshot"
393
+ # Resolution runs later, after the bounded LiteLLM probe. Remove the prior
394
+ # boot's completion barrier before forking the gateway so it cannot consume
395
+ # stale active-model/effort/alert files while this boot is still resolving.
396
+ rm -f "{{agentDir}}/.session-model-resolved"
393
397
  if [ -f "{{agentDir}}/.session-model" ]; then
394
398
  # A failed copy (ENOSPC, perms) must not be a SILENT drop — the resolution
395
399
  # block still falls back to the live carrier, but the gateway may consume
@@ -1954,6 +1958,13 @@ rm -f "{{agentDir}}/.session-model.boot-snapshot" "{{agentDir}}/.session-model-b
1954
1958
  # this guard covers only the configured default, which the live-config read
1955
1959
  # above makes a mainline path (pinning `model: fable` in yaml is the normal
1956
1960
  # way to run Fable).
1961
+ #
1962
+ # Snapshot the resolved model BEFORE this guard can rewrite it. _DECLARED_
1963
+ # ROUTING (below) must reflect this boot's actual INTENT — the resolved
1964
+ # override or configured default — not a post-demotion landing, or the exact
1965
+ # divergence this file exists to catch (missing key silently demoting
1966
+ # fable → opus) becomes invisible to the check built to catch it.
1967
+ _DECLARED_MODEL="$_EFFECTIVE_MODEL"
1957
1968
  case "$_EFFECTIVE_MODEL" in
1958
1969
  fable|claude-fable-5)
1959
1970
  if [ -z "$_LITELLM_OK" ]; then
@@ -2080,23 +2091,27 @@ export SWITCHROOM_SESSION_MODEL="$_EFFECTIVE_MODEL"
2080
2091
  #
2081
2092
  # Deterministic record of WHERE this boot actually landed, written immediately
2082
2093
  # before exec claude (overwrite-per-boot, one line). Consumers: the gateway
2083
- # boot card's "routing" row and any operator shell. _DECLARED_ROUTING is baked
2084
- # at render time by declaredRoutingMode() — the POST-REPOINT mode this boot is
2085
- # meant to land on (fable/claude-fable-5/sr-* declare router-root because the
2086
- # repoint case below moves them there; only non-fable Claude models declare
2087
- # passthrough) — the apply-time INTENT the landed mode is compared against.
2094
+ # boot card's "routing" row and any operator shell. _DECLARED_ROUTING is derived
2095
+ # from $_DECLARED_MODEL (the resolved override/default snapshotted BEFORE the
2096
+ # missing-key opus demotion above), so a legitimate session override is
2097
+ # compared against its own POST-REPOINT intent rather than the configured
2098
+ # default's render-time model class — and a missing-key demotion still reads
2099
+ # as a genuine divergence instead of chasing its own landed model.
2088
2100
  # Divergence (landed ≠ declared) appends a consume-once .session-model-alert
2089
2101
  # line the gateway relays to the operator, but ONLY when the (landed, declared)
2090
2102
  # PAIR differs from the PREVIOUS boot's pair — a persistently degraded agent
2091
2103
  # alerts once, not on every restart, yet a declared-only flip (operator changes
2092
2104
  # the configured model class while the agent stays stuck on the same wrong
2093
2105
  # landed mode) still re-alerts.
2094
- _DECLARED_ROUTING={{{declaredRoutingQ}}}
2095
- # When LiteLLM was never configured for this agent (no SWITCHROOM_LITELLM at
2096
- # boot entry, captured before any strip), compose injects NO routing env — the
2097
- # declared intent genuinely IS direct-oauth, not the model-class bake. This
2098
- # keeps non-LiteLLM fleets from false-alerting divergence on every boot.
2099
- if [ -z "${SWITCHROOM_LITELLM_DECLARED:-}" ]; then _DECLARED_ROUTING="direct-oauth"; fi
2106
+ if [ -z "${SWITCHROOM_LITELLM_DECLARED:-}" ]; then
2107
+ # No LiteLLM routing was configured for this agent at boot entry.
2108
+ _DECLARED_ROUTING="direct-oauth"
2109
+ else
2110
+ case "$_DECLARED_MODEL" in
2111
+ sr-*|fable|claude-fable-5) _DECLARED_ROUTING="router-root" ;;
2112
+ *) _DECLARED_ROUTING="passthrough" ;;
2113
+ esac
2114
+ fi
2100
2115
  case "${ANTHROPIC_BASE_URL:-}" in
2101
2116
  "") _ROUTING_MODE="direct-oauth" ;;
2102
2117
  */anthropic) _ROUTING_MODE="passthrough" ;;
@@ -2114,7 +2129,17 @@ echo "routing: $_routing_line" >&2
2114
2129
  if [ "$_ROUTING_MODE" != "$_DECLARED_ROUTING" ] && { [ "$_ROUTING_MODE" != "$_prev_routing_mode" ] || [ "$_DECLARED_ROUTING" != "$_prev_declared_routing" ]; }; then
2115
2130
  printf 'Routing divergence: this agent landed on `%s` (base `%s`, model `%s`) but its declared routing is `%s`. Check `%s/.routing-mode` and the boot log for the cause (LiteLLM key/provision or proxy reachability).\n' "$_ROUTING_MODE" "${ANTHROPIC_BASE_URL:-direct}" "$_EFFECTIVE_MODEL" "$_DECLARED_ROUTING" "{{agentDir}}" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
2116
2131
  fi
2117
- unset _prev_routing_mode _prev_declared_routing _routing_line
2132
+ unset _prev_routing_mode _prev_declared_routing _routing_line _DECLARED_MODEL
2133
+
2134
+ # Publish completion only after every boot-resolution output is final: active
2135
+ # model, active effort, routing mode, and any alert append. Atomic rename keeps
2136
+ # the gateway from observing a partially-written barrier.
2137
+ _session_model_resolved_tmp="{{agentDir}}/.session-model-resolved.tmp.$$"
2138
+ if printf '%s\n' "$$" > "$_session_model_resolved_tmp" 2>/dev/null; then
2139
+ mv -f "$_session_model_resolved_tmp" "{{agentDir}}/.session-model-resolved" 2>/dev/null || true
2140
+ fi
2141
+ rm -f "$_session_model_resolved_tmp"
2142
+ unset _session_model_resolved_tmp
2118
2143
 
2119
2144
  {{#if useSwitchroomPlugin}}
2120
2145
  if [ -n "$APPEND_PROMPT" ]; then