switchroom 0.19.29 → 0.19.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/switchroom.js +2044 -900
  2. package/dist/host-control/main.js +162 -80
  3. package/package.json +1 -1
  4. package/profiles/_base/cron-session.sh.hbs +5 -1
  5. package/profiles/_base/start.sh.hbs +15 -1
  6. package/telegram-plugin/dist/gateway/gateway.js +1435 -551
  7. package/telegram-plugin/edit-flood-fuse.ts +70 -20
  8. package/telegram-plugin/gateway/boot-beacon.ts +364 -0
  9. package/telegram-plugin/gateway/boot-sweep-gate.ts +20 -15
  10. package/telegram-plugin/gateway/gateway.ts +87 -88
  11. package/telegram-plugin/gateway/inbound-spool.ts +39 -0
  12. package/telegram-plugin/gateway/narrative-lane.ts +12 -0
  13. package/telegram-plugin/gateway/obligation-store.ts +28 -0
  14. package/telegram-plugin/gateway/stale-pin-sweep-store.ts +221 -0
  15. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +211 -0
  16. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +804 -0
  17. package/telegram-plugin/gateway/stale-pin-sweep.ts +1146 -0
  18. package/telegram-plugin/gateway/status-pin-retarget.ts +15 -2
  19. package/telegram-plugin/gateway/status-pin-store.ts +33 -11
  20. package/telegram-plugin/registry/turns-schema.ts +21 -1
  21. package/telegram-plugin/retry-api-call.ts +46 -21
  22. package/telegram-plugin/shared/bot-runtime.ts +61 -17
  23. package/telegram-plugin/shared/gw-trace-gate.ts +18 -2
  24. package/telegram-plugin/tests/activity-card-wiring.test.ts +7 -7
  25. package/telegram-plugin/tests/activity-drain-fuse-drop-not-failure.test.ts +324 -0
  26. package/telegram-plugin/tests/agent-card-result-footer.test.ts +193 -0
  27. package/telegram-plugin/tests/boot-beacon.test.ts +462 -0
  28. package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +6 -6
  29. package/telegram-plugin/tests/boot-sweep-gate.test.ts +42 -31
  30. package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +2 -0
  31. package/telegram-plugin/tests/inbound-spool-progress.test.ts +2 -0
  32. package/telegram-plugin/tests/inbound-spool.test.ts +134 -5
  33. package/telegram-plugin/tests/narrative-lane-golden.test.ts +28 -0
  34. package/telegram-plugin/tests/obligation-determinism.test.ts +2 -0
  35. package/telegram-plugin/tests/obligation-store.test.ts +67 -1
  36. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +3 -3
  37. package/telegram-plugin/tests/status-pin-store.test.ts +26 -5
  38. package/telegram-plugin/tests/tg-post-logger-error-shape.test.ts +161 -0
  39. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +30 -0
  40. package/telegram-plugin/tool-activity-summary.ts +104 -38
  41. package/telegram-plugin/worker-activity-feed.ts +33 -16
  42. package/telegram-plugin/gateway/dm-pin-sweep.test.ts +0 -251
  43. 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 homedir7 } from "node:os";
19064
- import { existsSync as existsSync15, readdirSync as readdirSync6 } from "node:fs";
19065
- import { join as join13, resolve as resolve10 } from "node:path";
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();
@@ -20845,16 +20845,18 @@ function needsSelfBump(ownVersion, pin) {
20845
20845
  return cmp !== null && cmp < 0;
20846
20846
  }
20847
20847
  function bumpHostdComposeImageTag(yaml, pin) {
20848
- const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
20849
- const m = yaml.match(re);
20850
- if (!m)
20848
+ const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)[ \t]*$/gm;
20849
+ const matches = [...yaml.matchAll(re)];
20850
+ if (matches.length === 0)
20851
20851
  return null;
20852
- const oldImageRef = `${m[2]}:${m[3]}`;
20853
- const newImageRef = `${m[2]}:${pin}`;
20852
+ const first = matches[0];
20853
+ const oldImageRef = `${first[2]}:${first[3]}`;
20854
+ const newImageRef = `${first[2]}:${pin}`;
20854
20855
  return {
20855
- yaml: yaml.replace(re, `$1${newImageRef}`),
20856
+ yaml: yaml.replace(re, (_m, indent, repo) => `${indent}${repo}:${pin}`),
20856
20857
  oldImageRef,
20857
- newImageRef
20858
+ newImageRef,
20859
+ bumpedCount: matches.length
20858
20860
  };
