switchroom 0.20.7 → 0.20.8

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.
@@ -12004,6 +12004,28 @@ var OverlayDocSchema = exports_external.object({
12004
12004
  // src/config/overlay-loader.ts
12005
12005
  var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12006
12006
  var OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
12007
+ var OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12008
+ function recordReadFailure(agentCfg, failure) {
12009
+ const node = agentCfg;
12010
+ const existing = node[OVERLAY_READ_FAILURES];
12011
+ if (Array.isArray(existing)) {
12012
+ existing.push(failure);
12013
+ return;
12014
+ }
12015
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12016
+ value: [failure],
12017
+ enumerable: false,
12018
+ configurable: true,
12019
+ writable: false
12020
+ });
12021
+ }
12022
+ function overlayReadFailures(config, agent) {
12023
+ const agentCfg = config.agents?.[agent];
12024
+ if (!agentCfg)
12025
+ return [];
12026
+ const list = agentCfg[OVERLAY_READ_FAILURES];
12027
+ return Array.isArray(list) ? list : [];
12028
+ }
12007
12029
  function deriveOverlayTitle(raw, fileName) {
12008
12030
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12009
12031
  if (titleFromComment)
@@ -12013,17 +12035,39 @@ function deriveOverlayTitle(raw, fileName) {
12013
12035
  return;
12014
12036
  return base.length > 0 ? base : undefined;
12015
12037
  }
12038
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12039
+ try {
12040
+ return readFileSync(file, "utf-8");
12041
+ } catch (err) {
12042
+ const code = err.code;
12043
+ if (code === "ENOENT")
12044
+ return;
12045
+ const w = {
12046
+ agent: agentName,
12047
+ file,
12048
+ reason: `read error: ${err.message}`,
12049
+ code: code ?? "EUNKNOWN"
12050
+ };
12051
+ recordReadFailure(agentCfg, { file, code: w.code });
12052
+ warnings.push(w);
12053
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12054
+ return;
12055
+ }
12056
+ }
12016
12057
  function overlayDirFor(agentName, subdir) {
12017
12058
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12018
12059
  return resolve2(base);
12019
12060
  }
