switchroom 0.13.35 → 0.13.37

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.
@@ -23247,6 +23247,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23247
23247
  if (existsSync12(`${hostHomeForChecks}/.switchroom/skills`)) {
23248
23248
  lines.push(` - ${homePrefix}/.switchroom/skills:${homePrefix}/.switchroom/skills:ro`);
23249
23249
  }
23250
+ if (existsSync12(`${hostHomeForChecks}/.switchroom/mcp-launchers`)) {
23251
+ lines.push(` - ${homePrefix}/.switchroom/mcp-launchers:${homePrefix}/.switchroom/mcp-launchers:ro`);
23252
+ }
23250
23253
  if (existsSync12(`${hostHomeForChecks}/.switchroom/credentials/${a.name}`)) {
23251
23254
  lines.push(` - ${homePrefix}/.switchroom/credentials/${a.name}:${homePrefix}/.switchroom/credentials:ro`);
23252
23255
  }
@@ -29681,6 +29684,7 @@ __export(exports_doctor, {
29681
29684
  deriveEd25519PublicKeyBytes: () => deriveEd25519PublicKeyBytes,
29682
29685
  classifyReadError: () => classifyReadError,
29683
29686
  checkVaultBrokerSocketPairs: () => checkVaultBrokerSocketPairs,
29687
+ checkUserDeclaredMcps: () => checkUserDeclaredMcps,
29684
29688
  checkTelegram: () => checkTelegram,
29685
29689
  checkStartShStale: () => checkStartShStale,
29686
29690
  checkSkillsPrerequisites: () => checkSkillsPrerequisites,
@@ -29978,6 +29982,33 @@ function checkConfig(config, configPath) {
29978
29982
  });
29979
29983
  return results;
29980
29984
  }
29985
+ function checkUserDeclaredMcps(name, agentConfig, config, renderedMcpServers) {
29986
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
29987
+ const declaredMcp = resolved.mcp_servers ?? {};
29988
+ const declaredKeys = Object.entries(declaredMcp).filter(([, v]) => v !== false).map(([k]) => k);
29989
+ const renderedKeys = Object.keys(renderedMcpServers);
29990
+ const missing = declaredKeys.filter((k) => !renderedKeys.includes(k));
29991
+ if (declaredKeys.length === 0) {
29992
+ return {
29993
+ name: `${name}: user-declared MCPs`,
29994
+ status: "skip",
29995
+ detail: "no user-declared mcp_servers in switchroom.yaml"
29996
+ };
29997
+ }
29998
+ if (missing.length === 0) {
29999
+ return {
30000
+ name: `${name}: user-declared MCPs`,
30001
+ status: "ok",
30002
+ detail: `${declaredKeys.length} declared, all in .mcp.json (${declaredKeys.join(", ")})`
30003
+ };
30004
+ }
30005
+ return {
30006
+ name: `${name}: user-declared MCPs`,
30007
+ status: "warn",
30008
+ detail: `${missing.length}/${declaredKeys.length} declared but missing from .mcp.json: ${missing.join(", ")}`,
30009
+ fix: `Run \`switchroom agent reconcile ${name} --restart\`. If the entry still doesn't appear, check switchroom.yaml shape (defaults.mcp_servers.<key> or agents.${name}.mcp_servers.<key>).`
30010
+ };
30011
+ }
29981
30012
  function checkLegacyState() {
29982
30013
  const results = [];
29983
30014
  const h = process.env.HOME ?? "/root";
@@ -30739,6 +30770,7 @@ function checkAgents(config, configPath) {
30739
30770
  detail: memoryEnabled ? "switchroom-telegram + hindsight" : "switchroom-telegram"
30740
30771
  });
30741
30772
  }