20859
20861
  }
20860
20862
  function encodePendingRolloutMarker(m) {
@@ -21208,7 +21210,7 @@ function allocateAgentUid(name) {
21208
21210
  }
21209
21211
 
21210
21212
  // src/build-info.ts
21211
- var VERSION = "0.19.29";
21213
+ var VERSION = "0.19.31";
21212
21214
 
21213
21215
  // src/setup/hindsight-recall-tunables.ts
21214
21216
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -21765,8 +21767,8 @@ import { createServer } from "node:net";
21765
21767
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
21766
21768
  import { mkdir as mkdir2, chmod, chown, unlink, appendFile } from "node:fs/promises";
21767
21769
  import {
21768
- readdirSync as readdirSync5,
21769
- existsSync as existsSync14,
21770
+ readdirSync as readdirSync6,
21771
+ existsSync as existsSync15,
21770
21772
  readFileSync as readFileSync11,
21771
21773
  writeFileSync as writeFileSync6,
21772
21774
  renameSync as renameSync5,
@@ -21779,7 +21781,7 @@ import {
21779
21781
  copyFileSync as copyFileSync3,
21780
21782
  unlinkSync as unlinkSync5
21781
21783
  } from "node:fs";
21782
- import { join as join12, dirname as dirname9, resolve as resolve9 } from "node:path";
21784
+ import { join as join13, dirname as dirname9, resolve as resolve9 } from "node:path";
21783
21785
  import { createHash as createHash6, randomUUID as randomUUID2, randomBytes } from "node:crypto";
21784
21786
 
21785
21787
  // src/host-control/protocol.ts
@@ -27737,7 +27739,51 @@ function recoverPinJournal(configPath, opts = {}) {
27737
27739
  }
27738
27740
 
27739
27741
  // src/cli/operator-uid.ts
27740
- function resolveOperatorUid() {
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 = {}) {
27741
27787
  const sudoUid = process.env.SUDO_UID;
27742
27788
  if (sudoUid !== undefined) {
27743
27789
  const parsed = parseInt(sudoUid, 10);
@@ -27755,20 +27801,20 @@ function resolveOperatorUid() {
27755
27801
  if (Number.isFinite(parsed) && parsed > 0)
27756
27802
  return parsed;
27757
27803
  }
27758
- return;
27804
+ return resolveOperatorUidFromOwnership(ownershipDeps);
27759
27805
  }
27760
27806
 
27761
27807
  // src/cli/rollout.ts
27762
27808
  init_atomic();
27763
27809
 
27764
27810
  // src/cli/resolve-version.ts
27765
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
27766
- import { dirname as dirname7, join as join9 } from "node:path";
27811
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "node:fs";
27812
+ import { dirname as dirname7, join as join10 } from "node:path";
27767
27813
  function readPackageVersion() {
27768
27814
  let dir = import.meta.dirname;
27769
27815
  for (let i = 0;i < 12 && dir && dir !== "/"; i++) {
27770
- const p = join9(dir, "package.json");
27771
- if (existsSync12(p)) {
27816
+ const p = join10(dir, "package.json");
27817
+ if (existsSync13(p)) {
27772
27818
  try {
27773
27819
  const pkg = JSON.parse(readFileSync9(p, "utf-8"));
27774
27820
  if (pkg?.name === "switchroom" && typeof pkg.version === "string") {
@@ -27867,6 +27913,9 @@ function parseAuditLine(line) {
27867
27913
  entry.failed_step = o.failed_step;
27868
27914
  if (typeof o.failed_agent === "string")
27869
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
+ }
27870
27919
  if (typeof o.agent === "string")
27871
27920
  entry.agent = o.agent;
27872
27921
  if (typeof o.n === "number")
@@ -27898,6 +27947,7 @@ function latestRolloutRowForRequest(raw, requestId) {
27898
27947
  var DOCKER_INSPECT_TIMEOUT_MS = 60 * 1000;
27899
27948
 
27900
27949
  // src/cli/rollout.ts
27950
+ var VERIFY_COMPONENTS_STEP = "verify-components";
27901
27951
  function isVersionAssertable(target) {
27902
27952
  return /^v?\d+\.\d+\.\d+$/.test(target.trim());
27903
27953
  }
@@ -27971,7 +28021,7 @@ var ROLLOUT_RUN_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_RUN_TIMEOUT_MS", 20 * 60
27971
28021
  var ROLLOUT_PROBE_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_PROBE_TIMEOUT_MS", 60 * 1000);
27972
28022
 
27973
28023
  // src/cli/write-compose.ts
27974
- import { chownSync } from "node:fs";
28024
+ import { chownSync as chownSync2 } from "node:fs";
27975
28025
  import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
27976
28026
  var AGENT_IMAGE_TAG_RE = /image:\s*\S*switchroom-agent:(\S+)/;
27977
28027
  var BACKUP_SUFFIX = ".bak";
@@ -28060,7 +28110,7 @@ async function restoreComposeBackup(composePath, opts = {}) {
28060
28110
  const operatorUid = resolveOperatorUid();
28061
28111
  if (operatorUid !== undefined && process.geteuid?.() === 0) {
28062
28112
  try {
28063
- chownSync(composePath, operatorUid, operatorUid);
28113
+ chownSync2(composePath, operatorUid, operatorUid);
28064
28114
  } catch {}
28065
28115
  }
28066
28116
  return { restored: true, imageTag: composeImageTag(content) };
@@ -28070,8 +28120,8 @@ async function restoreComposeBackup(composePath, opts = {}) {
28070
28120
  var import__2 = __toESM(require_commander(), 1);
28071
28121
 
28072
28122
  // src/cli/update.ts
28073
- import { join as join10, dirname as dirname8, resolve as resolve8 } from "node:path";
28074
- import { homedir as homedir6 } from "node:os";
28123
+ import { join as join11, dirname as dirname8, resolve as resolve8 } from "node:path";
28124
+ import { homedir as homedir7 } from "node:os";
28075
28125
 
28076
28126
  // src/agents/lifecycle.ts
28077
28127
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -28167,7 +28217,7 @@ function getAllAgentStatuses(config) {
28167
28217
 
28168
28218
  // src/cli/update.ts
28169
28219
  init_atomic();
28170
- var DEFAULT_COMPOSE_PATH = join10(homedir6(), ".switchroom", "compose", "docker-compose.yml");
28220
+ var DEFAULT_COMPOSE_PATH = join11(homedir7(), ".switchroom", "compose", "docker-compose.yml");
28171
28221
  var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
28172
28222
  function parseUpdateResultLine(stdout) {
28173
28223
  const idx = stdout.lastIndexOf(UPDATE_RESULT_SENTINEL);
@@ -28190,9 +28240,9 @@ function parseUpdateResultLine(stdout) {
28190
28240
 
28191
28241
  // src/host-control/config-edit-validator.ts
28192
28242
  var import_yaml5 = __toESM(require_dist(), 1);
28193
- import { mkdtempSync, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
28243
+ import { mkdtempSync, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
28194
28244
  import { tmpdir } from "node:os";
28195
- import { join as join11, isAbsolute as isAbsolute3, normalize, basename as basename4 } from "node:path";
28245
+ import { join as join12, isAbsolute as isAbsolute3, normalize, basename as basename4 } from "node:path";
28196
28246
  import { spawnSync as spawnSync2 } from "node:child_process";
28197
28247
  import { isDeepStrictEqual } from "node:util";
28198
28248
  var MAX_PATCH_BYTES = 1024 * 1024;
@@ -28332,7 +28382,7 @@ function validateShape(unifiedDiff, targetPath) {
28332
28382
  return null;
28333
28383
  }
28334
28384
  function applyPatch(unifiedDiff, configPath, gitBin) {
28335
- if (!existsSync13(configPath)) {
28385
+ if (!existsSync14(configPath)) {
28336
28386
  return {
28337
28387
  ok: false,
28338
28388
  code: "E_PATCH_APPLY_FAILED",
@@ -28340,13 +28390,13 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
28340
28390
  };
28341
28391
  }
28342
28392
  const liveContent = readFileSync10(configPath, "utf8");
28343
- const scratchDir = mkdtempSync(join11(tmpdir(), "config-propose-edit-"));
28393
+ const scratchDir = mkdtempSync(join12(tmpdir(), "config-propose-edit-"));
28344
28394
  try {
28345
28395
  const targetBasename = configPath.split("/").pop() ?? "switchroom.yaml";
28346
- const scratchFile = join11(scratchDir, targetBasename);
28396
+ const scratchFile = join12(scratchDir, targetBasename);
28347
28397
  writeFileSync5(scratchFile, liveContent);
28348
28398
  const normalizedDiff = normalizeDiffHeadersToBasename(unifiedDiff, targetBasename);
28349
- const patchFile = join11(scratchDir, "proposal.patch");
28399
+ const patchFile = join12(scratchDir, "proposal.patch");
28350
28400
  writeFileSync5(patchFile, normalizedDiff);
28351
28401
  const bin = gitBin ?? "git";
28352
28402
  const baseArgs = [
@@ -28574,7 +28624,7 @@ function validateConfigEdit(opts) {
28574
28624
  return schemaErr;
28575
28625
  let beforeData = {};
28576
28626
  try {
28577
- const beforeRaw = existsSync13(opts.configPath) ? readFileSync10(opts.configPath, "utf8") : "";
28627
+ const beforeRaw = existsSync14(opts.configPath) ? readFileSync10(opts.configPath, "utf8") : "";
28578
28628
  const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
28579
28629
  beforeData = beforeDoc.toJS();
28580
28630
  } catch {
@@ -28915,8 +28965,8 @@ function etaLine(s) {
28915
28965
  function deferredLines(target) {
28916
28966
  const bare = target.trim().replace(/^v/, "");
28917
28967
  return [
28918
- `**Deferred (still on the prior version — run host-side):**`,
28919
- `- \`switchroom-web\` — \`switchroom webd install --tag ${target}\``,
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):**`,
28920
28970
  `- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
28921
28971
  `- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
28922
28972
  ];
@@ -28946,6 +28996,21 @@ function renderWith(s, compact) {
28946
28996
  if (s.deferred !== false)
28947
28997
  parts2.push("", ...deferredLines(s.target));
28948
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(`
28949
29014
  `);
28950
29015
  }
28951
29016
  if (s.terminal === "error") {
@@ -29076,9 +29141,9 @@ function resolveDigests(imageRefs) {
29076
29141
  return out;
29077
29142
  }
29078
29143
  function readCachedInstallType(bindRoot) {
29079
- const cacheDir = join12(bindRoot, ".switchroom");
29080
- const cachePath = join12(cacheDir, "install-type.json");
29081
- if (existsSync14(cachePath)) {
29144
+ const cacheDir = join13(bindRoot, ".switchroom");
29145
+ const cachePath = join13(cacheDir, "install-type.json");
29146
+ if (existsSync15(cachePath)) {
29082
29147
  try {
29083
29148
  const raw = readFileSync11(cachePath, "utf-8");
29084
29149
  const parsed = JSON.parse(raw);
@@ -29167,7 +29232,7 @@ class HostdServer {
29167
29232
  fleetMutationInFlight = null;
29168
29233
  lastAutoRolloutFailedPin = null;
29169
29234
  autoRolloutLatchPath() {
29170
- return join12(this.hostdDirPath(), AUTO_ROLLOUT_LATCH_FILENAME);
29235
+ return join13(this.hostdDirPath(), AUTO_ROLLOUT_LATCH_FILENAME);
29171
29236
  }
29172
29237
  readAutoRolloutLatch() {
29173
29238
  try {
@@ -29210,7 +29275,7 @@ class HostdServer {
29210
29275
  return resolveHostdConfigPath(this.opts.configPath);
29211
29276
  }
29212
29277
  fleetComposePath() {
29213
- return join12(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
29278
+ return join13(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
29214
29279
  }
29215
29280
  recoverRolloutPinJournal(context) {
29216
29281
  let note = null;
@@ -29249,7 +29314,7 @@ class HostdServer {
29249
29314
  return note;
29250
29315
  }
29251
29316
  async start() {
29252
- const hostdDir = join12(this.opts.homeDir, ".switchroom", "hostd");
29317
+ const hostdDir = join13(this.opts.homeDir, ".switchroom", "hostd");
29253
29318
  await mkdir2(hostdDir, { recursive: true });
29254
29319
  await chmod(hostdDir, 493).catch(() => {
29255
29320
  return;
@@ -29270,13 +29335,13 @@ class HostdServer {
29270
29335
  }
29271
29336
  try {
29272
29337
  for (const name of agentNames) {
29273
- const dir = join12(hostdDir, name);
29274
- const sockPath = join12(dir, "sock");
29338
+ const dir = join13(hostdDir, name);
29339
+ const sockPath = join13(dir, "sock");
29275
29340
  await mkdir2(dir, { recursive: true });
29276
29341
  await chmod(dir, 493).catch(() => {
29277
29342
  return;
29278
29343
  });
29279
- if (existsSync14(sockPath))
29344
+ if (existsSync15(sockPath))
29280
29345
  await unlink(sockPath).catch(() => {
29281
29346
  return;
29282
29347
  });
@@ -29307,13 +29372,13 @@ class HostdServer {
29307
29372
  process.stderr.write(`hostd: SWITCHROOM_HOSTD_OPERATOR_UID='${opUidStr}' is not a positive integer; skipping operator listener
29308
29373
  `);
29309
29374
  } else {
29310
- const dir = join12(hostdDir, "operator");
29311
- const sockPath = join12(dir, "sock");
29375
+ const dir = join13(hostdDir, "operator");
29376
+ const sockPath = join13(dir, "sock");
29312
29377
  await mkdir2(dir, { recursive: true });
29313
29378
  await chmod(dir, 493).catch(() => {
29314
29379
  return;
29315
29380
  });
29316
- if (existsSync14(sockPath))
29381
+ if (existsSync15(sockPath))
29317
29382
  await unlink(sockPath).catch(() => {
29318
29383
  return;
29319
29384
  });
@@ -29349,7 +29414,7 @@ class HostdServer {
29349
29414
  }
29350
29415
  async reconcileOrphanedFleetMutations() {
29351
29416
  const path3 = this.auditLogPath();
29352
- if (!existsSync14(path3))
29417
+ if (!existsSync15(path3))
29353
29418
  return;
29354
29419
  let raw;
29355
29420
  try {
@@ -29752,11 +29817,11 @@ class HostdServer {
29752
29817
  missingApplyAssets() {
29753
29818
  const root = this.opts.applyAssetsRoot ?? resolve9(import.meta.dirname, "../..");
29754
29819
  return [
29755
- join12(root, "profiles"),
29756
- join12(root, "profiles", "default"),
29757
- join12(root, "vendor", "hindsight-memory"),
29758
- join12(root, "skills")
29759
- ].filter((p) => !existsSync14(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));
29760
29825
  }
29761
29826
  applyAssetPreflight(request_id, started) {
29762
29827
  const missing = this.missingApplyAssets();
@@ -30019,16 +30084,16 @@ class HostdServer {
30019
30084
  return entry;
30020
30085
  }
30021
30086
  hostdDirPath() {
30022
- return join12(this.opts.homeDir, ".switchroom", "hostd");
30087
+ return join13(this.opts.homeDir, ".switchroom", "hostd");
30023
30088
  }
30024
30089
  hostdDirHostPath() {
30025
30090
  const hostHome = this.opts.hostHomeDir ?? process.env.SWITCHROOM_HOST_HOME?.trim() ?? this.opts.homeDir;
30026
- return join12(hostHome, ".switchroom", "hostd");
30091
+ return join13(hostHome, ".switchroom", "hostd");
30027
30092
  }
30028
30093
  async beginSelfBump(req, caller, started) {
30029
30094
  const ownVersion = this.opts.selfVersion ?? SWITCHROOM_VERSION;
30030
- const composePath = join12(this.hostdDirPath(), "docker-compose.yml");
30031
- if (!existsSync14(composePath)) {
30095
+ const composePath = join13(this.hostdDirPath(), "docker-compose.yml");
30096
+ if (!existsSync15(composePath)) {
30032
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);
30033
30098
  }
30034
30099
  const before = readFileSync11(composePath, "utf8");
@@ -30050,12 +30115,12 @@ class HostdServer {
30050
30115
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
30051
30116
  const bakPath = `${composePath}.bak-selfbump-${ts}`;
30052
30117
  try {
30053
- const baks = readdirSync5(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
30118
+ const baks = readdirSync6(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
30054
30119
  for (const f of baks.slice(0, Math.max(0, baks.length - 4))) {
30055
- unlinkSync5(join12(this.hostdDirPath(), f));
30120
+ unlinkSync5(join13(this.hostdDirPath(), f));
30056
30121
  }
30057
30122
  } catch {}
30058
- const markerPath = join12(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30123
+ const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30059
30124
  const marker = {
30060
30125
  v: 1,
30061
30126
  request_id: req.request_id,
@@ -30069,6 +30134,8 @@ class HostdServer {
30069
30134
  };
30070
30135
  copyFileSync3(composePath, bakPath);
30071
30136
  writeFileSync6(composePath, bumped.yaml, "utf8");
30137
+ process.stderr.write(`hostd: self-bump rewrote ${bumped.bumpedCount} switchroom-hostd ` + `image line(s) → ${bumped.newImageRef}
30138
+ `);
30072
30139
  writeFileSync6(markerPath, encodePendingRolloutMarker(marker), {
30073
30140
  mode: 384
30074
30141
  });
@@ -30137,7 +30204,7 @@ class HostdServer {
30137
30204
  rollbackBump();
30138
30205
  entry.result = "error";
30139
30206
  entry.finished_at = Date.now();
30140
- entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${join12(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.`;
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.`;
30141
30208
  if (this.fleetMutationInFlight && this.fleetMutationInFlight.request_id === entry.request_id) {
30142
30209
  this.fleetMutationInFlight = null;
30143
30210
  }
@@ -30161,8 +30228,8 @@ class HostdServer {
30161
30228
  };
30162
30229
  }
30163
30230
  async resumePendingSelfBumpRollout() {
30164
- const markerPath = join12(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30165
- if (!existsSync14(markerPath))
30231
+ const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30232
+ if (!existsSync15(markerPath))
30166
30233
  return;
30167
30234
  let raw;
30168
30235
  try {
@@ -30217,7 +30284,7 @@ class HostdServer {
30217
30284
  return;
30218
30285
  }
30219
30286
  if (needsSelfBump(ownVersion, marker.pin)) {
30220
- await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${join12(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)}). ` + `Fallback: \`switchroom hostd install --tag ${marker.pin}\` ` + `host-side, then re-issue the rollout.`);
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.`);
30221
30288
  return;
30222
30289
  }
30223
30290
  process.stderr.write(`hostd: resuming rollout ${marker.request_id} → ${marker.pin} after ` + `self-bump (v${marker.prior_hostd_version} → v${ownVersion})
@@ -30737,8 +30804,8 @@ ${output.recovery.stderr}` : "";
30737
30804
  if (this.opts.imageRefsForDigests)
30738
30805
  return this.opts.imageRefsForDigests();
30739
30806
  try {
30740
- const composePath = join12(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
30741
- if (!existsSync14(composePath))
30807
+ const composePath = join13(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
30808
+ if (!existsSync15(composePath))
30742
30809
  return [];
30743
30810
  const r = spawnSync3("docker", [
30744
30811
  "compose",
@@ -30809,6 +30876,9 @@ ${output.recovery.stderr}` : "";
30809
30876
  entry.failed_step = parsed.failedStep;
30810
30877
  if (parsed.failedAgent)
30811
30878
  entry.failed_agent = parsed.failedAgent;
30879
+ if (parsed.drifted && parsed.drifted.length > 0) {
30880
+ entry.drifted = parsed.drifted;
30881
+ }
30812
30882
  if (parsed.got !== undefined)
30813
30883
  entry.got = parsed.got;
30814
30884
  if (parsed.timedOut)
@@ -30864,7 +30934,10 @@ ${pinNote}`.trim();
30864
30934
  });
30865
30935
  }
30866
30936
  async finishFailedAutoRollout(entry, priorPin) {
30867
- const notes = [
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
+ ] : [
30868
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"}\`).`
30869
30942
  ];
30870
30943
  try {
@@ -30887,6 +30960,10 @@ ${pinNote}`.trim();
30887
30960
  async recoverFailedAutoRollout(entry, priorPin) {
30888
30961
  const notes = [];
30889
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
+ }
30890
30967
  const beforeAnyAgent = entry.failed_step === "apply" || entry.failed_step === "restart-agent" && rolledCount === 0;
30891
30968
  if (!beforeAnyAgent) {
30892
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.`);
@@ -30933,7 +31010,8 @@ ${pinNote}`.trim();
30933
31010
  ...entry.finished_at !== null ? { elapsedMs: entry.finished_at - entry.started_at } : {},
30934
31011
  ...entry.failed_step ? { failedStep: entry.failed_step } : {},
30935
31012
  ...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
30936
- ...entry.got !== undefined ? { got: entry.got } : {}
31013
+ ...entry.got !== undefined ? { got: entry.got } : {},
31014
+ ...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {}
30937
31015
  });
30938
31016
  this.opts.rolloutRelay.postTerminal({
30939
31017
  requestId: entry.request_id,
@@ -30971,7 +31049,7 @@ ${pinNote}`.trim();
30971
31049
  }
30972
31050
  rolloutRowExistsInLog(targetRequestId) {
30973
31051
  const path3 = this.auditLogPath();
30974
- if (!existsSync14(path3))
31052
+ if (!existsSync15(path3))
30975
31053
  return false;
30976
31054
  try {
30977
31055
  const raw = readFileSync11(path3, "utf-8");
@@ -30982,7 +31060,7 @@ ${pinNote}`.trim();
30982
31060
  }
30983
31061
  rolloutStatusFromLog(responseRequestId, targetRequestId, started) {
30984
31062
  const path3 = this.auditLogPath();
30985
- if (!existsSync14(path3))
31063
+ if (!existsSync15(path3))
30986
31064
  return null;
30987
31065
  let raw;
30988
31066
  try {
@@ -31002,6 +31080,7 @@ ${pinNote}`.trim();
31002
31080
  ...row.agent ? { agent: row.agent } : {},
31003
31081
  ...row.failed_step ? { failedStep: row.failed_step } : {},
31004
31082
  ...row.failed_agent ? { failedAgent: row.failed_agent } : {},
31083
+ ...row.drifted && row.drifted.length > 0 ? { drifted: row.drifted } : {},
31005
31084
  ...row.pin ? { pin: row.pin } : {},
31006
31085
  ...row.prior_pin ? { prior_pin: row.prior_pin } : {}
31007
31086
  });
@@ -31024,6 +31103,7 @@ ${pinNote}`.trim();
31024
31103
  ...entry.failed_step ? { failedStep: entry.failed_step } : {},
31025
31104
  ...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
31026
31105
  ...entry.got !== undefined ? { got: entry.got } : {},
31106
+ ...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
31027
31107
  ...entry.pin ? { pin: entry.pin } : {},
31028
31108
  ...entry.prior_pin ? { prior_pin: entry.prior_pin } : {}
31029
31109
  }) : undefined;
@@ -31065,7 +31145,7 @@ ${pinNote}`.trim();
31065
31145
  }
31066
31146
  }
31067
31147
  auditLogPath() {
31068
- return this.opts.auditLogPath ?? join12(this.opts.homeDir, ".switchroom", "host-control-audit.log");
31148
+ return this.opts.auditLogPath ?? join13(this.opts.homeDir, ".switchroom", "host-control-audit.log");
31069
31149
  }
31070
31150
  maybeRotateAuditLog(path3) {
31071
31151
  const { maxBytes, maxFiles } = resolveRotationConfig({
@@ -31135,6 +31215,7 @@ ${pinNote}`.trim();
31135
31215
  ...entry.rolled ? { rolled: entry.rolled } : {},
31136
31216
  ...entry.failed_step ? { failed_step: entry.failed_step } : {},
31137
31217
  ...entry.failed_agent ? { failed_agent: entry.failed_agent } : {},
31218
+ ...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
31138
31219
  ...entry.got !== undefined ? { got: entry.got } : {},
31139
31220
  ...entry.deferred ? { deferred: entry.deferred } : {},
31140
31221
  ...entry.prior_pin ? { prior_pin: entry.prior_pin } : {}
@@ -31852,6 +31933,7 @@ class LogTailRolloutNarrator {
31852
31933
  ...entry.failed_step ? { failedStep: entry.failed_step } : {},
31853
31934
  ...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
31854
31935
  ...entry.got !== undefined ? { got: entry.got } : {},
31936
+ ...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
31855
31937
  ...st.agents.length > 0 ? { agents: st.agents } : {},
31856
31938
  ...st.render.m !== undefined ? { m: st.render.m } : {},
31857
31939
  requestId: entry.request_id,
@@ -32491,7 +32573,7 @@ import { appendFile as appendFile2 } from "node:fs/promises";
32491
32573
  async function loadConfigResilient() {
32492
32574
  try {
32493
32575
  const config = loadConfig();
32494
- clearStaleDegradedMarker(homedir7(), (m) => process.stderr.write(`hostd: ${m}
32576
+ clearStaleDegradedMarker(homedir8(), (m) => process.stderr.write(`hostd: ${m}
32495
32577
  `));
32496
32578
  return config;
32497
32579
  } catch (err2) {
@@ -32503,13 +32585,13 @@ async function loadConfigResilient() {
32503
32585
  } catch {
32504
32586
  configPath = undefined;
32505
32587
  }
32506
- const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join13(homedir7(), ".switchroom", "agents");
32588
+ const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join14(homedir8(), ".switchroom", "agents");
32507
32589
  const listGatewayCandidates = () => {
32508
32590
  const out = [];
32509
32591
  try {
32510
- for (const name of readdirSync6(agentsDir).sort()) {
32592
+ for (const name of readdirSync7(agentsDir).sort()) {
32511
32593
  const sock = resolve10(agentsDir, name, "telegram", "gateway.sock");
32512
- if (existsSync15(sock))
32594
+ if (existsSync16(sock))
32513
32595
  out.push({ agent: name, sock });
32514
32596
  }
32515
32597
  } catch {}
@@ -32532,7 +32614,7 @@ async function loadConfigResilient() {
32532
32614
  };
32533
32615
  return waitForConfigRecovery({
32534
32616
  initialError: err2,
32535
- homeDir: homedir7(),
32617
+ homeDir: homedir8(),
32536
32618
  loadFn: () => loadConfig(),
32537
32619
  configPath,
32538
32620
  notify,
@@ -32556,10 +32638,10 @@ async function main() {
32556
32638
  `);
32557
32639
  process.exit(2);
32558
32640
  }
32559
- const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join13(homedir7(), ".switchroom", "agents");
32641
+ const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join14(homedir8(), ".switchroom", "agents");
32560
32642
  const resolveGatewaySocket = (agentName) => {
32561
32643
  const sock = resolve10(agentsDir, agentName, "telegram", "gateway.sock");
32562
- return existsSync15(sock) ? sock : null;
32644
+ return existsSync16(sock) ? sock : null;
32563
32645
  };
32564
32646
  const approvalGateway = new SocketApprovalGateway({
32565
32647
  resolveGatewaySocket,
@@ -32578,7 +32660,7 @@ async function main() {
32578
32660
  }), { log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
32579
32661
  `) });
32580
32662
  const server = new HostdServer({
32581
- homeDir: homedir7(),
32663
+ homeDir: homedir8(),
32582
32664
  agentUids,
32583
32665
  config: {
32584
32666
  agents: Object.fromEntries(Object.entries(config.agents).map(([n, a]) => [
@@ -32607,7 +32689,7 @@ async function main() {
32607
32689
  const autoRel = config.host_control?.auto_release_check;
32608
32690
  const watcherMode = resolveReleaseWatcherMode(config);
32609
32691
  if (watcherMode.mode !== "off") {
32610
- const eventsLog = join13(homedir7(), ".switchroom", "release-watcher-events.jsonl");
32692
+ const eventsLog = join14(homedir8(), ".switchroom", "release-watcher-events.jsonl");
32611
32693
  const log = (m) => {
32612
32694
  process.stderr.write(`hostd: ${m}
32613
32695
  `);
@@ -32666,7 +32748,7 @@ async function main() {
32666
32748
  `);
32667
32749
  } else
32668
32750
  notifier = new UpdateNotifier({
32669
- statePath: join13(homedir7(), ".switchroom", "release-notify-state.json"),
32751
+ statePath: join14(homedir8(), ".switchroom", "release-notify-state.json"),
32670
32752
  repostSuppressMs: cardTimeoutMs,
32671
32753
  isFleetMutationInFlight: () => server.isFleetMutationLocked(),
32672
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.29",
4
+ "version": "0.19.31",
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
- echo "litellm(cron): no virtual key for '{{name}}' — falling back to direct OAuth (untracked, unguarded)" >&2
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).