switchroom 0.19.0 → 0.19.2

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 (34) hide show
  1. package/dist/agent-scheduler/index.js +29 -1
  2. package/dist/auth-broker/index.js +552 -48
  3. package/dist/cli/autoaccept-poll.js +29 -1
  4. package/dist/cli/drive-write-pretool.mjs +30 -2
  5. package/dist/cli/ms-365-write-pretool.mjs +30 -2
  6. package/dist/cli/switchroom.js +751 -36
  7. package/dist/host-control/main.js +3 -3
  8. package/dist/vault/approvals/kernel-server.js +2 -2
  9. package/dist/vault/broker/server.js +2 -2
  10. package/package.json +3 -2
  11. package/profiles/_base/start.sh.hbs +1 -0
  12. package/skills/switchroom-cli/SKILL.md +25 -0
  13. package/telegram-plugin/auth-snapshot-format.ts +39 -0
  14. package/telegram-plugin/dist/gateway/gateway.js +363 -25
  15. package/telegram-plugin/external-spend.ts +135 -0
  16. package/telegram-plugin/gateway/gateway.ts +83 -67
  17. package/telegram-plugin/gateway/model-command.ts +106 -0
  18. package/telegram-plugin/gateway/narrative-lane.ts +23 -9
  19. package/telegram-plugin/gateway/status-pin-store.ts +64 -4
  20. package/telegram-plugin/gateway/usage-mask.ts +29 -0
  21. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +19 -2
  22. package/telegram-plugin/quota-bar-format.ts +18 -0
  23. package/telegram-plugin/quota-check.ts +17 -2
  24. package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
  25. package/telegram-plugin/tests/external-spend.test.ts +168 -0
  26. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +57 -23
  27. package/telegram-plugin/tests/quota-bar-format.test.ts +43 -0
  28. package/telegram-plugin/tests/quota-check.test.ts +57 -0
  29. package/telegram-plugin/tests/status-pin-store.test.ts +198 -0
  30. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +50 -0
  31. package/telegram-plugin/tests/usage-footer-freshness.test.ts +141 -0
  32. package/telegram-plugin/tests/usage-mask.test.ts +35 -0
  33. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +27 -0
  34. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +131 -1
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.0", COMMIT_SHA = "9709a540";
2123
+ var VERSION = "0.19.2", COMMIT_SHA = "1fa69736";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -15452,14 +15452,27 @@ __export(exports_atomic, {
15452
15452
  atomicWriteFileSync: () => atomicWriteFileSync
15453
15453
  });
15454
15454
  import { randomBytes } from "node:crypto";
15455
- import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
15456
- function atomicWriteFileSync(destPath, contents, mode = 384) {
15455
+ import { closeSync as closeSync2, constants, fchmodSync, fchownSync, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
15456
+ function atomicWriteFileSync(destPath, contents, modeOrOpts = 384) {
15457
+ const opts = typeof modeOrOpts === "number" ? { mode: modeOrOpts } : modeOrOpts;
15458
+ const mode = opts.mode ?? 384;
15457
15459
  const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
15458
15460
  const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
15459
15461
  let fd = null;
15460
15462
  try {
15461
15463
  fd = openSync2(tmp, TMP_OPEN_FLAGS, mode);
15462
15464
  writeSync(fd, buf, 0, buf.length, 0);
15465
+ fchmodSync(fd, mode);
15466
+ if (opts.uid !== undefined) {
15467
+ try {
15468
+ fchownSync(fd, opts.uid, opts.gid ?? opts.uid);
15469
+ } catch (chownErr) {
15470
+ if (opts.onChownError)
15471
+ opts.onChownError(chownErr);
15472
+ else
15473
+ throw chownErr;
15474
+ }
15475
+ }
15463
15476
  fsyncSync(fd);
15464
15477
  closeSync2(fd);
15465
15478
  fd = null;
@@ -15655,6 +15668,53 @@ function resolveHindsightLlm(llm, litellm) {
15655
15668
  const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
15656
15669
  return { provider, model };
15657
15670
  }
15671
+ function isLoopbackHttpUrl(url) {
15672
+ try {
15673
+ const u = new URL(url.includes("://") ? url : `http://${url}`);
15674
+ const h = (u.hostname || "").toLowerCase();
15675
+ return h === "localhost" || h === "127.0.0.1" || h === "::1";
15676
+ } catch {
15677
+ return false;
15678
+ }
15679
+ }
15680
+ function collectHindsightLlmBaseUrls(llm, litellm) {
15681
+ const out = [];
15682
+ const seen = new Set;
15683
+ const push = (raw) => {
15684
+ const v = raw?.trim();
15685
+ if (!v || seen.has(v))
15686
+ return;
15687
+ seen.add(v);
15688
+ out.push(v);
15689
+ };
15690
+ push(litellm?.baseUrl);
15691
+ for (const [k, v] of resolveHindsightPerOpLlm(llm)) {
15692
+ if (k.endsWith("_BASE_URL"))
15693
+ push(v);
15694
+ }
15695
+ return out;
15696
+ }
15697
+ function hindsightNeedsHostNetwork(llm, litellm) {
15698
+ if (litellm?.baseUrl?.trim())
15699
+ return true;
15700
+ return collectHindsightLlmBaseUrls(llm).some(isLoopbackHttpUrl);
15701
+ }
15702
+ function pickHindsightLiteLlmProbeUrl(llm, litellm) {
15703
+ return collectHindsightLlmBaseUrls(llm, litellm)[0];
15704
+ }
15705
+ function buildLiteLlmAwareHealthPy(apiPort, litellmBaseUrl) {
15706
+ let host = "127.0.0.1";
15707
+ let port = "80";
15708
+ try {
15709
+ const u = new URL(litellmBaseUrl.includes("://") ? litellmBaseUrl : `http://${litellmBaseUrl}`);
15710
+ host = u.hostname || host;
15711
+ port = u.port || (u.protocol === "https:" ? "443" : "80");
15712
+ } catch {}
15713
+ return `import socket,urllib.request,sys;` + `r=urllib.request.urlopen('http://localhost:${apiPort}/health',timeout=4);` + `(r.getcode()==200) or sys.exit(1);` + `s=socket.create_connection(('${host}',${Number(port)}),2);s.close()`;
15714
+ }
15715
+ function buildLiteLlmAwareHealthCmd(apiPort, litellmBaseUrl) {
15716
+ return `python3 -c "${buildLiteLlmAwareHealthPy(apiPort, litellmBaseUrl)}"`;
15717
+ }
15658
15718
  function startHindsight(ports, litellm, imageTag, llm, mirrorDir) {
15659
15719
  const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
15660
15720
  const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
@@ -15687,7 +15747,9 @@ function startHindsight(ports, litellm, imageTag, llm, mirrorDir) {
15687
15747
  "-e",
15688
15748
  `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15689
15749
  "-e",
15690
- `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
15750
+ `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
15751
+ "-e",
15752
+ `HINDSIGHT_API_WORKER_ID=${HINDSIGHT_DEFAULT_WORKER_ID}`
15691
15753
  ];
15692
15754
  if (llmProvider === "claude-code") {
15693
15755
  envArgs.push("-e", `ANTHROPIC_MODEL=${llmModel}`);
@@ -15710,8 +15772,15 @@ x-litellm-tags: service:hindsight`);
15710
15772
  `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15711
15773
  `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15712
15774
  `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15713
- "--health-cmd",
15714
- litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
15775
+ ...(() => {
15776
+ const hostNet = hindsightNeedsHostNetwork(llm, litellm);
15777
+ const healthApiPort = hostNet ? apiPort : 8888;
15778
+ const probe = pickHindsightLiteLlmProbeUrl(llm, litellm);
15779
+ return [
15780
+ "--health-cmd",
15781
+ probe ? buildLiteLlmAwareHealthCmd(healthApiPort, probe) : HINDSIGHT_HEALTHCHECK_CMD
15782
+ ];
15783
+ })(),
15715
15784
  "--health-interval",
15716
15785
  "30s",
15717
15786
  "--health-timeout",
@@ -15721,8 +15790,11 @@ x-litellm-tags: service:hindsight`);
15721
15790
  "--health-start-period",
15722
15791
  "60s"
15723
15792
  ];
15724
- if (litellm) {
15793
+ if (hindsightNeedsHostNetwork(llm, litellm)) {
15725
15794
  args.push("--network", "host");
15795
+ if (!litellm) {
15796
+ envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:${apiPort}`);
15797
+ }
15726
15798
  } else {
15727
15799
  args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
15728
15800
  }
@@ -15744,13 +15816,40 @@ x-litellm-tags: service:hindsight`);
15744
15816
  }
15745
15817
  execFileSync("docker", args, { stdio: "pipe" });
15746
15818
  }
15747
- function stopHindsight() {
15748
- try {
15749
- execFileSync("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
15750
- } catch {}
15819
+ function listHindsightDataVolumeMounts(exec = (cmd, args) => execFileSync(cmd, args, { stdio: "pipe", encoding: "utf-8" })) {
15751
15820
  try {
15752
- execFileSync("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
15753
- } catch {}
15821
+ const out = exec("docker", [
15822
+ "ps",
15823
+ "-a",
15824
+ "--filter",
15825
+ `volume=${HINDSIGHT_DATA_VOLUME}`,
15826
+ "--format",
15827
+ "{{.Names}}"
15828
+ ]);
15829
+ return out.split(`
15830
+ `).map((l) => l.trim()).filter((n) => n.length > 0);
15831
+ } catch {
15832
+ return [];
15833
+ }
15834
+ }
15835
+ function stopHindsight(exec = (cmd, args) => {
15836
+ execFileSync(cmd, args, { stdio: "pipe" });
15837
+ }, listMounts = () => listHindsightDataVolumeMounts()) {
15838
+ const names = new Set([HINDSIGHT_DEFAULT_WORKER_ID, ...listMounts()]);
15839
+ for (const name of names) {
15840
+ if (name !== HINDSIGHT_DEFAULT_WORKER_ID) {
15841
+ console.error(`stopHindsight: removing hindsight data-volume twin ${name} (not the canonical ${HINDSIGHT_DEFAULT_WORKER_ID})`);
15842
+ }
15843
+ try {
15844
+ exec("docker", ["update", "--restart=no", name]);
15845
+ } catch {}
15846
+ try {
15847
+ exec("docker", ["stop", name]);
15848
+ } catch {}
15849
+ try {
15850
+ exec("docker", ["rm", "-f", name]);
15851
+ } catch {}
15852
+ }
15754
15853
  }
15755
15854
  function pullHindsightImage(imageTag) {
15756
15855
  execFileSync("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
@@ -15808,17 +15907,30 @@ function getHindsightStatus() {
15808
15907
  return null;
15809
15908
  }
15810
15909
  }
15811
- function generateHindsightComposeSnippet(llm, mirrorDir) {
15812
- const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm);
15910
+ function generateHindsightComposeSnippet(llm, mirrorDir, litellm) {
15911
+ const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm, litellm);
15813
15912
  const perOpLlm = resolveHindsightPerOpLlm(llm);
15913
+ const hostNetwork = hindsightNeedsHostNetwork(llm, litellm);
15914
+ const apiPort = HINDSIGHT_DEFAULT_API_PORT;
15915
+ const internalApiPort = hostNetwork ? apiPort : 8888;
15916
+ const probeUrl = pickHindsightLiteLlmProbeUrl(llm, litellm);
15917
+ const healthPy = probeUrl ? buildLiteLlmAwareHealthPy(internalApiPort, probeUrl) : HINDSIGHT_HEALTHCHECK_PY;
15814
15918
  const environment = [
15815
15919
  ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15816
15920
  ` - HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
15817
15921
  ` - HINDSIGHT_API_LLM_MODEL=${llmModel}`,
15818
15922
  ...perOpLlm.map(([k, v]) => ` - ${k}=${v}`),
15819
15923
  ...llmProvider === "claude-code" ? [` - ANTHROPIC_MODEL=${llmModel}`] : [],
15820
- ` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`
15924
+ ...hostNetwork ? [
15925
+ ` - HINDSIGHT_API_PORT=${apiPort}`,
15926
+ ` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:${apiPort}`
15927
+ ] : [` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`]
15821
15928
  ];
15929
+ if (litellm?.baseUrl?.trim() && litellm.apiKey?.trim()) {
15930
+ const litellmRoot = litellm.baseUrl.replace(/\/+$/, "");
15931
+ const anthropicBaseUrl = isClaudeModel(llmModel) ? `${litellmRoot}/anthropic` : litellmRoot;
15932
+ environment.push(` - ANTHROPIC_BASE_URL=${anthropicBaseUrl}`, ` - ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}\\nx-litellm-customer-id: hindsight\\nx-litellm-tags: service:hindsight`);
15933
+ }
15822
15934
  const initService = mirrorDir ? [
15823
15935
  " switchroom-hindsight-creds-init:",
15824
15936
  ` image: ${HINDSIGHT_IMAGE}`,
@@ -15835,6 +15947,13 @@ function generateHindsightComposeSnippet(llm, mirrorDir) {
15835
15947
  " switchroom-hindsight-creds-init:",
15836
15948
  " condition: service_completed_successfully"
15837
15949
  ] : [];
15950
+ const networkOrPorts = hostNetwork ? [
15951
+ " network_mode: host"
15952
+ ] : [
15953
+ " ports:",
15954
+ ` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
15955
+ ' - "127.0.0.1:19999:9999"'
15956
+ ];
15838
15957
  return [
15839
15958
  "services:",
15840
15959
  ...initService,
@@ -15842,9 +15961,7 @@ function generateHindsightComposeSnippet(llm, mirrorDir) {
15842
15961
  ` image: ${HINDSIGHT_IMAGE}`,
15843
15962
  " container_name: switchroom-hindsight",
15844
15963
  ...dependsOn,
15845
- " ports:",
15846
- ` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
15847
- ' - "127.0.0.1:19999:9999"',
15964
+ ...networkOrPorts,
15848
15965
  " environment:",
15849
15966
  ...environment,
15850
15967
  ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
@@ -15857,12 +15974,13 @@ function generateHindsightComposeSnippet(llm, mirrorDir) {
15857
15974
  ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
15858
15975
  ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15859
15976
  ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
15977
+ ` - HINDSIGHT_API_WORKER_ID=${HINDSIGHT_DEFAULT_WORKER_ID}`,
15860
15978
  ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
15861
15979
  ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15862
15980
  ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15863
15981
  ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15864
15982
  " healthcheck:",
15865
- ` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
15983
+ ` test: ${JSON.stringify(["CMD", "python3", "-c", healthPy])}`,
15866
15984
  " interval: 30s",
15867
15985
  " timeout: 5s",
15868
15986
  " retries: 3",
@@ -15946,7 +16064,7 @@ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFA
15946
16064
  atomicWriteFileSync2(configPath, tail, mode);
15947
16065
  return { added: true, reason: "added" };
15948
16066
  }
15949
- var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, HINDSIGHT_LLM_OPS;
16067
+ var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_WORKER_ID = "switchroom-hindsight", HINDSIGHT_DATA_VOLUME = "switchroom-hindsight-data", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, HINDSIGHT_LLM_OPS;
15950
16068
  var init_hindsight = __esm(() => {
15951
16069
  init_model_command();
15952
16070
  HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -16509,7 +16627,7 @@ var HINDSIGHT_SHIM_CLI_PATH = "/usr/local/bin/switchroom", HINDSIGHT_SHIM_AGENT_
16509
16627
  var init_hindsight2 = __esm(() => {
16510
16628
  init_users();
16511
16629
  init_hindsight();
16512
- DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
16630
+ DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) \u2014 only retain the outcome once a task " + "actually completes or a decision is made.";
16513
16631
  PROFILE_MEMORY_DEFAULTS = {
16514
16632
  "health-coach": {
16515
16633
  disposition: { skepticism: 2, literalism: 2, empathy: 5 },
@@ -30033,11 +30151,25 @@ function generateCompose(opts) {
30033
30151
  lines.push(` SWITCHROOM_CONFIG: /state/config/switchroom.yaml`);
30034
30152
  }
30035
30153
  lines.push(` SWITCHROOM_AUTH_BROKER_STATE_DIR: /state/auth-broker`);
30154
+ let authBrokerNeedsHostGateway = false;
30155
+ {
30156
+ const llBase = config.litellm?.base_url;
30157
+ if (typeof llBase === "string" && llBase.trim()) {
30158
+ const raw = llBase.trim().replace(/\/+$/, "");
30159
+ const bridged = raw.replace(/^http:\/\/127\.0\.0\.1(?=[:/]|$)/i, "http://host.docker.internal").replace(/^http:\/\/localhost(?=[:/]|$)/i, "http://host.docker.internal").replace(/^https:\/\/127\.0\.0\.1(?=[:/]|$)/i, "https://host.docker.internal").replace(/^https:\/\/localhost(?=[:/]|$)/i, "https://host.docker.internal");
30160
+ lines.push(` SWITCHROOM_LITELLM_BASE: ${JSON.stringify(bridged)}`);
30161
+ authBrokerNeedsHostGateway = true;
30162
+ }
30163
+ }
30036
30164
  lines.push(` SWITCHROOM_ACCOUNTS_DIR: /state/accounts`);
30037
30165
  lines.push(` SWITCHROOM_AGENTS_DIR: /state/agents`);
30038
30166
  if (opts.operatorUid !== undefined) {
30039
30167
  lines.push(` SWITCHROOM_AUTH_BROKER_OPERATOR_UID: "${opts.operatorUid}"`);
30040
30168
  }
30169
+ if (authBrokerNeedsHostGateway) {
30170
+ lines.push(` extra_hosts:`);
30171
+ lines.push(` - "host.docker.internal:host-gateway"`);
30172
+ }
30041
30173
  lines.push(` volumes:`);
30042
30174
  for (const a of describeAgents(config, opts.litellmConfirmedAgents)) {
30043
30175
  lines.push(` - auth-broker-${a.name}-sock:/run/switchroom/auth-broker/${a.name}`);
@@ -32692,7 +32824,7 @@ function decodeResponse2(line) {
32692
32824
  }
32693
32825
  return ResponseSchema2.parse(parsed);
32694
32826
  }
32695
- var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, MarkThrottledRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, MarkThrottledDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
32827
+ var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, MarkThrottledRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, GetExternalSpendRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, MarkThrottledDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GetExternalSpendDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
32696
32828
  var init_protocol2 = __esm(() => {
32697
32829
  init_zod();
32698
32830
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -32823,6 +32955,12 @@ var init_protocol2 = __esm(() => {
32823
32955
  key: exports_external.string().min(1).max(512),
32824
32956
  windowMs: exports_external.number().int().positive().max(86400000)
32825
32957
  });
32958
+ GetExternalSpendRequestSchema = exports_external.object({
32959
+ v: exports_external.literal(PROTOCOL_VERSION),
32960
+ op: exports_external.literal("get-external-spend"),
32961
+ id: exports_external.string().min(1),
32962
+ forceLive: exports_external.boolean().optional()
32963
+ });
32826
32964
  RequestSchema2 = exports_external.discriminatedUnion("op", [
32827
32965
  GetCredentialsRequestSchema,
32828
32966
  ListStateRequestSchema,
@@ -32836,7 +32974,8 @@ var init_protocol2 = __esm(() => {
32836
32974
  ListGoogleAccountsRequestSchema,
32837
32975
  ListMicrosoftAccountsRequestSchema,
32838
32976
  ProbeQuotaRequestSchema,
32839
- ClaimNotificationRequestSchema
32977
+ ClaimNotificationRequestSchema,
32978
+ GetExternalSpendRequestSchema
32840
32979
  ]);
32841
32980
  GetCredentialsDataSchema = exports_external.object({
32842
32981
  account: exports_external.string(),
@@ -32903,6 +33042,18 @@ var init_protocol2 = __esm(() => {
32903
33042
  ClaimNotificationDataSchema = exports_external.object({
32904
33043
  granted: exports_external.boolean()
32905
33044
  });
33045
+ GetExternalSpendDataSchema = exports_external.object({
33046
+ available: exports_external.boolean(),
33047
+ day24hUsd: exports_external.number().optional(),
33048
+ day7dUsd: exports_external.number().optional(),
33049
+ top: exports_external.array(exports_external.object({
33050
+ label: exports_external.string(),
33051
+ usd: exports_external.number()
33052
+ })).optional(),
33053
+ capturedAtMs: exports_external.number().int().nonnegative().optional(),
33054
+ served: exports_external.enum(["live", "cache"]).optional(),
33055
+ reason: exports_external.string().optional()
33056
+ });
32906
33057
  GoogleAccountStateSchema = exports_external.object({
32907
33058
  account: exports_external.string(),
32908
33059
  expiresAt: exports_external.number(),
@@ -33073,6 +33224,15 @@ class AuthBrokerClient {
33073
33224
  }
33074
33225
  return parsed;
33075
33226
  }
33227
+ async getExternalSpend(forceLive) {
33228
+ const data = await this.send({
33229
+ v: PROTOCOL_VERSION,
33230
+ id: randomUUID(),
33231
+ op: "get-external-spend",
33232
+ ...forceLive ? { forceLive: true } : {}
33233
+ });
33234
+ return data;
33235
+ }
33076
33236
  async setActive(account) {
33077
33237
  const data = await this.send({
33078
33238
  v: PROTOCOL_VERSION,
@@ -41964,9 +42124,124 @@ function classifyAutohealStatus(logs) {
41964
42124
  }
41965
42125
  return { name: "hindsight autoheal", status: "ok", detail: "no auto-restarts" };
41966
42126
  }
42127
+ function classifyLlmVerification(logs) {
42128
+ const latestFailed = new Map;
42129
+ const failDetail = new Map;
42130
+ let sawAny = false;
42131
+ for (const line of logs.split(`
42132
+ `)) {
42133
+ const fail3 = line.match(/LLM connection verification failed for '([^']+)' config:\s*(.*?)(?:\.\s*Server will start|$)/);
42134
+ if (fail3) {
42135
+ sawAny = true;
42136
+ const cfg = fail3[1];
42137
+ latestFailed.set(cfg, true);
42138
+ failDetail.set(cfg, (fail3[2] ?? "").trim());
42139
+ continue;
42140
+ }
42141
+ if (/connection verified successfully/.test(line) || /LLM verification passed/.test(line)) {
42142
+ sawAny = true;
42143
+ latestFailed.set("default", false);
42144
+ }
42145
+ }
42146
+ if (!sawAny)
42147
+ return null;
42148
+ const failedConfigs = [...latestFailed.entries()].filter(([, failed]) => failed).map(([cfg]) => cfg);
42149
+ if (failedConfigs.length === 0) {
42150
+ return {
42151
+ name: "hindsight LLM verification",
42152
+ status: "ok",
42153
+ detail: "boot LLM connection verified"
42154
+ };
42155
+ }
42156
+ const named = failedConfigs.map((cfg) => {
42157
+ const err = failDetail.get(cfg);
42158
+ return err ? `'${cfg}' (${err})` : `'${cfg}'`;
42159
+ }).join(", ");
42160
+ return {
42161
+ name: "hindsight LLM verification",
42162
+ status: "fail",
42163
+ detail: `boot LLM connection verification FAILED for config(s): ${named} \u2014 ` + `hindsight is serving but every LLM-dependent op (retain/reflect/` + `consolidation extraction) for that config will fail silently until the ` + `provider is reachable`,
42164
+ fix: "Almost always a model-name drift: the pinned `hindsight.llm.*.model` no " + "longer exists in the LiteLLM proxy's model_list (run `switchroom doctor` " + "\u2014 the LiteLLM model-routing check names the exact missing model), or the " + "provider is down. Fix the model reference (or the proxy), then " + "`switchroom memory --restart` so hindsight re-verifies."
42165
+ };
42166
+ }
42167
+ function classifyHindsightDataVolumeMounts(names) {
42168
+ const uniq = [...new Set(names.filter(Boolean))];
42169
+ if (uniq.length === 0) {
42170
+ return {
42171
+ name: "hindsight data-volume exclusive",
42172
+ status: "ok",
42173
+ detail: "no container currently mounts the live data volume"
42174
+ };
42175
+ }
42176
+ const twins = uniq.filter((n) => n !== HINDSIGHT_DEFAULT_WORKER_ID);
42177
+ if (uniq.length === 1 && uniq[0] === HINDSIGHT_DEFAULT_WORKER_ID) {
42178
+ return {
42179
+ name: "hindsight data-volume exclusive",
42180
+ status: "ok",
42181
+ detail: `only ${HINDSIGHT_DEFAULT_WORKER_ID} mounts ${HINDSIGHT_DATA_VOLUME}`
42182
+ };
42183
+ }
42184
+ if (twins.length > 0) {
42185
+ return {
42186
+ name: "hindsight data-volume exclusive",
42187
+ status: "fail",
42188
+ detail: `multiple containers mount ${HINDSIGHT_DATA_VOLUME}: [${uniq.join(", ")}] \u2014 ` + `dual postmasters corrupt the embedded PG checkpoint`,
42189
+ fix: "`switchroom memory --restart` (stopHindsight now disables restart + removes " + "every volume twin), or manually `docker update --restart=no <twin> && " + "docker rm -f <twin>` before starting the live container."
42190
+ };
42191
+ }
42192
+ return {
42193
+ name: "hindsight data-volume exclusive",
42194
+ status: "warn",
42195
+ detail: `unexpected volume mount set: [${uniq.join(", ")}]`
42196
+ };
42197
+ }
42198
+ function classifyHindsightNetworkMode(networkMode, litellmConfigured) {
42199
+ if (!litellmConfigured) {
42200
+ return {
42201
+ name: "hindsight network mode",
42202
+ status: "ok",
42203
+ detail: networkMode ? `${networkMode} (LiteLLM not configured)` : "unknown (LiteLLM not configured)"
42204
+ };
42205
+ }
42206
+ const mode = (networkMode ?? "").trim() || "unknown";
42207
+ if (mode === "host") {
42208
+ return {
42209
+ name: "hindsight network mode",
42210
+ status: "ok",
42211
+ detail: "host \u2014 LiteLLM 127.0.0.1 reachable"
42212
+ };
42213
+ }
42214
+ return {
42215
+ name: "hindsight network mode",
42216
+ status: "fail",
42217
+ detail: `NetworkMode=${mode} while LiteLLM routing is configured \u2014 retains will die with ` + `"Connection error" to 127.0.0.1:4010 while /health stays green`,
42218
+ fix: "`switchroom memory --restart` so startHindsight recreates with --network host. " + "Do not `docker start` a bridge/mis-networked container."
42219
+ };
42220
+ }
42221
+ function classifyLiteLlmReachability(reachable, endpoint) {
42222
+ if (reachable === null)
42223
+ return null;
42224
+ if (reachable) {
42225
+ return {
42226
+ name: "hindsight LiteLLM reachability",
42227
+ status: "ok",
42228
+ detail: `TCP ok to ${endpoint}`
42229
+ };
42230
+ }
42231
+ return {
42232
+ name: "hindsight LiteLLM reachability",
42233
+ status: "fail",
42234
+ detail: `cannot TCP-connect to ${endpoint} \u2014 hindsight LLM ops will fail`,
42235
+ fix: "Start/fix the LiteLLM proxy (host :4010), then `switchroom memory --restart` " + "if hindsight was started while it was down."
42236
+ };
42237
+ }
41967
42238
  function checkHindsightContainerHealth(opts) {
41968
42239
  const name = opts?.containerName ?? "switchroom-hindsight";
41969
- const exec = opts?.exec ?? ((cmd, args) => execFileSync18(cmd, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString());
42240
+ const exec = opts?.exec ?? ((cmd, args) => execFileSync18(cmd, args, {
42241
+ stdio: ["ignore", "pipe", "ignore"],
42242
+ timeout: 8000,
42243
+ maxBuffer: 16 * 1024 * 1024
42244
+ }).toString());
41970
42245
  const results = [];
41971
42246
  let shmRaw;
41972
42247
  try {
@@ -41978,6 +42253,29 @@ function checkHindsightContainerHealth(opts) {
41978
42253
  if (Number.isFinite(shmBytes) && shmBytes > 0) {
41979
42254
  results.push(classifyShmSize(shmBytes));
41980
42255
  }
42256
+ try {
42257
+ const startedAt = exec("docker", [
42258
+ "inspect",
42259
+ name,
42260
+ "--format",
42261
+ "{{.State.StartedAt}}"
42262
+ ]).trim();
42263
+ const startMs = Date.parse(startedAt);
42264
+ if (Number.isFinite(startMs)) {
42265
+ const untilIso = new Date(startMs + 10 * 60000).toISOString();
42266
+ const bootLogs = exec("docker", [
42267
+ "logs",
42268
+ "--since",
42269
+ startedAt,
42270
+ "--until",
42271
+ untilIso,
42272
+ name
42273
+ ]);
42274
+ const verifyRow = classifyLlmVerification(bootLogs);
42275
+ if (verifyRow)
42276
+ results.push(verifyRow);
42277
+ }
42278
+ } catch {}
41981
42279
  try {
41982
42280
  const logs = exec("docker", ["logs", "--since", "10m", name]);
41983
42281
  results.push(classifyExtractionLogs(logs));
@@ -41986,6 +42284,69 @@ function checkHindsightContainerHealth(opts) {
41986
42284
  const autohealLogs = exec("docker", ["logs", "--since", "1h", "switchroom-hindsight-autoheal"]);
41987
42285
  results.push(classifyAutohealStatus(autohealLogs));
41988
42286
  } catch {}
42287
+ try {
42288
+ const mounts = listHindsightDataVolumeMounts(exec);
42289
+ results.push(classifyHindsightDataVolumeMounts(mounts));
42290
+ } catch {}
42291
+ try {
42292
+ const netMode = exec("docker", [
42293
+ "inspect",
42294
+ name,
42295
+ "--format",
42296
+ "{{.HostConfig.NetworkMode}}"
42297
+ ]).trim();
42298
+ const envBlob = exec("docker", [
42299
+ "inspect",
42300
+ name,
42301
+ "--format",
42302
+ "{{range .Config.Env}}{{println .}}{{end}}"
42303
+ ]);
42304
+ const perOpBasePresent = /HINDSIGHT_API_(RETAIN|REFLECT|CONSOLIDATION)_LLM_BASE_URL=/.test(envBlob);
42305
+ const anthropicBaseMatch = envBlob.match(/(?:^|\n)ANTHROPIC_BASE_URL=(\S+)/);
42306
+ const anthropicLooksLikeLiteLlm = (() => {
42307
+ const raw = anthropicBaseMatch?.[1]?.trim();
42308
+ if (!raw)
42309
+ return false;
42310
+ try {
42311
+ const u = new URL(raw.includes("://") ? raw : `http://${raw}`);
42312
+ const port = u.port || (u.protocol === "https:" ? "443" : "80");
42313
+ const path5 = (u.pathname || "").replace(/\/+$/, "");
42314
+ if (port === "4010")
42315
+ return true;
42316
+ if (/\/anthropic$/i.test(path5))
42317
+ return true;
42318
+ if (/litellm/i.test(u.hostname || ""))
42319
+ return true;
42320
+ return false;
42321
+ } catch {
42322
+ return false;
42323
+ }
42324
+ })();
42325
+ const litellmConfigured = perOpBasePresent || anthropicLooksLikeLiteLlm;
42326
+ results.push(classifyHindsightNetworkMode(netMode, litellmConfigured));
42327
+ if (litellmConfigured) {
42328
+ const m = envBlob.match(/HINDSIGHT_API_RETAIN_LLM_BASE_URL=(\S+)/) ?? envBlob.match(/HINDSIGHT_API_REFLECT_LLM_BASE_URL=(\S+)/) ?? envBlob.match(/HINDSIGHT_API_CONSOLIDATION_LLM_BASE_URL=(\S+)/) ?? (anthropicLooksLikeLiteLlm ? anthropicBaseMatch : null);
42329
+ let endpoint = "127.0.0.1:4010";
42330
+ if (m) {
42331
+ try {
42332
+ const u = new URL(m[1].includes("://") ? m[1] : `http://${m[1]}`);
42333
+ endpoint = `${u.hostname || "127.0.0.1"}:${u.port || "80"}`;
42334
+ } catch {}
42335
+ }
42336
+ const [host, portStr] = endpoint.split(":");
42337
+ const port = Number(portStr) || 80;
42338
+ let reachable = null;
42339
+ try {
42340
+ exec("bash", ["-c", `echo > /dev/tcp/${host}/${port}`]);
42341
+ reachable = true;
42342
+ } catch {
42343
+ reachable = false;
42344
+ }
42345
+ const row = classifyLiteLlmReachability(reachable, endpoint);
42346
+ if (row)
42347
+ results.push(row);
42348
+ }
42349
+ } catch {}
41989
42350
  return results;
41990
42351
  }
41991
42352
  function classifyHindsightHealthProbe(status, port) {
@@ -42076,9 +42437,142 @@ function classifyToolContract(advertised) {
42076
42437
  var MIN_HINDSIGHT_SHM_BYTES, CONSOLIDATION_BACKLOG_WARN = 25, CONSOLIDATION_BACKLOG_FAIL = 200;
42077
42438
  var init_doctor_memory = __esm(() => {
42078
42439
  init_hindsight_tools();
42440
+ init_hindsight();
42079
42441
  MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
42080
42442
  });
42081
42443
 
42444
+ // src/litellm/model-validation.ts
42445
+ function isExplicitLitellmRoute(model) {
42446
+ return model.includes("/");
42447
+ }
42448
+ function collectReferencedModels(config) {
42449
+ const refs = [];
42450
+ const litellmEnabled = config.litellm?.enabled === true;
42451
+ if (!litellmEnabled)
42452
+ return refs;
42453
+ const llm = config.hindsight?.llm;
42454
+ if (llm) {
42455
+ const push = (model, consumer) => {
42456
+ if (model)
42457
+ refs.push({ model, consumer });
42458
+ };
42459
+ push(llm.model, "hindsight.llm.model (global)");
42460
+ push(llm.retain?.model, "hindsight.llm.retain");
42461
+ push(llm.reflect?.model, "hindsight.llm.reflect");
42462
+ push(llm.consolidation?.model, "hindsight.llm.consolidation");
42463
+ }
42464
+ for (const [name, agent] of Object.entries(config.agents ?? {})) {
42465
+ if (!agent)
42466
+ continue;
42467
+ const agentRoutes = agent.litellm?.enabled !== false;
42468
+ if (!agentRoutes)
42469
+ continue;
42470
+ if (agent.model && isExplicitLitellmRoute(agent.model)) {
42471
+ refs.push({ model: agent.model, consumer: `agents.${name}.model` });
42472
+ }
42473
+ if (agent.fallback_model && isExplicitLitellmRoute(agent.fallback_model)) {
42474
+ refs.push({ model: agent.fallback_model, consumer: `agents.${name}.fallback_model` });
42475
+ }
42476
+ }
42477
+ return refs;
42478
+ }
42479
+ function validateModelReferences(refs, proxyModels) {
42480
+ const missing = new Map;
42481
+ for (const { model, consumer } of refs) {
42482
+ if (proxyModels.has(model))
42483
+ continue;
42484
+ const list = missing.get(model);
42485
+ if (list) {
42486
+ if (!list.includes(consumer))
42487
+ list.push(consumer);
42488
+ } else {
42489
+ missing.set(model, [consumer]);
42490
+ }
42491
+ }
42492
+ return [...missing.entries()].map(([model, consumers]) => ({ model, consumers }));
42493
+ }
42494
+ async function fetchProxyModels(baseUrl, apiKey, fetchFn = fetch, timeoutMs = 8000) {
42495
+ const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
42496
+ const controller = new AbortController;
42497
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
42498
+ try {
42499
+ const res = await fetchFn(url, {
42500
+ method: "GET",
42501
+ headers: { Authorization: `Bearer ${apiKey}` },
42502
+ ...{ signal: controller.signal }
42503
+ });
42504
+ if (!res.ok) {
42505
+ return { kind: "unreachable", msg: `proxy returned HTTP ${res.status} for ${url}` };
42506
+ }
42507
+ const body = await res.json();
42508
+ const data = body?.data;
42509
+ if (!Array.isArray(data)) {
42510
+ return { kind: "unreachable", msg: `proxy ${url} response had no model \`data\` array` };
42511
+ }
42512
+ const models = data.map((m) => m?.id).filter((id) => typeof id === "string");
42513
+ return { kind: "ok", models };
42514
+ } catch (err) {
42515
+ return { kind: "unreachable", msg: err.message ?? String(err) };
42516
+ } finally {
42517
+ clearTimeout(timer);
42518
+ }
42519
+ }
42520
+ function classifyModelReferences(missing, refCount) {
42521
+ if (missing.length === 0) {
42522
+ return {
42523
+ name: "litellm model routing",
42524
+ status: "ok",
42525
+ detail: `all ${refCount} referenced model(s) present in the proxy model_list`
42526
+ };
42527
+ }
42528
+ const detail = missing.map((m) => `\`${m.model}\` (referenced by ${m.consumers.join(", ")})`).join("; ");
42529
+ return {
42530
+ name: "litellm model routing",
42531
+ status: "fail",
42532
+ detail: `${missing.length} model reference(s) NOT in the LiteLLM proxy model_list: ` + `${detail} \u2014 every LLM call from that consumer will 400 ("Invalid model ` + `name") until the proxy lists it or the reference is corrected`,
42533
+ fix: "Either add the missing model group back to the LiteLLM proxy config " + "(the operator-maintained `litellm-config.yaml` `model_list`) and redeploy " + "the proxy, or repoint the consumer at a model the proxy DOES route " + "(`switchroom doctor` lists them via the proxy `/v1/models`). For " + "hindsight, then `switchroom memory --restart` so it re-verifies."
42534
+ };
42535
+ }
42536
+ async function runLitellmModelChecks(config, opts) {
42537
+ const refs = collectReferencedModels(config);
42538
+ if (refs.length === 0)
42539
+ return [];
42540
+ const litellm = config.litellm;
42541
+ const baseUrl = litellm?.base_url;
42542
+ const adminKeyRef = litellm?.admin_key;
42543
+ if (!baseUrl || !adminKeyRef) {
42544
+ return [
42545
+ {
42546
+ name: "litellm model routing",
42547
+ status: "warn",
42548
+ detail: `${refs.length} model reference(s) to validate but litellm ` + `${!baseUrl ? "base_url" : "admin_key"} is unresolved \u2014 cannot query ` + `the proxy model_list`
42549
+ }
42550
+ ];
42551
+ }
42552
+ const apiKey = opts.resolveSecret(adminKeyRef);
42553
+ if (!apiKey) {
42554
+ return [
42555
+ {
42556
+ name: "litellm model routing",
42557
+ status: "warn",
42558
+ detail: `could not resolve the litellm admin_key (\`${adminKeyRef}\`) to query ` + `the proxy model_list \u2014 ${refs.length} model reference(s) left unverified`
42559
+ }
42560
+ ];
42561
+ }
42562
+ const probe2 = await fetchProxyModels(baseUrl, apiKey, opts.fetchFn);
42563
+ if (probe2.kind === "unreachable") {
42564
+ return [
42565
+ {
42566
+ name: "litellm model routing",
42567
+ status: "warn",
42568
+ detail: `LiteLLM proxy unreachable (${probe2.msg}) \u2014 ${refs.length} model ` + `reference(s) left unverified; not failing on an environment limitation`
42569
+ }
42570
+ ];
42571
+ }
42572
+ const missing = validateModelReferences(refs, new Set(probe2.models));
42573
+ return [classifyModelReferences(missing, refs.length)];
42574
+ }
42575
+
42082
42576
  // src/cli/doctor-docker.ts
42083
42577
  import { readFileSync as readFileSync56 } from "node:fs";
42084
42578
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -45289,6 +45783,7 @@ __export(exports_doctor, {
45289
45783
  checkTelegram: () => checkTelegram,
45290
45784
  checkTcp: () => checkTcp,
45291
45785
  checkStartShStale: () => checkStartShStale,
45786
+ checkStartShSessionModelCarrier: () => checkStartShSessionModelCarrier,
45292
45787
  checkSkillsPrerequisites: () => checkSkillsPrerequisites,
45293
45788
  checkRepoHygiene: () => checkRepoHygiene,
45294
45789
  checkPendingRetainsQueues: () => checkPendingRetainsQueues,
@@ -46362,6 +46857,53 @@ function checkStartShStale(agentName, startShPath) {
46362
46857
  }
46363
46858
  return { name: label, status: "ok", detail: "supervisor block present" };
46364
46859
  }
46860
+ function checkStartShSessionModelCarrier(agentName, startShPath) {
46861
+ const label = `${agentName}: start.sh /model session carrier`;
46862
+ if (!existsSync66(startShPath)) {
46863
+ return {
46864
+ name: label,
46865
+ status: "warn",
46866
+ detail: `${startShPath} not found`,
46867
+ fix: `Run \`switchroom apply\` to scaffold start.sh (rev5 \`.session-model\` carrier).`
46868
+ };
46869
+ }
46870
+ let content;
46871
+ try {
46872
+ content = readFileSync59(startShPath, "utf-8");
46873
+ } catch (err) {
46874
+ return {
46875
+ name: label,
46876
+ status: "skip",
46877
+ detail: `unreadable from host (${err.message}) \u2014 agent-UID-owned; the session-model carrier block can't be checked from the operator UID`,
46878
+ fix: `Verify in-agent: docker exec switchroom-${agentName} sh -c 'grep -E "\\[ -f .*\\.session-model\\" \\]" /state/agent/start.sh && echo ok'`
46879
+ };
46880
+ }
46881
+ const fileTestPaths = [...content.matchAll(/\[\s*-f\s+([^\]]+?)\s*\]/g)].map((m) => m[1].replace(/["']/g, "").trim());
46882
+ const hasBareSessionModelTest = fileTestPaths.some((p) => /(?:^|\/)\.session-model$/.test(p));
46883
+ const hasJsonCarrierParse = /configuredDefaultAtWrite/.test(content) || /session-model-boot-attempts/.test(content);
46884
+ if (hasBareSessionModelTest && hasJsonCarrierParse) {
46885
+ return {
46886
+ name: label,
46887
+ status: "ok",
46888
+ detail: "rev5 `.session-model` consume-once carrier present"
46889
+ };
46890
+ }
46891
+ if (hasBareSessionModelTest && !hasJsonCarrierParse) {
46892
+ return {
46893
+ name: label,
46894
+ status: "fail",
46895
+ detail: "start.sh tests `.session-model` but lacks rev5 JSON apply (`configuredDefaultAtWrite` / boot-attempts) \u2014 /model switches may not stick",
46896
+ fix: "Run `switchroom apply` to regenerate start.sh from profiles/_base/start.sh.hbs (rev5 carrier). Takes effect on the next agent restart (no need to bounce until release)."
46897
+ };
46898
+ }
46899
+ const legacyOnly = /\.session-model-override/.test(content) && !hasBareSessionModelTest;
46900
+ return {
46901
+ name: label,
46902
+ status: "fail",
46903
+ detail: legacyOnly ? "start.sh only reads legacy `.session-model-override`; gateway writes rev5 `.session-model` \u2014 Telegram /model relaunches boot the yaml pin (silent miss)" : "start.sh missing rev5 `.session-model` carrier file test \u2014 Telegram /model cannot apply session overrides across relaunch",
46904
+ fix: "Run `switchroom apply` to regenerate start.sh from the latest template (rev5 `.session-model` carrier). The rewrite is on disk immediately; session switches apply on the next agent restart/release bounce."
46905
+ };
46906
+ }
46365
46907
  function checkLeakedHomeSwitchroom(agentName, agentDir) {
46366
46908
  const label = `${agentName}: $HOME/.switchroom symlink (#910)`;
46367
46909
  const path5 = join67(agentDir, "home", ".switchroom");
@@ -46491,6 +47033,7 @@ function checkAgents(config, configPath) {
46491
47033
  });
46492
47034
  }
46493
47035
  results.push(checkStartShStale(name, join67(agentDir, "start.sh")));
47036
+ results.push(checkStartShSessionModelCarrier(name, join67(agentDir, "start.sh")));
46494
47037
  results.push(checkLeakedHomeSwitchroom(name, agentDir));
46495
47038
  const status = statuses[name];
46496
47039
  const active = status?.active ?? "unknown";
@@ -47174,6 +47717,22 @@ function registerDoctorCommand(program3) {
47174
47717
  },
47175
47718
  { title: "Vault access", results: await runSecretAccessChecks(config) },
47176
47719
  { title: "Memory (Hindsight)", results: await checkHindsight(config) },
47720
+ {
47721
+ title: "LiteLLM model routing (#3407)",
47722
+ results: await runLitellmModelChecks(config, {
47723
+ resolveSecret: (ref) => {
47724
+ if (!isVaultReference(ref))
47725
+ return ref;
47726
+ if (!passphrase || !existsSync66(vaultPath))
47727
+ return null;
47728
+ try {
47729
+ return getStringSecret(passphrase, vaultPath, parseVaultReference(ref));
47730
+ } catch {
47731
+ return null;
47732
+ }
47733
+ }
47734
+ })
47735
+ },
47177
47736
  { title: "Telegram", results: await checkTelegram(config) },
47178
47737
  { title: "Agents", results: checkAgents(config, configPath) },
47179
47738
  {
@@ -47292,6 +47851,7 @@ var init_doctor = __esm(() => {
47292
47851
  init_hindsight2();
47293
47852
  init_hindsight();
47294
47853
  init_doctor_memory();
47854
+ init_resolver();
47295
47855
  init_doctor_docker();
47296
47856
  init_doctor_auth_broker();
47297
47857
  init_doctor_hostd();
@@ -48117,6 +48677,10 @@ function installUpdatePromptHook(agentDir) {
48117
48677
  var HOOK_FILENAME = "update-card-on-prompt.sh", CONTAINER_AGENT_DIR = "/state/agent";
48118
48678
  var init_update_prompt_hook = () => {};
48119
48679
 
48680
+ // src/litellm/external-spend.ts
48681
+ var LITELLM_MASTER_KEY_STATE_BASENAME = "litellm-master-key";
48682
+ var init_external_spend = () => {};
48683
+
48120
48684
  // src/cli/install-detect.ts
48121
48685
  import * as fs5 from "node:fs";
48122
48686
  import * as path8 from "node:path";
@@ -48594,6 +49158,7 @@ __export(exports_apply, {
48594
49158
  provisionLiteLLMKeys: () => provisionLiteLLMKeys,
48595
49159
  probeVaultProvisioning: () => probeVaultProvisioning,
48596
49160
  parseComposeServiceNames: () => parseComposeServiceNames,
49161
+ materializeLitellmMasterKeyForBroker: () => materializeLitellmMasterKeyForBroker,
48597
49162
  isInAgentContainer: () => isInAgentContainer,
48598
49163
  inspectVaultBindMountDir: () => inspectVaultBindMountDir,
48599
49164
  formatScaffoldFailureResolution: () => formatScaffoldFailureResolution,
@@ -48606,7 +49171,7 @@ __export(exports_apply, {
48606
49171
  DEFAULT_COMPOSE_PATH: () => DEFAULT_COMPOSE_PATH2,
48607
49172
  COMPOSE_PROJECT: () => COMPOSE_PROJECT2
48608
49173
  });
48609
- import { accessSync as accessSync3, chownSync as chownSync8, constants as fsConstants6, copyFileSync as copyFileSync12, existsSync as existsSync86, mkdirSync as mkdirSync49, readFileSync as readFileSync74, readdirSync as readdirSync30, renameSync as renameSync20, statSync as statSync47, writeFileSync as writeFileSync31 } from "node:fs";
49174
+ import { accessSync as accessSync3, chmodSync as chmodSync14, chownSync as chownSync8, constants as fsConstants6, copyFileSync as copyFileSync12, existsSync as existsSync86, mkdirSync as mkdirSync49, readFileSync as readFileSync74, readdirSync as readdirSync30, renameSync as renameSync20, statSync as statSync47, writeFileSync as writeFileSync31 } from "node:fs";
48610
49175
  import { mkdir as mkdir2 } from "node:fs/promises";
48611
49176
  import { spawnSync as childSpawnSync } from "node:child_process";
48612
49177
  import readline from "node:readline";
@@ -48629,6 +49194,21 @@ async function resolveOperatorVaultPassphrase(home2) {
48629
49194
  return null;
48630
49195
  }
48631
49196
  }
49197
+ function materializeLitellmMasterKeyForBroker(masterKey, home2 = process.env.HOME ?? "/root") {
49198
+ try {
49199
+ const stateDir = join86(home2, ".switchroom", "state", "auth-broker");
49200
+ mkdirSync49(stateDir, { recursive: true, mode: 448 });
49201
+ const path9 = join86(stateDir, LITELLM_MASTER_KEY_STATE_BASENAME);
49202
+ writeFileSync31(path9, masterKey.trim() + `
49203
+ `, { mode: 384 });
49204
+ try {
49205
+ chmodSync14(path9, 384);
49206
+ } catch {}
49207
+ return { ok: true, path: path9 };
49208
+ } catch (err) {
49209
+ return { ok: false, error: err.message };
49210
+ }
49211
+ }
48632
49212
  async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ctx) {
48633
49213
  const { writeOut, failures } = ctx;
48634
49214
  const optedIn = [];
@@ -48644,6 +49224,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
48644
49224
  const needsHindsight = config.litellm?.enabled === true && config.memory?.backend === "hindsight";
48645
49225
  if (optedIn.length === 0 && !needsHindsight)
48646
49226
  return;
49227
+ let brokerKeyMaterialized = false;
48647
49228
  const [{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 }, { ensureTeam: ensureTeam2, ensureKey: ensureKey2, validateKey: validateKey2, bindKeyToTeam: bindKeyToTeam2 }, { addAgentSecret: addAgentSecret2 }] = await Promise.all([
48648
49229
  Promise.resolve().then(() => (init_client(), exports_client)),
48649
49230
  Promise.resolve().then(() => (init_provision(), exports_provision)),
@@ -48753,6 +49334,14 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
48753
49334
  masterKey = resolved.entry.value;
48754
49335
  }
48755
49336
  }
49337
+ if (masterKey && !brokerKeyMaterialized) {
49338
+ const mat = materializeLitellmMasterKeyForBroker(masterKey, ctx.home ?? homedir49());
49339
+ brokerKeyMaterialized = true;
49340
+ if (!mat.ok) {
49341
+ ctx.writeErr(source_default.yellow(` ! litellm: could not materialize master key for auth-broker external-spend (${mat.error})
49342
+ `));
49343
+ }
49344
+ }
48756
49345
  if (existing.kind === "ok") {
48757
49346
  const storedKey = existing.entry.kind === "string" ? existing.entry.value : null;
48758
49347
  let driftReprovision = false;
@@ -49782,6 +50371,7 @@ var init_apply = __esm(() => {
49782
50371
  init_connection_health();
49783
50372
  init_update_prompt_hook();
49784
50373
  init_compose();
50374
+ init_external_spend();
49785
50375
  init_write_compose();
49786
50376
  init_profiles();
49787
50377
  init_install_detect();
@@ -64018,6 +64608,12 @@ var init_mapping = __esm(() => {
64018
64608
  severity: 1,
64019
64609
  job_spec: "feel-like-a-colleague",
64020
64610
  signature: "represent:obligation-escalation"
64611
+ },
64612
+ "litellm-header-passthrough-misconfig": {
64613
+ failure_mode: "constraint-violation",
64614
+ severity: 3,
64615
+ job_spec: "keep-my-subscription-honest",
64616
+ signature: "litellm-header-passthrough:oauth-leak-scope"
64021
64617
  }
64022
64618
  };
64023
64619
  ALL_JOB_SPECS = [
@@ -64208,6 +64804,122 @@ var init_test_agents = __esm(() => {
64208
64804
  ];
64209
64805
  });
64210
64806
 
64807
+ // src/litellm/header-passthrough-guard.ts
64808
+ function isClaudeAllowlistedGroup(name) {
64809
+ if (name.endsWith("-openrouter"))
64810
+ return false;
64811
+ return name.startsWith("claude-") || name === "sonnet" || name === "fable";
64812
+ }
64813
+ function flagTruthy(v) {
64814
+ return v === true || v === "true";
64815
+ }
64816
+ function* iterGroups(mgs) {
64817
+ if (!mgs || typeof mgs !== "object")
64818
+ return;
64819
+ if (Array.isArray(mgs)) {
64820
+ for (const entry of mgs) {
64821
+ if (!entry || typeof entry !== "object")
64822
+ continue;
64823
+ const e = entry;
64824
+ const name = typeof e.model_group === "string" && e.model_group || typeof e.group_name === "string" && e.group_name || typeof e.model_name === "string" && e.model_name || null;
64825
+ if (name)
64826
+ yield [name, e];
64827
+ }
64828
+ return;
64829
+ }
64830
+ for (const [name, settings] of Object.entries(mgs)) {
64831
+ if (settings && typeof settings === "object") {
64832
+ yield [name, settings];
64833
+ }
64834
+ }
64835
+ }
64836
+ function detectHeaderMisconfig(parsed) {
64837
+ const violations = [];
64838
+ if (!parsed || typeof parsed !== "object")
64839
+ return violations;
64840
+ const root = parsed;
64841
+ const ls = root.litellm_settings;
64842
+ if (ls && typeof ls === "object" && flagTruthy(ls[FORWARD_HEADERS_FLAG])) {
64843
+ violations.push({
64844
+ scope: "global",
64845
+ detail: `${FORWARD_HEADERS_FLAG}: true under litellm_settings \u2014 the GLOBAL ` + `master switch forwards the subscription OAuth Authorization header ` + `to EVERY upstream (OpenRouter/OpenAI included). Scope it to Claude ` + `groups only.`
64846
+ });
64847
+ }
64848
+ for (const [group, settings] of iterGroups(root.model_group_settings)) {
64849
+ if (flagTruthy(settings[FORWARD_HEADERS_FLAG]) && !isClaudeAllowlistedGroup(group)) {
64850
+ violations.push({
64851
+ scope: "model_group",
64852
+ group,
64853
+ detail: `${FORWARD_HEADERS_FLAG}: true on non-Claude group '${group}' \u2014 ` + `forwards the subscription OAuth Authorization header to a ` + `non-subscription upstream. Remove it from this group.`
64854
+ });
64855
+ }
64856
+ }
64857
+ return violations;
64858
+ }
64859
+ function parseLitellmConfig(text) {
64860
+ try {
64861
+ return import_yaml26.parse(text);
64862
+ } catch {
64863
+ return null;
64864
+ }
64865
+ }
64866
+ var import_yaml26, FORWARD_HEADERS_FLAG = "forward_client_headers_to_llm_api", DEFAULT_LITELLM_CONFIG_PATH = "/data/coolify/services/vhz4jc1tzvk6gdql8jueiwq4/litellm-config.yaml";
64867
+ var init_header_passthrough_guard = __esm(() => {
64868
+ import_yaml26 = __toESM(require_dist(), 1);
64869
+ });
64870
+
64871
+ // src/fleet-health/litellm-config-sensor.ts
64872
+ import { readFileSync as readFileSync85, existsSync as existsSync98 } from "node:fs";
64873
+ function resolveLitellmConfigPath(explicit) {
64874
+ return explicit ?? process.env.LITELLM_CONFIG_PATH ?? DEFAULT_LITELLM_CONFIG_PATH;
64875
+ }
64876
+ function scanLitellmConfig(opts = {}) {
64877
+ const path9 = resolveLitellmConfigPath(opts.path);
64878
+ const exists = opts.existsFn ?? existsSync98;
64879
+ const read = opts.readFn ?? ((p) => readFileSync85(p, "utf-8"));
64880
+ const log = opts.log ?? (() => {});
64881
+ const nowIso = opts.nowIso ?? new Date().toISOString();
64882
+ if (!exists(path9)) {
64883
+ log(`fleet-health: litellm-config sensor SKIPPED \u2014 config file absent at ${path9} ` + `(set LITELLM_CONFIG_PATH if it moved; hermetic CI/dev expected to skip)`);
64884
+ return { status: "skipped", path: path9, findings: [] };
64885
+ }
64886
+ let text;
64887
+ try {
64888
+ text = read(path9);
64889
+ } catch (e) {
64890
+ log(`fleet-health: litellm-config sensor SKIPPED \u2014 ${path9} unreadable: ${String(e)}`);
64891
+ return { status: "skipped", path: path9, findings: [] };
64892
+ }
64893
+ const parsed = parseLitellmConfig(text);
64894
+ if (parsed == null) {
64895
+ log(`fleet-health: litellm-config sensor SKIPPED \u2014 ${path9} unparseable YAML`);
64896
+ return { status: "skipped", path: path9, findings: [] };
64897
+ }
64898
+ const violations = detectHeaderMisconfig(parsed);
64899
+ if (violations.length === 0) {
64900
+ log(`fleet-health: litellm-config sensor OK \u2014 header passthrough correctly scoped (${path9})`);
64901
+ return { status: "ok", path: path9, findings: [] };
64902
+ }
64903
+ const findings = violations.map((v, i) => {
64904
+ const where = v.scope === "global" ? "litellm_settings (global)" : `group '${v.group}'`;
64905
+ return {
64906
+ signal: "litellm-header-passthrough-misconfig",
64907
+ agent: LITELLM_PROXY_PSEUDO_AGENT,
64908
+ turn_id: `litellm-config:${v.scope}:${v.group ?? "global"}`,
64909
+ log_pointer: `${path9}: ${where} \u2014 ${v.detail}`,
64910
+ ts: nowIso
64911
+ };
64912
+ });
64913
+ for (const f of findings) {
64914
+ log(`fleet-health: litellm-config sensor VIOLATION \u2014 ${f.log_pointer}`);
64915
+ }
64916
+ return { status: "violation", path: path9, findings };
64917
+ }
64918
+ var LITELLM_PROXY_PSEUDO_AGENT = "litellm-proxy";
64919
+ var init_litellm_config_sensor = __esm(() => {
64920
+ init_header_passthrough_guard();
64921
+ });
64922
+
64211
64923
  // src/fleet-health/scan.ts
64212
64924
  var exports_scan = {};
64213
64925
  __export(exports_scan, {
@@ -64219,9 +64931,9 @@ __export(exports_scan, {
64219
64931
  ledgerPathForBase: () => ledgerPathForBase
64220
64932
  });
64221
64933
  import {
64222
- readFileSync as readFileSync85,
64934
+ readFileSync as readFileSync86,
64223
64935
  readdirSync as readdirSync38,
64224
- existsSync as existsSync98,
64936
+ existsSync as existsSync99,
64225
64937
  mkdirSync as mkdirSync56,
64226
64938
  writeFileSync as writeFileSync37
64227
64939
  } from "node:fs";
@@ -64232,7 +64944,7 @@ function resolveSwitchroomBase(home2 = process.env.SWITCHROOM_HOME ?? process.en
64232
64944
  }
64233
64945
  function listAgents(base) {
64234
64946
  const dir = resolve57(base, "agents");
64235
- if (!existsSync98(dir))
64947
+ if (!existsSync99(dir))
64236
64948
  return [];
64237
64949
  try {
64238
64950
  return readdirSync38(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
@@ -64261,16 +64973,16 @@ function runScan(opts = {}) {
64261
64973
  let gwText = "";
64262
64974
  let sawArtifact = false;
64263
64975
  try {
64264
- if (existsSync98(turnsPath)) {
64265
- turnsText = readFileSync85(turnsPath, "utf-8");
64976
+ if (existsSync99(turnsPath)) {
64977
+ turnsText = readFileSync86(turnsPath, "utf-8");
64266
64978
  sawArtifact = true;
64267
64979
  }
64268
64980
  } catch (e) {
64269
64981
  log(`fleet-health: WARN skipping ${agent} turns.jsonl unreadable: ${String(e)}`);
64270
64982
  }
64271
64983
  try {
64272
- if (existsSync98(gwPath)) {
64273
- gwText = readFileSync85(gwPath, "utf-8");
64984
+ if (existsSync99(gwPath)) {
64985
+ gwText = readFileSync86(gwPath, "utf-8");
64274
64986
  sawArtifact = true;
64275
64987
  }
64276
64988
  } catch (e) {
@@ -64292,6 +65004,8 @@ function runScan(opts = {}) {
64292
65004
  skipped.push(agent);
64293
65005
  }
64294
65006
  }
65007
+ const litellm = scanLitellmConfig({ path: opts.litellmConfigPath, log });
65008
+ findings.push(...litellm.findings);
64295
65009
  const prior = readLedgerIfPresent(base);
64296
65010
  const ledger = buildLedger(findings, {
64297
65011
  ownerAgent: opts.ownerAgent,
@@ -64310,9 +65024,9 @@ function runScan(opts = {}) {
64310
65024
  function readLedgerIfPresent(base) {
64311
65025
  const path9 = ledgerPathForBase(base);
64312
65026
  try {
64313
- if (!existsSync98(path9))
65027
+ if (!existsSync99(path9))
64314
65028
  return null;
64315
- return JSON.parse(readFileSync85(path9, "utf-8"));
65029
+ return JSON.parse(readFileSync86(path9, "utf-8"));
64316
65030
  } catch {
64317
65031
  return null;
64318
65032
  }
@@ -64332,6 +65046,7 @@ var init_scan = __esm(() => {
64332
65046
  init_detect();
64333
65047
  init_ledger();
64334
65048
  init_test_agents();
65049
+ init_litellm_config_sensor();
64335
65050
  });
64336
65051
 
64337
65052
  // src/cli/index.ts
@@ -78331,7 +79046,7 @@ Cross-agent reflection plan
78331
79046
  }
78332
79047
  console.log();
78333
79048
  }));
78334
- memory.command("setup").description("Manage the Hindsight Docker container").option("--stop", "Stop and remove the Hindsight container").option("--status", "Show Hindsight container status").option("--recreate", "Pull the latest image and recreate the container (reusing its current port). Used by `switchroom update` to keep the hindsight singleton current.").option("--tag <version>", "Pin the hindsight image tag to pull + run (e.g. v0.15.18), overriding the " + "default floating `:latest`. Threaded through by `switchroom rollout` so a " + "version-pinned roll recreates hindsight on the SAME tag as the rest of the " + "fleet. Omit for `:latest` (the standalone default).").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").action(async (opts) => {
79049
+ memory.command("setup").description("Manage the Hindsight Docker container").option("--stop", "Stop and remove the Hindsight container").option("--status", "Show Hindsight container status").option("--recreate", "Pull the latest image and recreate the container (reusing its current port). Used by `switchroom update` to keep the hindsight singleton current. Also required after any `hindsight.llm` model/provider config change \u2014 env is only re-derived on recreate, a plain docker restart is not enough (see docs/operators/hindsight-model-change.md).").option("--tag <version>", "Pin the hindsight image tag to pull + run (e.g. v0.15.18), overriding the " + "default floating `:latest`. Threaded through by `switchroom rollout` so a " + "version-pinned roll recreates hindsight on the SAME tag as the rest of the " + "fleet. Omit for `:latest` (the standalone default).").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").action(async (opts) => {
78335
79050
  if (opts.status) {
78336
79051
  if (!isDockerAvailable()) {
78337
79052
  console.log(source_default.red(" Docker is not available."));