30773
+ results.push(checkUserDeclaredMcps(name, agentConfig, config, mcp.mcpServers ?? {}));
30742
30774
  } catch (err) {
30743
30775
  results.push({
30744
30776
  name: `${name}: .mcp.json`,
@@ -31354,6 +31386,7 @@ var init_doctor = __esm(() => {
31354
31386
  init_doctor_status();
31355
31387
  init_vault();
31356
31388
  init_loader();
31389
+ init_merge();
31357
31390
  init_paths();
31358
31391
  init_helpers();
31359
31392
  init_lifecycle();
@@ -47711,8 +47744,8 @@ var {
47711
47744
  } = import__.default;
47712
47745
 
47713
47746
  // src/build-info.ts
47714
- var VERSION = "0.13.35";
47715
- var COMMIT_SHA = "c41aabe5";
47747
+ var VERSION = "0.13.37";
47748
+ var COMMIT_SHA = "623c57e0";
47716
47749
 
47717
47750
  // src/cli/agent.ts
47718
47751
  init_source();
@@ -49460,6 +49493,14 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
49460
49493
  mcpServers[gdrive.key] = gdrive.value;
49461
49494
  }
49462
49495
  }
49496
+ if (agentConfig.mcp_servers) {
49497
+ const filtered = filterMcpServers(agentConfig.mcp_servers);
49498
+ if (filtered) {
49499
+ for (const [key, value] of Object.entries(filtered)) {
49500
+ mcpServers[key] = value;
49501
+ }
49502
+ }
49503
+ }
49463
49504
  writeIfChanged(mcpJsonPath, () => JSON.stringify({ mcpServers }, null, 2) + `
49464
49505
  `, created, skipped, 384);
49465
49506
  mcpServerKeysToTrust = Object.keys(mcpServers);
@@ -50504,6 +50545,14 @@ ${body}
50504
50545
  mcpServers[gdrive.key] = gdrive.value;
50505
50546
  }
50506
50547
  }
50548
+ if (agentConfig.mcp_servers) {
50549
+ const filtered = filterMcpServers(agentConfig.mcp_servers);
50550
+ if (filtered) {
50551
+ for (const [key, value] of Object.entries(filtered)) {
50552
+ mcpServers[key] = value;
50553
+ }
50554
+ }
50555
+ }
50507
50556
  const mcpJson = { mcpServers };
50508
50557
  const after = JSON.stringify(mcpJson, null, 2) + `
50509
50558
  `;
@@ -61580,6 +61629,23 @@ function registerVaultBackupCommand(vault, program3) {
61580
61629
  });
61581
61630
  }
61582
61631
 
61632
+ // src/cli/vault-denied-envelope.ts
61633
+ var ENVELOPE_SENTINEL = "ERROR-ENVELOPE:";
61634
+ function writeVaultDeniedEnvelope(vaultKey, brokerCode, human) {
61635
+ const envelope = {
61636
+ v: 1,
61637
+ code: "VAULT-BROKER-DENIED",
61638
+ human: `${brokerCode}: ${human}`,
61639
+ fix: {
61640
+ kind: "request_vault_grant",
61641
+ vault_key: vaultKey
61642
+ },
61643
+ request_id: `vault-cli-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
61644
+ };
61645
+ process.stderr.write(`${ENVELOPE_SENTINEL} ${JSON.stringify(envelope)}
61646
+ `);
61647
+ }
61648
+
61583
61649
  // src/cli/vault.ts
61584
61650
  function isSandboxContext() {
61585
61651
  return process.env.SWITCHROOM_RUNTIME === "docker";
@@ -61805,6 +61871,7 @@ function registerVaultCommand(program3) {
61805
61871
  }
61806
61872
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
61807
61873
  `);
61874
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
61808
61875
  process.exit(VAULT_EXIT_DENIED);
61809
61876
  }
61810
61877
  if (inSandbox) {
@@ -62020,6 +62087,7 @@ Push passphrase to broker for future requests? [Y/n]: `);
62020
62087
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
62021
62088
  ` + `${recoveryHint("denied", key)}
62022
62089
  `);
62090
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
62023
62091
  process.exit(2);
62024
62092
  }
