switchroom 0.20.6 → 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.
Files changed (30) hide show
  1. package/dist/agent-scheduler/index.js +106 -12
  2. package/dist/auth-broker/index.js +71 -9
  3. package/dist/cli/notion-write-pretool.mjs +68 -7
  4. package/dist/cli/switchroom.js +387 -30
  5. package/dist/host-control/main.js +72 -10
  6. package/dist/vault/approvals/kernel-server.js +71 -9
  7. package/dist/vault/broker/server.js +71 -9
  8. package/package.json +1 -1
  9. package/telegram-plugin/bridge/ipc-client.ts +17 -1
  10. package/telegram-plugin/dist/bridge/bridge.js +5 -2
  11. package/telegram-plugin/dist/gateway/gateway.js +342 -120
  12. package/telegram-plugin/dist/server.js +5 -2
  13. package/telegram-plugin/gateway/boot-reason.ts +61 -0
  14. package/telegram-plugin/gateway/cron-session.ts +66 -0
  15. package/telegram-plugin/gateway/gateway.ts +50 -54
  16. package/telegram-plugin/gateway/narrative-lane.ts +21 -1
  17. package/telegram-plugin/gateway/obligation-ledger.ts +9 -0
  18. package/telegram-plugin/gateway/obligation-wiring.ts +19 -4
  19. package/telegram-plugin/gateway/pending-inbound-buffer.ts +43 -3
  20. package/telegram-plugin/gateway/represent-delivery-guard.ts +188 -0
  21. package/telegram-plugin/gateway/stream-render.ts +24 -2
  22. package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
  23. package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
  24. package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
  25. package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
  26. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +145 -9
  27. package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
  28. package/telegram-plugin/tests/represent-guard.test.ts +295 -0
  29. package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
  30. package/telegram-plugin/turn-flush-safety.ts +79 -0
@@ -11419,7 +11419,7 @@ var HindsightConfigSchema = exports_external.object({
11419
11419
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
11420
11420
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
11421
11421
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
11422
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11422
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11423
11423
  });
11424
11424
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
11425
11425
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -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
@@ -11444,7 +11444,7 @@ var init_schema = __esm(() => {
11444
11444
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
11445
11445
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
11446
11446
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
11447
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11447
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11448
11448
  });
11449
11449
  MicrosoftWorkspaceConfigSchema = exports_external.object({
11450
11450
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -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
@@ -19170,7 +19231,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19170
19231
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
19171
19232
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
19172
19233
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
19173
- "HINDSIGHT_API_TEMPORAL_LANGUAGES"
19234
+ "HINDSIGHT_API_TEMPORAL_LANGUAGES",
19235
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
19174
19236
  ]);
19175
19237
  var HINDSIGHT_WORKER_SLOT_TYPES = [
19176
19238
  "consolidation",
@@ -12180,7 +12180,7 @@ var HindsightConfigSchema = exports_external.object({
12180
12180
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
12181
12181
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
12182
12182
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
12183
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
12183
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
12184
12184
  });
12185
12185
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
12186
12186
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -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 ?? []) {