12020
- function listYamlFiles(dir) {
12061
+ function listYamlFiles(dir, onUnreadableDir) {
12021
12062
  if (!existsSync2(dir))
12022
12063
  return [];
12023
12064
  let entries;
12024
12065
  try {
12025
12066
  entries = readdirSync(dir);
12026
- } catch {
12067
+ } catch (err) {
12068
+ const code = err.code;
12069
+ if (code !== "ENOENT")
12070
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12027
12071
  return [];
12028
12072
  }
12029
12073
  const out = [];
@@ -12061,12 +12105,24 @@ function applyAgentOverlays(config) {
12061
12105
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12062
12106
  try {
12063
12107
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12064
- const files = listYamlFiles(scheduleDir);
12108
+ const files = listYamlFiles(scheduleDir, (code) => {
12109
+ const w = {
12110
+ agent: agentName,
12111
+ file: scheduleDir,
12112
+ reason: `read error: cannot list overlay directory (${code})`,
12113
+ code
12114
+ };
12115
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12116
+ warnings.push(w);
12117
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12118
+ });
12065
12119
  if (files.length > 0) {
12066
12120
  const merged = [...agentCfg.schedule ?? []];
12067
12121
  for (const file of files) {
12122
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12123
+ if (raw === undefined)
12124
+ continue;
12068
12125
  try {
12069
- const raw = readFileSync(file, "utf-8");
12070
12126
  const parsed = $parse(raw);
12071
12127
  const doc = OverlayDocSchema.parse(parsed);
12072
12128
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12101,13 +12157,25 @@ function applyAgentOverlays(config) {
12101
12157
  }
12102
12158
  try {
12103
12159
  const skillsDir = overlayDirFor(agentName, "skills.d");
12104
- const skillFiles = listYamlFiles(skillsDir);
12160
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12161
+ const w = {
12162
+ agent: agentName,
12163
+ file: skillsDir,
12164
+ reason: `read error: cannot list overlay directory (${code})`,
12165
+ code
12166
+ };
12167
+ recordReadFailure(agentCfg, { file: skillsDir, code });
12168
+ warnings.push(w);
12169
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
12170
+ });
12105
12171
  if (skillFiles.length === 0) {} else {
12106
12172
  const merged = [...agentCfg.skills ?? []];
12107
12173
  const seen = new Set(merged);
12108
12174
  for (const file of skillFiles) {
12175
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12176
+ if (raw === undefined)
12177
+ continue;
12109
12178
  try {
12110
- const raw = readFileSync(file, "utf-8");
12111
12179
  const parsed = $parse(raw);
12112
12180
  const doc = OverlayDocSchema.parse(parsed);
12113
12181
  for (const skillName of doc.skills ?? []) {
@@ -15264,6 +15332,17 @@ function createScheduleReloader(opts) {
15264
15332
  }
15265
15333
  };
15266
15334
  }
15335
+ function formatReadFailures(failures) {
15336
+ return failures.map((f) => `${f.file} (${f.code})`).join(", ") + " — fix ownership/mode so the agent uid can read the file " + "(a root-euid writer leaves cron overlays root-owned 0600; " + "`switchroom apply` / a reconcile ownership sweep heals this)";
15337
+ }
15338
+ function loadAgentEntriesStrict(configPath, agentName) {
15339
+ const config = loadConfig(configPath);
15340
+ const failures = overlayReadFailures(config, agentName);
15341
+ if (failures.length > 0) {
15342
+ throw new Error(`schedule.d overlay unreadable — refusing a reload that may be ` + `missing entries: ${formatReadFailures(failures)}`);
15343
+ }
15344
+ return collectScheduleEntries(config).filter((e) => e.agent === agentName);
15345
+ }
15267
15346
  function resolveReloadPollMs(env) {
15268
15347
  const raw = Number.parseInt(env.SWITCHROOM_SCHEDULER_RELOAD_POLL_MS ?? "", 10);
15269
15348
  return Number.isFinite(raw) && raw >= 1000 ? raw : 30000;
@@ -15303,6 +15382,11 @@ async function main() {
15303
15382
  const config = loadConfig(configPath);
15304
15383
  const allEntries = collectScheduleEntries(config);
15305
15384
  const entries = allEntries.filter((e) => e.agent === agentName);
15385
+ const bootReadFailures = overlayReadFailures(config, agentName);
15386
+ if (bootReadFailures.length > 0) {
15387
+ process.stderr.write(`agent-scheduler: ${agentName} WARNING: ${bootReadFailures.length} ` + `schedule.d overlay file(s) unreadable at boot — their cron entries ` + `are NOT registered: ${formatReadFailures(bootReadFailures)}
15388
+ `);
15389
+ }
15306
15390
  const channel = resolveChannelTarget(config, agentName);
15307
15391
  if (channel === null) {
15308
15392
  process.stderr.write(`agent-scheduler: ${agentName} has no resolvable chat target ` + `(missing telegram.forum_chat_id) — exiting
@@ -15519,15 +15603,24 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
15519
15603
  let reloader;
15520
15604
  let reloadTimer;
15521
15605
  if (isHotReloadEnabled(process.env)) {
15606
+ let lastReloadError;
15522
15607
  reloader = createScheduleReloader({
15523
- loadEntries: () => collectScheduleEntries(loadConfig(configPath)).filter((e) => e.agent === agentName),
15608
+ loadEntries: () => loadAgentEntriesStrict(configPath, agentName),
15524
15609
  register: registerForEntries,
15525
15610
  initialTasks: tasks,
15526
15611
  initialEntries: entries,
15527
- log: (m) => process.stdout.write(`agent-scheduler: ${agentName} ${m}
15528
- `),
15529
- onError: (e) => process.stderr.write(`agent-scheduler: ${agentName} reload skipped (config error, keeping current schedule): ${e.message}
15530
- `)
15612
+ log: (m) => {
15613
+ lastReloadError = undefined;
15614
+ process.stdout.write(`agent-scheduler: ${agentName} ${m}
15615
+ `);
15616
+ },
15617
+ onError: (e) => {
15618
+ if (e.message === lastReloadError)
15619
+ return;
15620
+ lastReloadError = e.message;
15621
+ process.stderr.write(`agent-scheduler: ${agentName} reload skipped (config error, keeping current schedule): ${e.message}
15622
+ `);
15623
+ }
15531
15624
  });
15532
15625
  reloadTimer = setInterval(() => reloader.tick(), resolveReloadPollMs(process.env));
15533
15626
  }
@@ -15560,6 +15653,7 @@ export {
15560
15653
  registerAgentSchedule,
15561
15654
  recoverPendingEscalations,
15562
15655
  main,
15656
+ loadAgentEntriesStrict,
15563
15657
  isHotReloadEnabled,
15564
15658
  ipcDispatcher,
15565
15659
  createScheduleReloader
@@ -12030,6 +12030,20 @@ var init_overlay_schema = __esm(() => {
12030
12030
  // src/config/overlay-loader.ts
12031
12031
  import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
12032
12032
  import { basename, resolve as resolve2 } from "node:path";
12033
+ function recordReadFailure(agentCfg, failure) {
12034
+ const node = agentCfg;
12035
+ const existing = node[OVERLAY_READ_FAILURES];
12036
+ if (Array.isArray(existing)) {
12037
+ existing.push(failure);
12038
+ return;
12039
+ }
12040
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12041
+ value: [failure],
12042
+ enumerable: false,
12043
+ configurable: true,
12044
+ writable: false
12045
+ });
12046
+ }
12033
12047
  function deriveOverlayTitle(raw, fileName) {
12034
12048
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12035
12049
  if (titleFromComment)
@@ -12039,17 +12053,39 @@ function deriveOverlayTitle(raw, fileName) {
12039
12053
  return;
12040
12054
  return base.length > 0 ? base : undefined;
12041
12055
  }
12056
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12057
+ try {
12058
+ return readFileSync(file, "utf-8");
12059
+ } catch (err) {
12060
+ const code = err.code;
12061
+ if (code === "ENOENT")
12062
+ return;
12063
+ const w = {
12064
+ agent: agentName,
12065
+ file,
12066
+ reason: `read error: ${err.message}`,
12067
+ code: code ?? "EUNKNOWN"
12068
+ };
12069
+ recordReadFailure(agentCfg, { file, code: w.code });
12070
+ warnings.push(w);
12071
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12072
+ return;
12073
+ }
12074
+ }
12042
12075
  function overlayDirFor(agentName, subdir) {
12043
12076
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12044
12077
  return resolve2(base);
12045
12078
  }
12046
- function listYamlFiles(dir) {
12079
+ function listYamlFiles(dir, onUnreadableDir) {
12047
12080
  if (!existsSync2(dir))
12048
12081
  return [];
12049
12082
  let entries;
12050
12083
  try {
12051
12084
  entries = readdirSync(dir);
12052
- } catch {
12085
+ } catch (err) {
12086
+ const code = err.code;
12087
+ if (code !== "ENOENT")
12088
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12053
12089
  return [];
12054
12090
  }
12055
12091
  const out = [];
@@ -12087,12 +12123,24 @@ function applyAgentOverlays(config) {
12087
12123
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12088
12124
  try {
12089
12125
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12090
- const files = listYamlFiles(scheduleDir);
12126
+ const files = listYamlFiles(scheduleDir, (code) => {
12127
+ const w = {
12128
+ agent: agentName,
12129
+ file: scheduleDir,
12130
+ reason: `read error: cannot list overlay directory (${code})`,
12131
+ code
12132
+ };
12133
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12134
+ warnings.push(w);
12135
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12136
+ });
12091
12137
  if (files.length > 0) {
12092
12138
  const merged = [...agentCfg.schedule ?? []];
12093
12139
  for (const file of files) {
12140
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12141
+ if (raw === undefined)
12142
+ continue;
12094
12143
  try {
12095
- const raw = readFileSync(file, "utf-8");
12096
12144
  const parsed = import_yaml.parse(raw);
12097
12145
  const doc = OverlayDocSchema.parse(parsed);
12098
12146
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12127,13 +12175,25 @@ function applyAgentOverlays(config) {
12127
12175
  }
12128
12176
  try {
12129
12177
  const skillsDir = overlayDirFor(agentName, "skills.d");
12130
- const skillFiles = listYamlFiles(skillsDir);
12178
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12179
+ const w = {
12180
+ agent: agentName,
12181
+ file: skillsDir,
12182
+ reason: `read error: cannot list overlay directory (${code})`,
12183
+ code
12184
+ };
12185
+ recordReadFailure(agentCfg, { file: skillsDir, code });
12186
+ warnings.push(w);
12187
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
12188
+ });
12131
12189
  if (skillFiles.length === 0) {} else {
12132
12190
  const merged = [...agentCfg.skills ?? []];
12133
12191
  const seen = new Set(merged);
12134
12192
  for (const file of skillFiles) {
12193
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12194
+ if (raw === undefined)
12195
+ continue;
12135
12196
  try {
12136
- const raw = readFileSync(file, "utf-8");
12137
12197
  const parsed = import_yaml.parse(raw);
12138
12198
  const doc = OverlayDocSchema.parse(parsed);
12139
12199
  for (const skillName of doc.skills ?? []) {
@@ -12161,7 +12221,7 @@ function applyAgentOverlays(config) {
12161
12221
  }
12162
12222
  return { config, warnings };
12163
12223
  }
12164
- var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
12224
+ var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
12165
12225
  var init_overlay_loader = __esm(() => {
12166
12226
  init_zod();
12167
12227
  init_overlay_schema();
@@ -12169,6 +12229,7 @@ var init_overlay_loader = __esm(() => {
12169
12229
  import_yaml = __toESM(require_dist(), 1);
12170
12230
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12171
12231
  OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
12232
+ OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12172
12233
  });
12173
12234
 
12174
12235
  // src/config/merge.ts
@@ -12767,6 +12767,21 @@ var OverlayDocSchema = exports_external.object({
12767
12767
  // src/config/overlay-loader.ts
12768
12768
  var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12769
12769
  var OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
12770
+ var OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12771
+ function recordReadFailure(agentCfg, failure) {
12772
+ const node = agentCfg;
12773
+ const existing = node[OVERLAY_READ_FAILURES];
12774
+ if (Array.isArray(existing)) {
12775
+ existing.push(failure);
12776
+ return;
12777
+ }
12778
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12779
+ value: [failure],
12780
+ enumerable: false,
12781
+ configurable: true,
12782
+ writable: false
12783
+ });
12784
+ }
12770
12785
  function deriveOverlayTitle(raw, fileName) {
12771
12786
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12772
12787
  if (titleFromComment)
@@ -12776,17 +12791,39 @@ function deriveOverlayTitle(raw, fileName) {
12776
12791
  return;
12777
12792
  return base.length > 0 ? base : undefined;
12778
12793
  }
12794
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12795
+ try {
12796
+ return readFileSync(file, "utf-8");
12797
+ } catch (err) {
12798
+ const code = err.code;
12799
+ if (code === "ENOENT")
12800
+ return;
12801
+ const w = {
12802
+ agent: agentName,
12803
+ file,
12804
+ reason: `read error: ${err.message}`,
12805
+ code: code ?? "EUNKNOWN"
12806
+ };
12807
+ recordReadFailure(agentCfg, { file, code: w.code });
12808
+ warnings.push(w);
12809
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12810
+ return;
12811
+ }
12812
+ }
12779
12813
  function overlayDirFor(agentName, subdir) {
12780
12814
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12781
12815
  return resolve2(base);
12782
12816
  }
12783
- function listYamlFiles(dir) {
12817
+ function listYamlFiles(dir, onUnreadableDir) {
12784
12818
  if (!existsSync2(dir))
12785
12819
  return [];
12786
12820
  let entries;
12787
12821
  try {
12788
12822
  entries = readdirSync(dir);
12789
- } catch {
12823
+ } catch (err) {
12824
+ const code = err.code;
12825
+ if (code !== "ENOENT")
12826
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12790
12827
  return [];
12791
12828
  }
12792
12829
  const out = [];
@@ -12824,12 +12861,24 @@ function applyAgentOverlays(config) {
12824
12861
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12825
12862
  try {
12826
12863
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12827
- const files = listYamlFiles(scheduleDir);
12864
+ const files = listYamlFiles(scheduleDir, (code) => {
12865
+ const w = {
12866
+ agent: agentName,
12867
+ file: scheduleDir,
12868
+ reason: `read error: cannot list overlay directory (${code})`,
12869
+ code
12870
+ };
12871
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12872
+ warnings.push(w);
12873
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12874
+ });
12828
12875
  if (files.length > 0) {
12829
12876
  const merged = [...agentCfg.schedule ?? []];
12830
12877
  for (const file of files) {
12878
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12879
+ if (raw === undefined)
12880
+ continue;
12831
12881
  try {
12832
- const raw = readFileSync(file, "utf-8");
12833
12882
  const parsed = $parse(raw);
12834
12883
  const doc = OverlayDocSchema.parse(parsed);
12835
12884
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12864,13 +12913,25 @@ function applyAgentOverlays(config) {
12864
12913
  }
12865
12914
  try {
12866
12915
  const skillsDir = overlayDirFor(agentName, "skills.d");
12867
- const skillFiles = listYamlFiles(skillsDir);
12916
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12917
+ const w = {
12918
+ agent: agentName,
12919
+ file: skillsDir,
12920
+ reason: `read error: cannot list overlay directory (${code})`,
12921
+ code
12922
+ };
12923
+ recordReadFailure(agentCfg, { file: skillsDir, code });
12924
+ warnings.push(w);
12925
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
12926
+ });
12868
12927
  if (skillFiles.length === 0) {} else {
12869
12928
  const merged = [...agentCfg.skills ?? []];
12870
12929
  const seen = new Set(merged);
12871
12930
  for (const file of skillFiles) {
12931
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12932
+ if (raw === undefined)
12933
+ continue;
12872
12934
  try {
12873
- const raw = readFileSync(file, "utf-8");
12874
12935
  const parsed = $parse(raw);
12875
12936
  const doc = OverlayDocSchema.parse(parsed);
12876
12937
  for (const skillName of doc.skills ?? []) {