62025
62093
  } else {
@@ -76198,7 +76266,34 @@ function denyPendingScheduleEntry(opts) {
76198
76266
 
76199
76267
  // src/cli/agent-config-write.ts
76200
76268
  import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
76269
+ import { randomUUID as randomUUID5 } from "node:crypto";
76201
76270
  var MAX_ENTRIES_PER_AGENT = 20;
76271
+ var MIN_CRON_INTERVAL_MIN = 5;
76272
+ function extractCronSmallestGapMin(expr) {
76273
+ const fields = expr.trim().split(/\s+/);
76274
+ if (fields.length < 5)
76275
+ return 0;
76276
+ const min = fields[0];
76277
+ if (min === "*")
76278
+ return 1;
76279
+ const step = min.match(/^\*\/(\d+)$/);
76280
+ if (step)
76281
+ return Number(step[1]);
76282
+ if (min.includes(",")) {
76283
+ const parts = min.split(",").map((s) => Number(s)).filter((n) => Number.isFinite(n));
76284
+ if (parts.length >= 2) {
76285
+ const sorted = [...parts].sort((a, b) => a - b);
76286
+ let smallest = Infinity;
76287
+ for (let i = 1;i < sorted.length; i++) {
76288
+ const gap = sorted[i] - sorted[i - 1];
76289
+ if (gap > 0 && gap < smallest)
76290
+ smallest = gap;
76291
+ }
76292
+ return Number.isFinite(smallest) ? smallest : 0;
76293
+ }
76294
+ }
76295
+ return 0;
76296
+ }
76202
76297
  function checkOperatorContext(verb, env2 = process.env) {
76203
76298
  if (env2.SWITCHROOM_OPERATOR === "1")
76204
76299
  return { ok: true };
@@ -76211,8 +76306,45 @@ function checkOperatorContext(verb, env2 = process.env) {
76211
76306
  }
76212
76307
  return { ok: true };
76213
76308
  }
76309
+ function buildEnvelopeForCode(code, message, extra) {
76310
+ const request_id = `agent-config-${randomUUID5()}`;
76311
+ if (code === "E_CRON_TOO_FREQUENT") {
76312
+ return {
76313
+ v: 1,
76314
+ code,
76315
+ human: message,
76316
+ fix: {
76317
+ kind: "quota_exceeded",
76318
+ quota: "cron_min_interval_minutes",
76319
+ current: typeof extra.requested_interval_min === "number" ? extra.requested_interval_min : 0,
76320
+ limit: MIN_CRON_INTERVAL_MIN
76321
+ },
76322
+ request_id
76323
+ };
76324
+ }
76325
+ if (code === "E_QUOTA_EXCEEDED") {
76326
+ const current = typeof extra.current === "number" ? extra.current : MAX_ENTRIES_PER_AGENT;
76327
+ return {
76328
+ v: 1,
76329
+ code,
76330
+ human: message,
76331
+ fix: {
76332
+ kind: "quota_exceeded",
76333
+ quota: "schedule_entries_per_agent",
76334
+ current,
76335
+ limit: MAX_ENTRIES_PER_AGENT
76336
+ },
76337
+ request_id
76338
+ };
76339
+ }
76340
+ return;
76341
+ }
76214
76342
  function emitError(code, message, extra = {}) {
76215
- process.stderr.write(JSON.stringify({ code, message, ...extra }) + `
76343
+ const error_envelope = buildEnvelopeForCode(code, message, extra);
76344
+ const line = { code, message, ...extra };
76345
+ if (error_envelope)
76346
+ line.error_envelope = error_envelope;
76347
+ process.stderr.write(JSON.stringify(line) + `
76216
76348
  `);
76217
76349
  }
76218
76350
  function exitCodeFor(code) {
@@ -76276,7 +76408,8 @@ function scheduleAdd(opts) {
76276
76408
  ok: false,
76277
76409
  code: "E_CRON_TOO_FREQUENT",
76278
76410
  message: "cron interval is tighter than the minimum (5 minutes)",
76279
- exit: 9
76411
+ exit: 9,
76412
+ meta: { requested_interval_min: extractCronSmallestGapMin(opts.cronExpr) }
76280
76413
  };
76281
76414
  }
76282
76415
  const rej = filterOverlaySecrets(dry.doc, "overlay");
@@ -76294,7 +76427,8 @@ function scheduleAdd(opts) {
76294
76427
  ok: false,
76295
76428
  code: "E_QUOTA_EXCEEDED",
76296
76429
  message: `agent already has ${existing.length} overlay entries (max ${MAX_ENTRIES_PER_AGENT})`,
76297
- exit: 9
76430
+ exit: 9,
76431
+ meta: { current: existing.length }
76298
76432
  };
76299
76433
  }
76300
76434
  const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
@@ -76493,7 +76627,7 @@ function registerAgentConfigWriteCommands(program3) {
76493
76627
  else if (process.env.SWITCHROOM_AGENT_NAME)
76494
76628
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76495
76629
  if (!r.ok) {
76496
- emitError(r.code, r.message);
76630
+ emitError(r.code, r.message, r.meta ?? {});
76497
76631
  appendAudit(resolvedAgent, "schedule.add", { cron: opts.cron, prompt: opts.prompt, name: opts.name, code: r.code, would_recreate: false }, r.exit);
76498
76632
  process.exit(r.exit);
76499
76633
  }
@@ -76606,7 +76740,7 @@ function registerAgentConfigWriteCommands(program3) {
76606
76740
  else if (process.env.SWITCHROOM_AGENT_NAME)
76607
76741
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76608
76742
  if (!r.ok) {
76609
- emitError(r.code, r.message);
76743
+ emitError(r.code, r.message, r.meta ?? {});
76610
76744
  appendAudit(resolvedAgent, "schedule.remove", { ...opts, code: r.code }, r.exit);
76611
76745
  process.exit(r.exit);
76612
76746
  }
@@ -14920,16 +14920,6 @@ function deniedResponse(request_id, error, duration_ms = 0) {
14920
14920
  error
14921
14921
  };
14922
14922
  }
14923
- function errorResponse(request_id, error, duration_ms = 0) {
14924
- return {
14925
- v: 1,
14926
- request_id,
14927
- result: "error",
14928
- exit_code: null,
14929
- duration_ms,
14930
- error
14931
- };
14932
- }
14933
14923
 
14934
14924
  // src/analytics/error-friction.ts
14935
14925
  import { createHash as createHash2 } from "node:crypto";
@@ -19515,6 +19505,11 @@ class ErrorBuilder {
19515
19505
  return this;
19516
19506
  }
19517
19507
  docs(url) {
19508
+ try {
19509
+ new URL(url);
19510
+ } catch {
19511
+ throw new TypeError(`err().docs(): invalid URL: ${JSON.stringify(url)} — ErrorEnvelopeSchema requires a fully-qualified URL`);
19512
+ }
19518
19513
  this._docs = url;
19519
19514
  return this;
19520
19515
  }
@@ -20391,6 +20386,14 @@ var CONFIG_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
20391
20386
  function defaultApprovalId() {
20392
20387
  return randomBytes(4).toString("hex");
20393
20388
  }
20389
+ function formatConfigApprovalDenyError(approval, approvalId) {
20390
+ if (approval.denySource === "dispatch_failure") {
20391
+ const detail = approval.reason ?? "card dispatch failed";
20392
+ return `E_APPROVAL_DISPATCH_FAILED: ${detail} (approval_id=${approvalId})`;
20393
+ }
20394
+ const suffix = approval.reason ? `: ${approval.reason}` : "";
20395
+ return `E_DENIED: operator denied config_propose_edit${suffix} (approval_id=${approvalId})`;
20396
+ }
20394
20397
  function unlinkSyncBestEffort(path2) {
20395
20398
  try {
20396
20399
  unlinkSync(path2);
@@ -20627,8 +20630,10 @@ class HostdServer {
20627
20630
  resp = await this.handleConfigProposeEdit(req, caller, started);
20628
20631
  break;
20629
20632
  }
20630
- } catch (err2) {
20631
- resp = errorResponse(req.request_id, `hostd dispatch failed: ${err2.message}`, Date.now() - started);
20633
+ } catch (e) {
20634
+ const msg = e.message;
20635
+ resp = err("E_DISPATCH_FAILED", "hostd dispatch failed").why(msg).op(req.op).caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20636
+ resp = { ...resp, error: `hostd dispatch failed: ${msg}` };
20632
20637
  }
20633
20638
  await this.writeAudit({ caller, req, resp });
20634
20639
  socket.write(encodeResponse(resp));
@@ -20924,7 +20929,7 @@ class HostdServer {
20924
20929
  async handleConfigProposeEdit(req, caller, started) {
20925
20930
  const enabled = this.opts.config.hostd?.config_edit_enabled === true;
20926
20931
  if (!enabled) {
20927
- return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20932
+ return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
20928
20933
  }
20929
20934
  const configPath = this.opts.configPath ?? req.args.target_path;
20930
20935
  const verdict = validateConfigEdit({
@@ -20933,10 +20938,12 @@ class HostdServer {
20933
20938
  unifiedDiff: req.args.unified_diff
20934
20939
  });
20935
20940
  if (!verdict.ok) {
20936
- return errorResponse(req.request_id, `${verdict.code}: ${verdict.detail}`, Date.now() - started);
20941
+ return err(verdict.code, verdict.detail).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20937
20942
  }
20938
20943
  if (!this.opts.approvalGateway) {
20939
- return errorResponse(req.request_id, "E_NO_APPROVAL_GATEWAY: validation passed but hostd was " + "started without an approval-gateway wiring; the operator " + "build is missing the telegram-plugin link", Date.now() - started);
20944
+ return err("E_NO_APPROVAL_GATEWAY", "validation passed but hostd was started without an approval-gateway wiring; the operator build is missing the telegram-plugin link").fixOperatorAction("infra", [
20945
+ "ensure hostd was launched with --approval-gateway / telegram-plugin link"
20946
+ ]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20940
20947
  }
20941
20948
  const callerName = caller.kind === "agent" ? caller.name : "operator";
20942
20949
  const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
@@ -20948,35 +20955,45 @@ class HostdServer {
20948
20955
  timeoutMs: CONFIG_APPROVAL_TIMEOUT_MS
20949
20956
  });
20950
20957
  if (approval.verdict === "deny") {
20951
- return errorResponse(req.request_id, `E_DENIED: operator denied config_propose_edit (approval_id=${approvalId})`, Date.now() - started);
20958
+ const legacy = formatConfigApprovalDenyError(approval, approvalId);
20959
+ const isDispatchFailure = approval.denySource === "dispatch_failure";
20960
+ const code = isDispatchFailure ? "E_APPROVAL_DISPATCH_FAILED" : "E_DENIED";
20961
+ const human = isDispatchFailure ? "approval card dispatch failed before the operator could see it" : "operator denied config_propose_edit";
20962
+ const b = err(code, human).why(legacy).fixOperatorAction(isDispatchFailure ? "infra" : "policy_denied", isDispatchFailure ? ["check telegram-plugin gateway connectivity"] : undefined).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined);
20963
+ if (!isDispatchFailure)
20964
+ b.asDenied();
20965
+ const built = b.build(req.request_id, Date.now() - started);
20966
+ return { ...built, error: legacy };
20952
20967
  }
20953
20968
  if (approval.verdict === "timeout") {
20954
- return errorResponse(req.request_id, `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`, Date.now() - started);
20969
+ const legacy = `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`;
20970
+ const built = err("E_APPROVAL_TIMEOUT", "operator approval card expired without a tap").why(`approval_id=${approvalId}`).fixOperatorAction("policy_denied").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
20971
+ return { ...built, error: legacy };
20955
20972
  }
20956
20973
  const release = await this.acquireConfigApplyLock();
20957
20974
  try {
20958
20975
  let snapshot;
20959
20976
  try {
20960
20977
  snapshot = readFileSync5(configPath, "utf-8");
20961
- } catch (err2) {
20978
+ } catch (e) {
20962
20979
  await approval.finalize({
20963
20980
  outcome: "reconcile_failed_rolled_back",
20964
- detail: `pre-write snapshot read failed: ${err2.message}`
20981
+ detail: `pre-write snapshot read failed: ${e.message}`
20965
20982
  });
20966
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: snapshot read failed: ${err2.message}`, Date.now() - started);
20983
+ return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
20967
20984
  }
20968
20985
  const postApply = verdict.postApplyContent;
20969
20986
  const tmp = configPath + ".tmp";
20970
20987
  try {
20971
20988
  writeFileSync3(tmp, postApply);
20972
20989
  renameSync(tmp, configPath);
20973
- } catch (err2) {
20990
+ } catch (e) {
20974
20991
  unlinkSyncBestEffort(tmp);
20975
20992
  await approval.finalize({
20976
20993
  outcome: "reconcile_failed_rolled_back",
20977
- detail: `atomic write failed: ${err2.message}`
20994
+ detail: `atomic write failed: ${e.message}`
20978
20995
  });
20979
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: write failed: ${err2.message}`, Date.now() - started);
20996
+ return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
20980
20997
  }
20981
20998
  const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
20982
20999
  const recRes = await runner({ requestId: approvalId });
@@ -20996,13 +21013,13 @@ class HostdServer {
20996
21013
  try {
20997
21014
  writeFileSync3(tmp, snapshot);
20998
21015
  renameSync(tmp, configPath);
20999
- } catch (err2) {
21000
- rollbackDetail = `snapshot restore failed: ${err2.message}`;
21016
+ } catch (e) {
21017
+ rollbackDetail = `snapshot restore failed: ${e.message}`;
21001
21018
  await approval.finalize({
21002
21019
  outcome: "reconcile_failed_rolled_back",
21003
21020
  detail: rollbackDetail
21004
21021
  });
21005
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: ${rollbackDetail}`, Date.now() - started);
21022
+ return this.reconcileFailedRolledBack(rollbackDetail, req, caller, started);
21006
21023
  }
21007
21024
  const recRes2 = await runner({ requestId: approvalId });
21008
21025
  const recoveryNote = recRes2.exit_code === 0 ? "rolled back successfully" : `rolled back but recovery reconcile also failed (exit ${recRes2.exit_code})`;
@@ -21010,11 +21027,18 @@ class HostdServer {
21010
21027
  outcome: "reconcile_failed_rolled_back",
21011
21028
  detail: recoveryNote
21012
21029
  });
