switchroom 0.19.30 → 0.19.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/switchroom.js +1121 -584
- package/dist/host-control/main.js +151 -73
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +5 -1
- package/profiles/_base/start.sh.hbs +15 -1
- package/telegram-plugin/dist/bridge/bridge.js +3 -0
- package/telegram-plugin/dist/gateway/gateway.js +1660 -677
- package/telegram-plugin/dist/server.js +3 -0
- package/telegram-plugin/edit-flood-fuse.ts +70 -20
- package/telegram-plugin/gateway/boot-beacon.ts +364 -0
- package/telegram-plugin/gateway/boot-sweep-gate.ts +20 -15
- package/telegram-plugin/gateway/gateway.ts +87 -88
- package/telegram-plugin/gateway/inbound-spool.ts +39 -0
- package/telegram-plugin/gateway/narrative-lane.ts +12 -0
- package/telegram-plugin/gateway/obligation-store.ts +28 -0
- package/telegram-plugin/gateway/stale-pin-sweep-store.ts +221 -0
- package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +211 -0
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +804 -0
- package/telegram-plugin/gateway/stale-pin-sweep.ts +1146 -0
- package/telegram-plugin/gateway/status-pin-retarget.ts +15 -2
- package/telegram-plugin/gateway/status-pin-store.ts +33 -11
- package/telegram-plugin/gateway/stream-render.ts +578 -321
- package/telegram-plugin/registry/turns-schema.ts +21 -1
- package/telegram-plugin/retry-api-call.ts +46 -21
- package/telegram-plugin/session-tail.ts +13 -0
- package/telegram-plugin/shared/bot-runtime.ts +61 -17
- package/telegram-plugin/shared/gw-trace-gate.ts +18 -2
- package/telegram-plugin/tests/activity-card-wiring.test.ts +7 -7
- package/telegram-plugin/tests/activity-drain-fuse-drop-not-failure.test.ts +324 -0
- package/telegram-plugin/tests/agent-card-result-footer.test.ts +193 -0
- package/telegram-plugin/tests/boot-beacon.test.ts +462 -0
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +6 -6
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +42 -31
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +2 -0
- package/telegram-plugin/tests/inbound-spool-progress.test.ts +2 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +134 -5
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +28 -0
- package/telegram-plugin/tests/obligation-determinism.test.ts +2 -0
- package/telegram-plugin/tests/obligation-store.test.ts +67 -1
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +3 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +26 -5
- package/telegram-plugin/tests/stream-render-golden.test.ts +20 -5
- package/telegram-plugin/tests/tg-post-logger-error-shape.test.ts +161 -0
- package/telegram-plugin/tests/turn-mint-defers-until-dequeue.test.ts +196 -0
- package/telegram-plugin/tests/turn-mint-harness.ts +155 -0
- package/telegram-plugin/tests/turn-supersede-finalizes-prior-card.test.ts +124 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +30 -0
- package/telegram-plugin/tool-activity-summary.ts +104 -38
- package/telegram-plugin/worker-activity-feed.ts +33 -16
- package/telegram-plugin/gateway/dm-pin-sweep.test.ts +0 -251
- package/telegram-plugin/gateway/dm-pin-sweep.ts +0 -178
|
@@ -19060,9 +19060,9 @@ var require_commander = __commonJS((exports2) => {
|
|
|
19060
19060
|
});
|
|
19061
19061
|
|
|
19062
19062
|
// src/host-control/main.ts
|
|
19063
|
-
import { homedir as
|
|
19064
|
-
import { existsSync as
|
|
19065
|
-
import { join as
|
|
19063
|
+
import { homedir as homedir8 } from "node:os";
|
|
19064
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7 } from "node:fs";
|
|
19065
|
+
import { join as join14, resolve as resolve10 } from "node:path";
|
|
19066
19066
|
|
|
19067
19067
|
// src/config/loader.ts
|
|
19068
19068
|
init_zod();
|
|
@@ -21210,7 +21210,7 @@ function allocateAgentUid(name) {
|
|
|
21210
21210
|
}
|
|
21211
21211
|
|
|
21212
21212
|
// src/build-info.ts
|
|
21213
|
-
var VERSION = "0.19.
|
|
21213
|
+
var VERSION = "0.19.32";
|
|
21214
21214
|
|
|
21215
21215
|
// src/setup/hindsight-recall-tunables.ts
|
|
21216
21216
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
@@ -21767,8 +21767,8 @@ import { createServer } from "node:net";
|
|
|
21767
21767
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
21768
21768
|
import { mkdir as mkdir2, chmod, chown, unlink, appendFile } from "node:fs/promises";
|
|
21769
21769
|
import {
|
|
21770
|
-
readdirSync as
|
|
21771
|
-
existsSync as
|
|
21770
|
+
readdirSync as readdirSync6,
|
|
21771
|
+
existsSync as existsSync15,
|
|
21772
21772
|
readFileSync as readFileSync11,
|
|
21773
21773
|
writeFileSync as writeFileSync6,
|
|
21774
21774
|
renameSync as renameSync5,
|
|
@@ -21781,7 +21781,7 @@ import {
|
|
|
21781
21781
|
copyFileSync as copyFileSync3,
|
|
21782
21782
|
unlinkSync as unlinkSync5
|
|
21783
21783
|
} from "node:fs";
|
|
21784
|
-
import { join as
|
|
21784
|
+
import { join as join13, dirname as dirname9, resolve as resolve9 } from "node:path";
|
|
21785
21785
|
import { createHash as createHash6, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
21786
21786
|
|
|
21787
21787
|
// src/host-control/protocol.ts
|
|
@@ -27739,7 +27739,51 @@ function recoverPinJournal(configPath, opts = {}) {
|
|
|
27739
27739
|
}
|
|
27740
27740
|
|
|
27741
27741
|
// src/cli/operator-uid.ts
|
|
27742
|
-
|
|
27742
|
+
import {
|
|
27743
|
+
chownSync,
|
|
27744
|
+
existsSync as existsSync12,
|
|
27745
|
+
lstatSync as lstatSync2,
|
|
27746
|
+
readdirSync as readdirSync5,
|
|
27747
|
+
realpathSync as realpathSync2,
|
|
27748
|
+
statSync as statSync7
|
|
27749
|
+
} from "node:fs";
|
|
27750
|
+
import { homedir as homedir6 } from "node:os";
|
|
27751
|
+
import { join as join9 } from "node:path";
|
|
27752
|
+
var CONTAINER_OPERATOR_HOME = "/host-home";
|
|
27753
|
+
var SWITCHROOM_HOME_MARKER = "switchroom.yaml";
|
|
27754
|
+
function operatorHomeCandidates(env, home2) {
|
|
27755
|
+
const roots = [env.SWITCHROOM_HOST_HOME?.trim(), home2, CONTAINER_OPERATOR_HOME];
|
|
27756
|
+
const out = [];
|
|
27757
|
+
for (const r of roots) {
|
|
27758
|
+
if (!r || r.length === 0)
|
|
27759
|
+
continue;
|
|
27760
|
+
const dir = join9(r, ".switchroom");
|
|
27761
|
+
if (!out.includes(dir))
|
|
27762
|
+
out.push(dir);
|
|
27763
|
+
}
|
|
27764
|
+
return out;
|
|
27765
|
+
}
|
|
27766
|
+
function resolveOperatorUidFromOwnership(deps = {}) {
|
|
27767
|
+
const env = deps.env ?? process.env;
|
|
27768
|
+
const home2 = deps.home ?? homedir6();
|
|
27769
|
+
const exists = deps.exists ?? ((p) => existsSync12(p));
|
|
27770
|
+
const statUid = deps.statUid ?? ((p) => {
|
|
27771
|
+
try {
|
|
27772
|
+
return statSync7(p).uid;
|
|
27773
|
+
} catch {
|
|
27774
|
+
return;
|
|
27775
|
+
}
|
|
27776
|
+
});
|
|
27777
|
+
for (const dir of operatorHomeCandidates(env, home2)) {
|
|
27778
|
+
if (!exists(join9(dir, SWITCHROOM_HOME_MARKER)))
|
|
27779
|
+
continue;
|
|
27780
|
+
const uid = statUid(dir);
|
|
27781
|
+
if (uid !== undefined && Number.isFinite(uid) && uid > 0)
|
|
27782
|
+
return uid;
|
|
27783
|
+
}
|
|
27784
|
+
return;
|
|
27785
|
+
}
|
|
27786
|
+
function resolveOperatorUid(ownershipDeps = {}) {
|
|
27743
27787
|
const sudoUid = process.env.SUDO_UID;
|
|
27744
27788
|
if (sudoUid !== undefined) {
|
|
27745
27789
|
const parsed = parseInt(sudoUid, 10);
|
|
@@ -27757,20 +27801,20 @@ function resolveOperatorUid() {
|
|
|
27757
27801
|
if (Number.isFinite(parsed) && parsed > 0)
|
|
27758
27802
|
return parsed;
|
|
27759
27803
|
}
|
|
27760
|
-
return;
|
|
27804
|
+
return resolveOperatorUidFromOwnership(ownershipDeps);
|
|
27761
27805
|
}
|
|
27762
27806
|
|
|
27763
27807
|
// src/cli/rollout.ts
|
|
27764
27808
|
init_atomic();
|
|
27765
27809
|
|
|
27766
27810
|
// src/cli/resolve-version.ts
|
|
27767
|
-
import { existsSync as
|
|
27768
|
-
import { dirname as dirname7, join as
|
|
27811
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "node:fs";
|
|
27812
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
27769
27813
|
function readPackageVersion() {
|
|
27770
27814
|
let dir = import.meta.dirname;
|
|
27771
27815
|
for (let i = 0;i < 12 && dir && dir !== "/"; i++) {
|
|
27772
|
-
const p =
|
|
27773
|
-
if (
|
|
27816
|
+
const p = join10(dir, "package.json");
|
|
27817
|
+
if (existsSync13(p)) {
|
|
27774
27818
|
try {
|
|
27775
27819
|
const pkg = JSON.parse(readFileSync9(p, "utf-8"));
|
|
27776
27820
|
if (pkg?.name === "switchroom" && typeof pkg.version === "string") {
|
|
@@ -27869,6 +27913,9 @@ function parseAuditLine(line) {
|
|
|
27869
27913
|
entry.failed_step = o.failed_step;
|
|
27870
27914
|
if (typeof o.failed_agent === "string")
|
|
27871
27915
|
entry.failed_agent = o.failed_agent;
|
|
27916
|
+
if (Array.isArray(o.drifted) && o.drifted.every((x) => typeof x === "string")) {
|
|
27917
|
+
entry.drifted = o.drifted;
|
|
27918
|
+
}
|
|
27872
27919
|
if (typeof o.agent === "string")
|
|
27873
27920
|
entry.agent = o.agent;
|
|
27874
27921
|
if (typeof o.n === "number")
|
|
@@ -27900,6 +27947,7 @@ function latestRolloutRowForRequest(raw, requestId) {
|
|
|
27900
27947
|
var DOCKER_INSPECT_TIMEOUT_MS = 60 * 1000;
|
|
27901
27948
|
|
|
27902
27949
|
// src/cli/rollout.ts
|
|
27950
|
+
var VERIFY_COMPONENTS_STEP = "verify-components";
|
|
27903
27951
|
function isVersionAssertable(target) {
|
|
27904
27952
|
return /^v?\d+\.\d+\.\d+$/.test(target.trim());
|
|
27905
27953
|
}
|
|
@@ -27973,7 +28021,7 @@ var ROLLOUT_RUN_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_RUN_TIMEOUT_MS", 20 * 60
|
|
|
27973
28021
|
var ROLLOUT_PROBE_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_PROBE_TIMEOUT_MS", 60 * 1000);
|
|
27974
28022
|
|
|
27975
28023
|
// src/cli/write-compose.ts
|
|
27976
|
-
import { chownSync } from "node:fs";
|
|
28024
|
+
import { chownSync as chownSync2 } from "node:fs";
|
|
27977
28025
|
import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
|
|
27978
28026
|
var AGENT_IMAGE_TAG_RE = /image:\s*\S*switchroom-agent:(\S+)/;
|
|
27979
28027
|
var BACKUP_SUFFIX = ".bak";
|
|
@@ -28062,7 +28110,7 @@ async function restoreComposeBackup(composePath, opts = {}) {
|
|
|
28062
28110
|
const operatorUid = resolveOperatorUid();
|
|
28063
28111
|
if (operatorUid !== undefined && process.geteuid?.() === 0) {
|
|
28064
28112
|
try {
|
|
28065
|
-
|
|
28113
|
+
chownSync2(composePath, operatorUid, operatorUid);
|
|
28066
28114
|
} catch {}
|
|
28067
28115
|
}
|
|
28068
28116
|
return { restored: true, imageTag: composeImageTag(content) };
|
|
@@ -28072,8 +28120,8 @@ async function restoreComposeBackup(composePath, opts = {}) {
|
|
|
28072
28120
|
var import__2 = __toESM(require_commander(), 1);
|
|
28073
28121
|
|
|
28074
28122
|
// src/cli/update.ts
|
|
28075
|
-
import { join as
|
|
28076
|
-
import { homedir as
|
|
28123
|
+
import { join as join11, dirname as dirname8, resolve as resolve8 } from "node:path";
|
|
28124
|
+
import { homedir as homedir7 } from "node:os";
|
|
28077
28125
|
|
|
28078
28126
|
// src/agents/lifecycle.ts
|
|
28079
28127
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -28169,7 +28217,7 @@ function getAllAgentStatuses(config) {
|
|
|
28169
28217
|
|
|
28170
28218
|
// src/cli/update.ts
|
|
28171
28219
|
init_atomic();
|
|
28172
|
-
var DEFAULT_COMPOSE_PATH =
|
|
28220
|
+
var DEFAULT_COMPOSE_PATH = join11(homedir7(), ".switchroom", "compose", "docker-compose.yml");
|
|
28173
28221
|
var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
|
|
28174
28222
|
function parseUpdateResultLine(stdout) {
|
|
28175
28223
|
const idx = stdout.lastIndexOf(UPDATE_RESULT_SENTINEL);
|
|
@@ -28192,9 +28240,9 @@ function parseUpdateResultLine(stdout) {
|
|
|
28192
28240
|
|
|
28193
28241
|
// src/host-control/config-edit-validator.ts
|
|
28194
28242
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
28195
|
-
import { mkdtempSync, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as
|
|
28243
|
+
import { mkdtempSync, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
|
|
28196
28244
|
import { tmpdir } from "node:os";
|
|
28197
|
-
import { join as
|
|
28245
|
+
import { join as join12, isAbsolute as isAbsolute3, normalize, basename as basename4 } from "node:path";
|
|
28198
28246
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
28199
28247
|
import { isDeepStrictEqual } from "node:util";
|
|
28200
28248
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
@@ -28334,7 +28382,7 @@ function validateShape(unifiedDiff, targetPath) {
|
|
|
28334
28382
|
return null;
|
|
28335
28383
|
}
|
|
28336
28384
|
function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
28337
|
-
if (!
|
|
28385
|
+
if (!existsSync14(configPath)) {
|
|
28338
28386
|
return {
|
|
28339
28387
|
ok: false,
|
|
28340
28388
|
code: "E_PATCH_APPLY_FAILED",
|
|
@@ -28342,13 +28390,13 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
|
28342
28390
|
};
|
|
28343
28391
|
}
|
|
28344
28392
|
const liveContent = readFileSync10(configPath, "utf8");
|
|
28345
|
-
const scratchDir = mkdtempSync(
|
|
28393
|
+
const scratchDir = mkdtempSync(join12(tmpdir(), "config-propose-edit-"));
|
|
28346
28394
|
try {
|
|
28347
28395
|
const targetBasename = configPath.split("/").pop() ?? "switchroom.yaml";
|
|
28348
|
-
const scratchFile =
|
|
28396
|
+
const scratchFile = join12(scratchDir, targetBasename);
|
|
28349
28397
|
writeFileSync5(scratchFile, liveContent);
|
|
28350
28398
|
const normalizedDiff = normalizeDiffHeadersToBasename(unifiedDiff, targetBasename);
|
|
28351
|
-
const patchFile =
|
|
28399
|
+
const patchFile = join12(scratchDir, "proposal.patch");
|
|
28352
28400
|
writeFileSync5(patchFile, normalizedDiff);
|
|
28353
28401
|
const bin = gitBin ?? "git";
|
|
28354
28402
|
const baseArgs = [
|
|
@@ -28576,7 +28624,7 @@ function validateConfigEdit(opts) {
|
|
|
28576
28624
|
return schemaErr;
|
|
28577
28625
|
let beforeData = {};
|
|
28578
28626
|
try {
|
|
28579
|
-
const beforeRaw =
|
|
28627
|
+
const beforeRaw = existsSync14(opts.configPath) ? readFileSync10(opts.configPath, "utf8") : "";
|
|
28580
28628
|
const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
|
|
28581
28629
|
beforeData = beforeDoc.toJS();
|
|
28582
28630
|
} catch {
|
|
@@ -28917,8 +28965,8 @@ function etaLine(s) {
|
|
|
28917
28965
|
function deferredLines(target) {
|
|
28918
28966
|
const bare = target.trim().replace(/^v/, "");
|
|
28919
28967
|
return [
|
|
28920
|
-
`**
|
|
28921
|
-
|
|
28968
|
+
`**Verified on ${target}** — every component this roll owned passed \`verify-components\`, so this is host convergence, not just the agents. Anything the roll was told to skip is named in its warnings.`,
|
|
28969
|
+
`**Still host-side (nothing in a roll can do these):**`,
|
|
28922
28970
|
`- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
|
|
28923
28971
|
`- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
|
|
28924
28972
|
];
|
|
@@ -28948,6 +28996,21 @@ function renderWith(s, compact) {
|
|
|
28948
28996
|
if (s.deferred !== false)
|
|
28949
28997
|
parts2.push("", ...deferredLines(s.target));
|
|
28950
28998
|
return parts2.join(`
|
|
28999
|
+
`);
|
|
29000
|
+
}
|
|
29001
|
+
if (s.terminal === "error" && s.failedStep === VERIFY_COMPONENTS_STEP) {
|
|
29002
|
+
const drifted = s.drifted ?? [];
|
|
29003
|
+
const parts2 = [headerLine(s, "⚠️")];
|
|
29004
|
+
let summary = `**INCOMPLETE** — ${rolledCount}${s.m !== undefined ? `/${s.m}` : ""} ` + `agent(s) reached ${s.target}`;
|
|
29005
|
+
if (elapsedMs !== undefined)
|
|
29006
|
+
summary += ` in ${formatDurationMs(elapsedMs)}`;
|
|
29007
|
+
summary += `, but the host did NOT fully converge.`;
|
|
29008
|
+
parts2.push(summary);
|
|
29009
|
+
parts2.push("", `**Still behind ${s.target}:** ` + (drifted.length > 0 ? drifted.map((d) => `\`${d}\``).join(", ") : "one or more components (see the roll's warnings)"));
|
|
29010
|
+
parts2.push("", `Re-running the roll will NOT fix this — the agents are already on ` + `target. Finish the stale component(s), then \`switchroom update --check\`.`);
|
|
29011
|
+
if (checklist.length > 0)
|
|
29012
|
+
parts2.push("", ...checklist);
|
|
29013
|
+
return parts2.join(`
|
|
28951
29014
|
`);
|
|
28952
29015
|
}
|
|
28953
29016
|
if (s.terminal === "error") {
|
|
@@ -29078,9 +29141,9 @@ function resolveDigests(imageRefs) {
|
|
|
29078
29141
|
return out;
|
|
29079
29142
|
}
|
|
29080
29143
|
function readCachedInstallType(bindRoot) {
|
|
29081
|
-
const cacheDir =
|
|
29082
|
-
const cachePath =
|
|
29083
|
-
if (
|
|
29144
|
+
const cacheDir = join13(bindRoot, ".switchroom");
|
|
29145
|
+
const cachePath = join13(cacheDir, "install-type.json");
|
|
29146
|
+
if (existsSync15(cachePath)) {
|
|
29084
29147
|
try {
|
|
29085
29148
|
const raw = readFileSync11(cachePath, "utf-8");
|
|
29086
29149
|
const parsed = JSON.parse(raw);
|
|
@@ -29169,7 +29232,7 @@ class HostdServer {
|
|
|
29169
29232
|
fleetMutationInFlight = null;
|
|
29170
29233
|
lastAutoRolloutFailedPin = null;
|
|
29171
29234
|
autoRolloutLatchPath() {
|
|
29172
|
-
return
|
|
29235
|
+
return join13(this.hostdDirPath(), AUTO_ROLLOUT_LATCH_FILENAME);
|
|
29173
29236
|
}
|
|
29174
29237
|
readAutoRolloutLatch() {
|
|
29175
29238
|
try {
|
|
@@ -29212,7 +29275,7 @@ class HostdServer {
|
|
|
29212
29275
|
return resolveHostdConfigPath(this.opts.configPath);
|
|
29213
29276
|
}
|
|
29214
29277
|
fleetComposePath() {
|
|
29215
|
-
return
|
|
29278
|
+
return join13(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
29216
29279
|
}
|
|
29217
29280
|
recoverRolloutPinJournal(context) {
|
|
29218
29281
|
let note = null;
|
|
@@ -29251,7 +29314,7 @@ class HostdServer {
|
|
|
29251
29314
|
return note;
|
|
29252
29315
|
}
|
|
29253
29316
|
async start() {
|
|
29254
|
-
const hostdDir =
|
|
29317
|
+
const hostdDir = join13(this.opts.homeDir, ".switchroom", "hostd");
|
|
29255
29318
|
await mkdir2(hostdDir, { recursive: true });
|
|
29256
29319
|
await chmod(hostdDir, 493).catch(() => {
|
|
29257
29320
|
return;
|
|
@@ -29272,13 +29335,13 @@ class HostdServer {
|
|
|
29272
29335
|
}
|
|
29273
29336
|
try {
|
|
29274
29337
|
for (const name of agentNames) {
|
|
29275
|
-
const dir =
|
|
29276
|
-
const sockPath =
|
|
29338
|
+
const dir = join13(hostdDir, name);
|
|
29339
|
+
const sockPath = join13(dir, "sock");
|
|
29277
29340
|
await mkdir2(dir, { recursive: true });
|
|
29278
29341
|
await chmod(dir, 493).catch(() => {
|
|
29279
29342
|
return;
|
|
29280
29343
|
});
|
|
29281
|
-
if (
|
|
29344
|
+
if (existsSync15(sockPath))
|
|
29282
29345
|
await unlink(sockPath).catch(() => {
|
|
29283
29346
|
return;
|
|
29284
29347
|
});
|
|
@@ -29309,13 +29372,13 @@ class HostdServer {
|
|
|
29309
29372
|
process.stderr.write(`hostd: SWITCHROOM_HOSTD_OPERATOR_UID='${opUidStr}' is not a positive integer; skipping operator listener
|
|
29310
29373
|
`);
|
|
29311
29374
|
} else {
|
|
29312
|
-
const dir =
|
|
29313
|
-
const sockPath =
|
|
29375
|
+
const dir = join13(hostdDir, "operator");
|
|
29376
|
+
const sockPath = join13(dir, "sock");
|
|
29314
29377
|
await mkdir2(dir, { recursive: true });
|
|
29315
29378
|
await chmod(dir, 493).catch(() => {
|
|
29316
29379
|
return;
|
|
29317
29380
|
});
|
|
29318
|
-
if (
|
|
29381
|
+
if (existsSync15(sockPath))
|
|
29319
29382
|
await unlink(sockPath).catch(() => {
|
|
29320
29383
|
return;
|
|
29321
29384
|
});
|
|
@@ -29351,7 +29414,7 @@ class HostdServer {
|
|
|
29351
29414
|
}
|
|
29352
29415
|
async reconcileOrphanedFleetMutations() {
|
|
29353
29416
|
const path3 = this.auditLogPath();
|
|
29354
|
-
if (!
|
|
29417
|
+
if (!existsSync15(path3))
|
|
29355
29418
|
return;
|
|
29356
29419
|
let raw;
|
|
29357
29420
|
try {
|
|
@@ -29754,11 +29817,11 @@ class HostdServer {
|
|
|
29754
29817
|
missingApplyAssets() {
|
|
29755
29818
|
const root = this.opts.applyAssetsRoot ?? resolve9(import.meta.dirname, "../..");
|
|
29756
29819
|
return [
|
|
29757
|
-
|
|
29758
|
-
|
|
29759
|
-
|
|
29760
|
-
|
|
29761
|
-
].filter((p) => !
|
|
29820
|
+
join13(root, "profiles"),
|
|
29821
|
+
join13(root, "profiles", "default"),
|
|
29822
|
+
join13(root, "vendor", "hindsight-memory"),
|
|
29823
|
+
join13(root, "skills")
|
|
29824
|
+
].filter((p) => !existsSync15(p));
|
|
29762
29825
|
}
|
|
29763
29826
|
applyAssetPreflight(request_id, started) {
|
|
29764
29827
|
const missing = this.missingApplyAssets();
|
|
@@ -30021,16 +30084,16 @@ class HostdServer {
|
|
|
30021
30084
|
return entry;
|
|
30022
30085
|
}
|
|
30023
30086
|
hostdDirPath() {
|
|
30024
|
-
return
|
|
30087
|
+
return join13(this.opts.homeDir, ".switchroom", "hostd");
|
|
30025
30088
|
}
|
|
30026
30089
|
hostdDirHostPath() {
|
|
30027
30090
|
const hostHome = this.opts.hostHomeDir ?? process.env.SWITCHROOM_HOST_HOME?.trim() ?? this.opts.homeDir;
|
|
30028
|
-
return
|
|
30091
|
+
return join13(hostHome, ".switchroom", "hostd");
|
|
30029
30092
|
}
|
|
30030
30093
|
async beginSelfBump(req, caller, started) {
|
|
30031
30094
|
const ownVersion = this.opts.selfVersion ?? SWITCHROOM_VERSION;
|
|
30032
|
-
const composePath =
|
|
30033
|
-
if (!
|
|
30095
|
+
const composePath = join13(this.hostdDirPath(), "docker-compose.yml");
|
|
30096
|
+
if (!existsSync15(composePath)) {
|
|
30034
30097
|
return deniedResponse(req.request_id, `rollout: hostd's CLI (v${ownVersion}) is older than the target ` + `${req.args.pin} and needs a self-bump first, but its compose file ` + `is missing at ${composePath}. Run \`switchroom hostd install ` + `--tag ${req.args.pin}\` on the host, then re-run the roll. ` + `Nothing was changed.`, Date.now() - started);
|
|
30035
30098
|
}
|
|
30036
30099
|
const before = readFileSync11(composePath, "utf8");
|
|
@@ -30052,12 +30115,12 @@ class HostdServer {
|
|
|
30052
30115
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
30053
30116
|
const bakPath = `${composePath}.bak-selfbump-${ts}`;
|
|
30054
30117
|
try {
|
|
30055
|
-
const baks =
|
|
30118
|
+
const baks = readdirSync6(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
|
|
30056
30119
|
for (const f of baks.slice(0, Math.max(0, baks.length - 4))) {
|
|
30057
|
-
unlinkSync5(
|
|
30120
|
+
unlinkSync5(join13(this.hostdDirPath(), f));
|
|
30058
30121
|
}
|
|
30059
30122
|
} catch {}
|
|
30060
|
-
const markerPath =
|
|
30123
|
+
const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
30061
30124
|
const marker = {
|
|
30062
30125
|
v: 1,
|
|
30063
30126
|
request_id: req.request_id,
|
|
@@ -30141,7 +30204,7 @@ class HostdServer {
|
|
|
30141
30204
|
rollbackBump();
|
|
30142
30205
|
entry.result = "error";
|
|
30143
30206
|
entry.finished_at = Date.now();
|
|
30144
|
-
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${
|
|
30207
|
+
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${join13(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)} on the host. ` + `Compose bump rolled back. Fallback: \`switchroom hostd install ` + `--tag ${req.args.pin}\` host-side, then re-run the roll.`;
|
|
30145
30208
|
if (this.fleetMutationInFlight && this.fleetMutationInFlight.request_id === entry.request_id) {
|
|
30146
30209
|
this.fleetMutationInFlight = null;
|
|
30147
30210
|
}
|
|
@@ -30165,8 +30228,8 @@ class HostdServer {
|
|
|
30165
30228
|
};
|
|
30166
30229
|
}
|
|
30167
30230
|
async resumePendingSelfBumpRollout() {
|
|
30168
|
-
const markerPath =
|
|
30169
|
-
if (!
|
|
30231
|
+
const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
30232
|
+
if (!existsSync15(markerPath))
|
|
30170
30233
|
return;
|
|
30171
30234
|
let raw;
|
|
30172
30235
|
try {
|
|
@@ -30221,7 +30284,7 @@ class HostdServer {
|
|
|
30221
30284
|
return;
|
|
30222
30285
|
}
|
|
30223
30286
|
if (needsSelfBump(ownVersion, marker.pin)) {
|
|
30224
|
-
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${
|
|
30287
|
+
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${join13(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)}). ` + `Fallback: \`switchroom hostd install --tag ${marker.pin}\` ` + `host-side, then re-issue the rollout.`);
|
|
30225
30288
|
return;
|
|
30226
30289
|
}
|
|
30227
30290
|
process.stderr.write(`hostd: resuming rollout ${marker.request_id} → ${marker.pin} after ` + `self-bump (v${marker.prior_hostd_version} → v${ownVersion})
|
|
@@ -30741,8 +30804,8 @@ ${output.recovery.stderr}` : "";
|
|
|
30741
30804
|
if (this.opts.imageRefsForDigests)
|
|
30742
30805
|
return this.opts.imageRefsForDigests();
|
|
30743
30806
|
try {
|
|
30744
|
-
const composePath =
|
|
30745
|
-
if (!
|
|
30807
|
+
const composePath = join13(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
30808
|
+
if (!existsSync15(composePath))
|
|
30746
30809
|
return [];
|
|
30747
30810
|
const r = spawnSync3("docker", [
|
|
30748
30811
|
"compose",
|
|
@@ -30813,6 +30876,9 @@ ${output.recovery.stderr}` : "";
|
|
|
30813
30876
|
entry.failed_step = parsed.failedStep;
|
|
30814
30877
|
if (parsed.failedAgent)
|
|
30815
30878
|
entry.failed_agent = parsed.failedAgent;
|
|
30879
|
+
if (parsed.drifted && parsed.drifted.length > 0) {
|
|
30880
|
+
entry.drifted = parsed.drifted;
|
|
30881
|
+
}
|
|
30816
30882
|
if (parsed.got !== undefined)
|
|
30817
30883
|
entry.got = parsed.got;
|
|
30818
30884
|
if (parsed.timedOut)
|
|
@@ -30868,7 +30934,10 @@ ${pinNote}`.trim();
|
|
|
30868
30934
|
});
|
|
30869
30935
|
}
|
|
30870
30936
|
async finishFailedAutoRollout(entry, priorPin) {
|
|
30871
|
-
const
|
|
30937
|
+
const drifted = entry.drifted ?? [];
|
|
30938
|
+
const notes = entry.failed_step === "verify-components" ? [
|
|
30939
|
+
`Unattended auto-update roll to ${entry.pin ?? "?"} rolled ` + `${(entry.rolled ?? []).length} agent(s) but did NOT converge the ` + `whole host: ${drifted.join(", ") || "one or more components"} ` + `still behind. Re-running the roll will not fix this — run the ` + `per-component install named in the rollout warnings, then ` + `\`switchroom update --check\` to confirm.`
|
|
30940
|
+
] : [
|
|
30872
30941
|
`Unattended auto-update roll to ${entry.pin ?? "?"} FAILED at ` + `${entry.failed_step ?? "unknown step"}` + `${entry.failed_agent ? ` (agent ${entry.failed_agent})` : ""}. ` + `Unattended retries of this version are disabled; fix the cause, ` + `then roll manually (\`switchroom rollout --pin ${entry.pin ?? "vX.Y.Z"}\`).`
|
|
30873
30942
|
];
|
|
30874
30943
|
try {
|
|
@@ -30891,6 +30960,10 @@ ${pinNote}`.trim();
|
|
|
30891
30960
|
async recoverFailedAutoRollout(entry, priorPin) {
|
|
30892
30961
|
const notes = [];
|
|
30893
30962
|
const rolledCount = (entry.rolled ?? []).length;
|
|
30963
|
+
if (entry.failed_step === "verify-components") {
|
|
30964
|
+
notes.push(`No automatic rollback: the agent fleet reached ` + `${entry.pin ?? "the target"} and the pin is committed — the gap is ` + `${(entry.drifted ?? []).join(", ") || "a component"}, not the fleet. ` + `Rolling back would move working agents backwards; converge the ` + `named component(s) forward instead.`);
|
|
30965
|
+
return notes;
|
|
30966
|
+
}
|
|
30894
30967
|
const beforeAnyAgent = entry.failed_step === "apply" || entry.failed_step === "restart-agent" && rolledCount === 0;
|
|
30895
30968
|
if (!beforeAnyAgent) {
|
|
30896
30969
|
notes.push(rolledCount > 0 ? `No automatic rollback: ${rolledCount} agent(s) already confirmed on ` + `${entry.pin ?? "the target"} past a green canary ` + `(${(entry.rolled ?? []).join(", ")}). The fleet is mixed — ` + `finish the roll or roll back manually.` : `No automatic rollback: the roll stopped at ` + `${entry.failed_step ?? "an unknown step"} before any change ` + `landed — nothing to revert. Verify host-side if unsure.`);
|
|
@@ -30937,7 +31010,8 @@ ${pinNote}`.trim();
|
|
|
30937
31010
|
...entry.finished_at !== null ? { elapsedMs: entry.finished_at - entry.started_at } : {},
|
|
30938
31011
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
30939
31012
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
30940
|
-
...entry.got !== undefined ? { got: entry.got } : {}
|
|
31013
|
+
...entry.got !== undefined ? { got: entry.got } : {},
|
|
31014
|
+
...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {}
|
|
30941
31015
|
});
|
|
30942
31016
|
this.opts.rolloutRelay.postTerminal({
|
|
30943
31017
|
requestId: entry.request_id,
|
|
@@ -30975,7 +31049,7 @@ ${pinNote}`.trim();
|
|
|
30975
31049
|
}
|
|
30976
31050
|
rolloutRowExistsInLog(targetRequestId) {
|
|
30977
31051
|
const path3 = this.auditLogPath();
|
|
30978
|
-
if (!
|
|
31052
|
+
if (!existsSync15(path3))
|
|
30979
31053
|
return false;
|
|
30980
31054
|
try {
|
|
30981
31055
|
const raw = readFileSync11(path3, "utf-8");
|
|
@@ -30986,7 +31060,7 @@ ${pinNote}`.trim();
|
|
|
30986
31060
|
}
|
|
30987
31061
|
rolloutStatusFromLog(responseRequestId, targetRequestId, started) {
|
|
30988
31062
|
const path3 = this.auditLogPath();
|
|
30989
|
-
if (!
|
|
31063
|
+
if (!existsSync15(path3))
|
|
30990
31064
|
return null;
|
|
30991
31065
|
let raw;
|
|
30992
31066
|
try {
|
|
@@ -31006,6 +31080,7 @@ ${pinNote}`.trim();
|
|
|
31006
31080
|
...row.agent ? { agent: row.agent } : {},
|
|
31007
31081
|
...row.failed_step ? { failedStep: row.failed_step } : {},
|
|
31008
31082
|
...row.failed_agent ? { failedAgent: row.failed_agent } : {},
|
|
31083
|
+
...row.drifted && row.drifted.length > 0 ? { drifted: row.drifted } : {},
|
|
31009
31084
|
...row.pin ? { pin: row.pin } : {},
|
|
31010
31085
|
...row.prior_pin ? { prior_pin: row.prior_pin } : {}
|
|
31011
31086
|
});
|
|
@@ -31028,6 +31103,7 @@ ${pinNote}`.trim();
|
|
|
31028
31103
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
31029
31104
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
31030
31105
|
...entry.got !== undefined ? { got: entry.got } : {},
|
|
31106
|
+
...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
|
|
31031
31107
|
...entry.pin ? { pin: entry.pin } : {},
|
|
31032
31108
|
...entry.prior_pin ? { prior_pin: entry.prior_pin } : {}
|
|
31033
31109
|
}) : undefined;
|
|
@@ -31069,7 +31145,7 @@ ${pinNote}`.trim();
|
|
|
31069
31145
|
}
|
|
31070
31146
|
}
|
|
31071
31147
|
auditLogPath() {
|
|
31072
|
-
return this.opts.auditLogPath ??
|
|
31148
|
+
return this.opts.auditLogPath ?? join13(this.opts.homeDir, ".switchroom", "host-control-audit.log");
|
|
31073
31149
|
}
|
|
31074
31150
|
maybeRotateAuditLog(path3) {
|
|
31075
31151
|
const { maxBytes, maxFiles } = resolveRotationConfig({
|
|
@@ -31139,6 +31215,7 @@ ${pinNote}`.trim();
|
|
|
31139
31215
|
...entry.rolled ? { rolled: entry.rolled } : {},
|
|
31140
31216
|
...entry.failed_step ? { failed_step: entry.failed_step } : {},
|
|
31141
31217
|
...entry.failed_agent ? { failed_agent: entry.failed_agent } : {},
|
|
31218
|
+
...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
|
|
31142
31219
|
...entry.got !== undefined ? { got: entry.got } : {},
|
|
31143
31220
|
...entry.deferred ? { deferred: entry.deferred } : {},
|
|
31144
31221
|
...entry.prior_pin ? { prior_pin: entry.prior_pin } : {}
|
|
@@ -31856,6 +31933,7 @@ class LogTailRolloutNarrator {
|
|
|
31856
31933
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
31857
31934
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
31858
31935
|
...entry.got !== undefined ? { got: entry.got } : {},
|
|
31936
|
+
...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
|
|
31859
31937
|
...st.agents.length > 0 ? { agents: st.agents } : {},
|
|
31860
31938
|
...st.render.m !== undefined ? { m: st.render.m } : {},
|
|
31861
31939
|
requestId: entry.request_id,
|
|
@@ -32495,7 +32573,7 @@ import { appendFile as appendFile2 } from "node:fs/promises";
|
|
|
32495
32573
|
async function loadConfigResilient() {
|
|
32496
32574
|
try {
|
|
32497
32575
|
const config = loadConfig();
|
|
32498
|
-
clearStaleDegradedMarker(
|
|
32576
|
+
clearStaleDegradedMarker(homedir8(), (m) => process.stderr.write(`hostd: ${m}
|
|
32499
32577
|
`));
|
|
32500
32578
|
return config;
|
|
32501
32579
|
} catch (err2) {
|
|
@@ -32507,13 +32585,13 @@ async function loadConfigResilient() {
|
|
|
32507
32585
|
} catch {
|
|
32508
32586
|
configPath = undefined;
|
|
32509
32587
|
}
|
|
32510
|
-
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ??
|
|
32588
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join14(homedir8(), ".switchroom", "agents");
|
|
32511
32589
|
const listGatewayCandidates = () => {
|
|
32512
32590
|
const out = [];
|
|
32513
32591
|
try {
|
|
32514
|
-
for (const name of
|
|
32592
|
+
for (const name of readdirSync7(agentsDir).sort()) {
|
|
32515
32593
|
const sock = resolve10(agentsDir, name, "telegram", "gateway.sock");
|
|
32516
|
-
if (
|
|
32594
|
+
if (existsSync16(sock))
|
|
32517
32595
|
out.push({ agent: name, sock });
|
|
32518
32596
|
}
|
|
32519
32597
|
} catch {}
|
|
@@ -32536,7 +32614,7 @@ async function loadConfigResilient() {
|
|
|
32536
32614
|
};
|
|
32537
32615
|
return waitForConfigRecovery({
|
|
32538
32616
|
initialError: err2,
|
|
32539
|
-
homeDir:
|
|
32617
|
+
homeDir: homedir8(),
|
|
32540
32618
|
loadFn: () => loadConfig(),
|
|
32541
32619
|
configPath,
|
|
32542
32620
|
notify,
|
|
@@ -32560,10 +32638,10 @@ async function main() {
|
|
|
32560
32638
|
`);
|
|
32561
32639
|
process.exit(2);
|
|
32562
32640
|
}
|
|
32563
|
-
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ??
|
|
32641
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join14(homedir8(), ".switchroom", "agents");
|
|
32564
32642
|
const resolveGatewaySocket = (agentName) => {
|
|
32565
32643
|
const sock = resolve10(agentsDir, agentName, "telegram", "gateway.sock");
|
|
32566
|
-
return
|
|
32644
|
+
return existsSync16(sock) ? sock : null;
|
|
32567
32645
|
};
|
|
32568
32646
|
const approvalGateway = new SocketApprovalGateway({
|
|
32569
32647
|
resolveGatewaySocket,
|
|
@@ -32582,7 +32660,7 @@ async function main() {
|
|
|
32582
32660
|
}), { log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
|
|
32583
32661
|
`) });
|
|
32584
32662
|
const server = new HostdServer({
|
|
32585
|
-
homeDir:
|
|
32663
|
+
homeDir: homedir8(),
|
|
32586
32664
|
agentUids,
|
|
32587
32665
|
config: {
|
|
32588
32666
|
agents: Object.fromEntries(Object.entries(config.agents).map(([n, a]) => [
|
|
@@ -32611,7 +32689,7 @@ async function main() {
|
|
|
32611
32689
|
const autoRel = config.host_control?.auto_release_check;
|
|
32612
32690
|
const watcherMode = resolveReleaseWatcherMode(config);
|
|
32613
32691
|
if (watcherMode.mode !== "off") {
|
|
32614
|
-
const eventsLog =
|
|
32692
|
+
const eventsLog = join14(homedir8(), ".switchroom", "release-watcher-events.jsonl");
|
|
32615
32693
|
const log = (m) => {
|
|
32616
32694
|
process.stderr.write(`hostd: ${m}
|
|
32617
32695
|
`);
|
|
@@ -32670,7 +32748,7 @@ async function main() {
|
|
|
32670
32748
|
`);
|
|
32671
32749
|
} else
|
|
32672
32750
|
notifier = new UpdateNotifier({
|
|
32673
|
-
statePath:
|
|
32751
|
+
statePath: join14(homedir8(), ".switchroom", "release-notify-state.json"),
|
|
32674
32752
|
repostSuppressMs: cardTimeoutMs,
|
|
32675
32753
|
isFleetMutationInFlight: () => server.isFleetMutationLocked(),
|
|
32676
32754
|
startApply: (requestId) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.19.
|
|
4
|
+
"version": "0.19.32",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -127,7 +127,11 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
127
127
|
sr_ll_ok=""
|
|
128
128
|
sr_ll_unreachable=""
|
|
129
129
|
if [ -z "$sr_ll_key" ]; then
|
|
130
|
-
|
|
130
|
+
# Remediation, not just diagnosis. Unlike start.sh this block re-runs on
|
|
131
|
+
# EVERY fire, so no restart is needed — a provisioned key is picked up by
|
|
132
|
+
# the next fire on its own. Say that, so the operator does not bounce the
|
|
133
|
+
# agent chasing a cron-only problem.
|
|
134
|
+
echo "litellm(cron): no virtual key for '{{name}}' — falling back to direct OAuth (untracked, unguarded). FIX: re-run 'switchroom apply' to provision the key; the next fire picks it up (no restart needed)." >&2
|
|
131
135
|
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ]; then
|
|
132
136
|
# Bounded retry probe — short budget for cron latency (3 attempts,
|
|
133
137
|
# worst-case ~25s: 3×5s curl timeouts + 2×5s sleeps).
|
|
@@ -108,7 +108,13 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
108
108
|
sr_ll_ok=""
|
|
109
109
|
sr_ll_unreachable=""
|
|
110
110
|
if [ -z "$sr_ll_key" ]; then
|
|
111
|
-
|
|
111
|
+
# Say what to DO, not just what happened: the fail-open is a BOOT-TIME
|
|
112
|
+
# decision that is never re-evaluated, so provisioning the key later
|
|
113
|
+
# changes nothing until the agent is restarted. An operator who reads
|
|
114
|
+
# only "untracked" and provisions a key reasonably assumes it took
|
|
115
|
+
# effect; it did not. `switchroom doctor` carries the standing version
|
|
116
|
+
# of this signal (it reads .routing-mode) once the boot log has rotated.
|
|
117
|
+
echo "litellm(outer): WARNING — no virtual key for agent '$SWITCHROOM_AGENT_NAME'; gateway falling back to direct Anthropic OAuth (untracked, unguarded). FIX: re-run 'switchroom apply' to provision the key, then RESTART this agent — the fail-open is decided once at boot and is never re-evaluated for the life of the session." >&2
|
|
112
118
|
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ]; then
|
|
113
119
|
# Bounded retry probe (co-boot race fix, 2026-07): when the whole stack
|
|
114
120
|
# co-boots, the litellm proxy's heavy Python app is often not yet healthy
|
|
@@ -1563,6 +1569,14 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1563
1569
|
echo "WARNING: no litellm virtual key for agent '$SWITCHROOM_AGENT_NAME'." >&2
|
|
1564
1570
|
echo "WARNING: falling back to direct Anthropic OAuth (untracked, unguarded)" >&2
|
|
1565
1571
|
echo "WARNING: — cannot authenticate to the proxy without a key." >&2
|
|
1572
|
+
# Remediation, not just diagnosis. The fail-open is decided ONCE here and
|
|
1573
|
+
# never re-evaluated, so a key provisioned mid-session is inert — an
|
|
1574
|
+
# operator who is not told that will provision one and believe the agent
|
|
1575
|
+
# healed. `switchroom doctor` reads .routing-mode for the standing version
|
|
1576
|
+
# of this signal, since this banner only lives in a rotating boot log.
|
|
1577
|
+
echo "WARNING: FIX: re-run 'switchroom apply' to provision the key, then" >&2
|
|
1578
|
+
echo "WARNING: RESTART this agent — the fail-open is decided once at boot" >&2
|
|
1579
|
+
echo "WARNING: and is never re-evaluated for the life of this session." >&2
|
|
1566
1580
|
echo "==========================================================" >&2
|
|
1567
1581
|
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ]; then
|
|
1568
1582
|
# Bounded retry probe (co-boot race fix, 2026-07): when the whole stack
|