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.
- package/dist/agent-scheduler/index.js +29 -1
- package/dist/auth-broker/index.js +552 -48
- package/dist/cli/autoaccept-poll.js +29 -1
- package/dist/cli/drive-write-pretool.mjs +30 -2
- package/dist/cli/ms-365-write-pretool.mjs +30 -2
- package/dist/cli/switchroom.js +751 -36
- package/dist/host-control/main.js +3 -3
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +1 -0
- package/skills/switchroom-cli/SKILL.md +25 -0
- package/telegram-plugin/auth-snapshot-format.ts +39 -0
- package/telegram-plugin/dist/gateway/gateway.js +363 -25
- package/telegram-plugin/external-spend.ts +135 -0
- package/telegram-plugin/gateway/gateway.ts +83 -67
- package/telegram-plugin/gateway/model-command.ts +106 -0
- package/telegram-plugin/gateway/narrative-lane.ts +23 -9
- package/telegram-plugin/gateway/status-pin-store.ts +64 -4
- package/telegram-plugin/gateway/usage-mask.ts +29 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +19 -2
- package/telegram-plugin/quota-bar-format.ts +18 -0
- package/telegram-plugin/quota-check.ts +17 -2
- package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
- package/telegram-plugin/tests/external-spend.test.ts +168 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +57 -23
- package/telegram-plugin/tests/quota-bar-format.test.ts +43 -0
- package/telegram-plugin/tests/quota-check.test.ts +57 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +198 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +50 -0
- package/telegram-plugin/tests/usage-footer-freshness.test.ts +141 -0
- package/telegram-plugin/tests/usage-mask.test.ts +35 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +27 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +131 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.19.
|
|
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,
|
|
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
|
-
|
|
15714
|
-
|
|
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
|
|
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
|
-
|
|
15753
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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, {
|
|
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
|
|
64934
|
+
readFileSync as readFileSync86,
|
|
64223
64935
|
readdirSync as readdirSync38,
|
|
64224
|
-
existsSync as
|
|
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 (!
|
|
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 (
|
|
64265
|
-
turnsText =
|
|
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 (
|
|
64273
|
-
gwText =
|
|
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 (!
|
|
65027
|
+
if (!existsSync99(path9))
|
|
64314
65028
|
return null;
|
|
64315
|
-
return JSON.parse(
|
|
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."));
|