21013
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: reconcile exit ${recRes.exit_code}; ${recoveryNote}`, Date.now() - started);
21030
+ return this.reconcileFailedRolledBack(`reconcile exit ${recRes.exit_code}; ${recoveryNote}`, req, caller, started);
21014
21031
  } finally {
21015
21032
  release();
21016
21033
  }
21017
21034
  }
21035
+ reconcileFailedRolledBack(detail, req, caller, started) {
21036
+ const legacy = `E_RECONCILE_FAILED_ROLLED_BACK: ${detail}`;
21037
+ const built = err("E_RECONCILE_FAILED_ROLLED_BACK", "config write or reconcile failed; live file rolled back to snapshot").why(detail).fixOperatorAction("infra", [
21038
+ "inspect hostd logs + switchroom apply output to identify the underlying failure"
21039
+ ]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
21040
+ return { ...built, error: legacy };
21041
+ }
21018
21042
  configApplyLock = Promise.resolve();
21019
21043
  async acquireConfigApplyLock() {
21020
21044
  let release;
@@ -21180,7 +21204,9 @@ class HostdServer {
21180
21204
  handleGetStatus(req, _caller, started) {
21181
21205
  const entry = this.statusByRequestId.get(req.args.target_request_id);
21182
21206
  if (!entry) {
21183
- return errorResponse(req.request_id, `get_status: internal: entry missing despite gate accept`, Date.now() - started);
21207
+ const legacy = `get_status: internal: entry missing despite gate accept`;
21208
+ const built = err("E_INTERNAL", "entry missing despite gate accept").op("get_status").build(req.request_id, Date.now() - started);
21209
+ return { ...built, error: legacy };
21184
21210
  }
21185
21211
  return this.statusEntryToResponse(req.request_id, entry);
21186
21212
  }
@@ -21339,6 +21365,8 @@ class SocketApprovalGateway {
21339
21365
  if (sockPath === null) {
21340
21366
  return {
21341
21367
  verdict: "deny",
21368
+ reason: "no reachable telegram gateway socket for this agent",
21369
+ denySource: "dispatch_failure",
21342
21370
  finalize: async () => {}
21343
21371
  };
21344
21372
  }
@@ -21379,7 +21407,12 @@ class SocketApprovalGateway {
21379
21407
  return;
21380
21408
  resolved = true;
21381
21409
  log(`request_config_approval write failed (requestId=${req.requestId}): ${err2.message}`);
21382
- resolve6({ verdict: "deny", finalize: async () => {} });
21410
+ resolve6({
21411
+ verdict: "deny",
21412
+ reason: `request_config_approval write failed: ${err2.message}`,
21413
+ denySource: "dispatch_failure",
21414
+ finalize: async () => {}
21415
+ });
21383
21416
  }
21384
21417
  });
21385
21418
  client2.on("data", (chunk2) => {
@@ -21400,8 +21433,13 @@ class SocketApprovalGateway {
21400
21433
  const obj = parsed;
21401
21434
  if (obj.type === "config_approval_resolved" && obj.requestId === req.requestId && (obj.verdict === "approve" || obj.verdict === "deny" || obj.verdict === "timeout") && !resolved) {
21402
21435
  resolved = true;
21436
+ const verdict = obj.verdict;
21437
+ const reasonField = typeof obj.reason === "string" ? obj.reason : undefined;
21438
+ const denySource = verdict === "deny" ? obj.denySource === "dispatch_failure" ? "dispatch_failure" : "operator" : undefined;
21403
21439
  resolve6({
21404
- verdict: obj.verdict,
21440
+ verdict,
21441
+ ...reasonField !== undefined ? { reason: reasonField } : {},
21442
+ ...denySource !== undefined ? { denySource } : {},
21405
21443
  finalize
21406
21444
  });
21407
21445
  }
@@ -21412,13 +21450,23 @@ class SocketApprovalGateway {
21412
21450
  return;
21413
21451
  resolved = true;
21414
21452
  log(`gateway socket error (requestId=${req.requestId}): ${err2.message}`);
21415
- resolve6({ verdict: "deny", finalize: async () => {} });
21453
+ resolve6({
21454
+ verdict: "deny",
21455
+ reason: `gateway socket error: ${err2.message}`,
21456
+ denySource: "dispatch_failure",
21457
+ finalize: async () => {}
21458
+ });
21416
21459
  });
21417
21460
  client2.on("close", () => {
21418
21461
  if (resolved)
21419
21462
  return;
21420
21463
  resolved = true;
21421
- resolve6({ verdict: "deny", finalize: async () => {} });
21464
+ resolve6({
21465
+ verdict: "deny",
21466
+ reason: "gateway socket closed before verdict",
21467
+ denySource: "dispatch_failure",
21468
+ finalize: async () => {}
21469
+ });
21422
21470
  });
21423
21471
  });
21424
21472
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.35",
3
+ "version": "0.13.37",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {