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
@@ -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)."),
@@ -12040,6 +12040,20 @@ var init_overlay_schema = __esm(() => {
12040
12040
  // src/config/overlay-loader.ts
12041
12041
  import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
12042
12042
  import { basename, resolve as resolve2 } from "node:path";
12043
+ function recordReadFailure(agentCfg, failure) {
12044
+ const node = agentCfg;
12045
+ const existing = node[OVERLAY_READ_FAILURES];
12046
+ if (Array.isArray(existing)) {
12047
+ existing.push(failure);
12048
+ return;
12049
+ }
12050
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12051
+ value: [failure],
12052
+ enumerable: false,
12053
+ configurable: true,
12054
+ writable: false
12055
+ });
12056
+ }
12043
12057
  function deriveOverlayTitle(raw, fileName) {
12044
12058
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12045
12059
  if (titleFromComment)
@@ -12049,17 +12063,39 @@ function deriveOverlayTitle(raw, fileName) {
12049
12063
  return;
12050
12064
  return base.length > 0 ? base : undefined;
12051
12065
  }
12066
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12067
+ try {
12068
+ return readFileSync(file, "utf-8");
12069
+ } catch (err) {
12070
+ const code = err.code;
12071
+ if (code === "ENOENT")
12072
+ return;
12073
+ const w = {
12074
+ agent: agentName,
12075
+ file,
12076
+ reason: `read error: ${err.message}`,
12077
+ code: code ?? "EUNKNOWN"
12078
+ };
12079
+ recordReadFailure(agentCfg, { file, code: w.code });
12080
+ warnings.push(w);
12081
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12082
+ return;
12083
+ }
12084
+ }
12052
12085
  function overlayDirFor(agentName, subdir) {
12053
12086
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12054
12087
  return resolve2(base);
12055
12088
  }
12056
- function listYamlFiles(dir) {
12089
+ function listYamlFiles(dir, onUnreadableDir) {
12057
12090
  if (!existsSync2(dir))
12058
12091
  return [];
12059
12092
  let entries;
12060
12093
  try {
12061
12094
  entries = readdirSync(dir);
12062
- } catch {
12095
+ } catch (err) {
12096
+ const code = err.code;
12097
+ if (code !== "ENOENT")
12098
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12063
12099
  return [];
12064
12100
  }
12065
12101
  const out = [];
@@ -12097,12 +12133,24 @@ function applyAgentOverlays(config) {
12097
12133
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12098
12134
  try {
12099
12135
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12100
- const files = listYamlFiles(scheduleDir);
12136
+ const files = listYamlFiles(scheduleDir, (code) => {
12137
+ const w = {
12138
+ agent: agentName,
12139
+ file: scheduleDir,
12140
+ reason: `read error: cannot list overlay directory (${code})`,
12141
+ code
12142
+ };
12143
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12144
+ warnings.push(w);
12145
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12146
+ });
12101
12147
  if (files.length > 0) {
12102
12148
  const merged = [...agentCfg.schedule ?? []];
12103
12149
  for (const file of files) {
12150
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12151
+ if (raw === undefined)
12152
+ continue;
12104
12153
  try {
12105
- const raw = readFileSync(file, "utf-8");
12106
12154
  const parsed = import_yaml.parse(raw);
12107
12155
  const doc = OverlayDocSchema.parse(parsed);
12108
12156
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12137,13 +12185,25 @@ function applyAgentOverlays(config) {
12137
12185
  }
12138
12186
  try {
12139
12187
  const skillsDir = overlayDirFor(agentName, "skills.d");
12140
- const skillFiles = listYamlFiles(skillsDir);
12188
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12189
+ const w = {
12190
+ agent: agentName,
12191
+ file: skillsDir,
12192
+ reason: `read error: cannot list overlay directory (${code})`,
12193
+ code
12194
+ };
12195
+ recordReadFailure(agentCfg, { file: skillsDir, code });
12196
+ warnings.push(w);
12197
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
12198
+ });
12141
12199
  if (skillFiles.length === 0) {} else {
12142
12200
  const merged = [...agentCfg.skills ?? []];
12143
12201
  const seen = new Set(merged);
12144
12202
  for (const file of skillFiles) {
12203
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12204
+ if (raw === undefined)
12205
+ continue;
12145
12206
  try {
12146
- const raw = readFileSync(file, "utf-8");
12147
12207
  const parsed = import_yaml.parse(raw);
12148
12208
  const doc = OverlayDocSchema.parse(parsed);
12149
12209
  for (const skillName of doc.skills ?? []) {
@@ -12171,7 +12231,7 @@ function applyAgentOverlays(config) {
12171
12231
  }
12172
12232
  return { config, warnings };
12173
12233
  }
12174
- var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
12234
+ var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
12175
12235
  var init_overlay_loader = __esm(() => {
12176
12236
  init_zod();
12177
12237
  init_overlay_schema();
@@ -12179,6 +12239,7 @@ var init_overlay_loader = __esm(() => {
12179
12239
  import_yaml = __toESM(require_dist(), 1);
12180
12240
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12181
12241
  OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
12242
+ OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12182
12243
  });
12183
12244
 
12184
12245
  // src/config/merge.ts
@@ -21432,7 +21493,7 @@ function allocateAgentUid(name) {
21432
21493
  }
21433
21494
 
21434
21495
  // src/build-info.ts
21435
- var VERSION = "0.20.6";
21496
+ var VERSION = "0.20.8";
21436
21497
 
21437
21498
  // src/setup/hindsight-recall-tunables.ts
21438
21499
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -21708,7 +21769,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
21708
21769
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
21709
21770
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
21710
21771
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
21711
- "HINDSIGHT_API_TEMPORAL_LANGUAGES"
21772
+ "HINDSIGHT_API_TEMPORAL_LANGUAGES",
21773
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
21712
21774
  ]);
21713
21775
  var HINDSIGHT_WORKER_SLOT_TYPES = [
21714
21776
  "consolidation",
@@ -4884,7 +4884,7 @@ var init_schema = __esm(() => {
4884
4884
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4885
4885
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4886
4886
  }).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."),
4887
- 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).")
4887
+ 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).")
4888
4888
  });
4889
4889
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4890
4890
  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)."),
@@ -12844,6 +12844,20 @@ var init_overlay_schema = __esm(() => {
12844
12844
  // src/config/overlay-loader.ts
12845
12845
  import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
12846
12846
  import { basename, resolve as resolve3 } from "node:path";
12847
+ function recordReadFailure(agentCfg, failure) {
12848
+ const node = agentCfg;
12849
+ const existing = node[OVERLAY_READ_FAILURES];
12850
+ if (Array.isArray(existing)) {
12851
+ existing.push(failure);
12852
+ return;
12853
+ }
12854
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12855
+ value: [failure],
12856
+ enumerable: false,
12857
+ configurable: true,
12858
+ writable: false
12859
+ });
12860
+ }
12847
12861
  function deriveOverlayTitle(raw, fileName) {
12848
12862
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12849
12863
  if (titleFromComment)
@@ -12853,17 +12867,39 @@ function deriveOverlayTitle(raw, fileName) {
12853
12867
  return;
12854
12868
  return base.length > 0 ? base : undefined;
12855
12869
  }
12870
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12871
+ try {
12872
+ return readFileSync2(file, "utf-8");
12873
+ } catch (err) {
12874
+ const code = err.code;
12875
+ if (code === "ENOENT")
12876
+ return;
12877
+ const w = {
12878
+ agent: agentName,
12879
+ file,
12880
+ reason: `read error: ${err.message}`,
12881
+ code: code ?? "EUNKNOWN"
12882
+ };
12883
+ recordReadFailure(agentCfg, { file, code: w.code });
12884
+ warnings.push(w);
12885
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12886
+ return;
12887
+ }
12888
+ }
12856
12889
  function overlayDirFor(agentName, subdir) {
12857
12890
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12858
12891
  return resolve3(base);
12859
12892
  }
12860
- function listYamlFiles(dir) {
12893
+ function listYamlFiles(dir, onUnreadableDir) {
12861
12894
  if (!existsSync3(dir))
12862
12895
  return [];
12863
12896
  let entries;
12864
12897
  try {
12865
12898
  entries = readdirSync(dir);
12866
- } catch {
12899
+ } catch (err) {
12900
+ const code = err.code;
12901
+ if (code !== "ENOENT")
12902
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12867
12903
  return [];
12868
12904
  }
12869
12905
  const out = [];
@@ -12901,12 +12937,24 @@ function applyAgentOverlays(config) {
12901
12937
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12902
12938
  try {
12903
12939
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12904
- const files = listYamlFiles(scheduleDir);
12940
+ const files = listYamlFiles(scheduleDir, (code) => {
12941
+ const w = {
12942
+ agent: agentName,
12943
+ file: scheduleDir,
12944
+ reason: `read error: cannot list overlay directory (${code})`,
12945
+ code
12946
+ };
12947
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12948
+ warnings.push(w);
12949
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12950
+ });
12905
12951
  if (files.length > 0) {
12906
12952
  const merged = [...agentCfg.schedule ?? []];
12907
12953
  for (const file of files) {
12954
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12955
+ if (raw === undefined)
12956
+ continue;
12908
12957
  try {
12909
- const raw = readFileSync2(file, "utf-8");
12910
12958
  const parsed = import_yaml.parse(raw);
12911
12959
  const doc = OverlayDocSchema.parse(parsed);
12912
12960
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12941,13 +12989,25 @@ function applyAgentOverlays(config) {
12941
12989
  }
12942
12990
  try {
12943
12991
  const skillsDir = overlayDirFor(agentName, "skills.d");
12944
- const skillFiles = listYamlFiles(skillsDir);
12992
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12993
+ const w = {
12994
+ agent: agentName,
12995
+ file: skillsDir,
12996
+ reason: `read error: cannot list overlay directory (${code})`,
12997
+ code
12998
+ };
12999
+ recordReadFailure(agentCfg, { file: skillsDir, code });
13000
+ warnings.push(w);
13001
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
13002
+ });
12945
13003
  if (skillFiles.length === 0) {} else {
12946
13004
  const merged = [...agentCfg.skills ?? []];
12947
13005
  const seen = new Set(merged);
12948
13006
  for (const file of skillFiles) {
13007
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
13008
+ if (raw === undefined)
13009
+ continue;
12949
13010
  try {
12950
- const raw = readFileSync2(file, "utf-8");
12951
13011
  const parsed = import_yaml.parse(raw);
12952
13012
  const doc = OverlayDocSchema.parse(parsed);
12953
13013
  for (const skillName of doc.skills ?? []) {
@@ -12975,7 +13035,7 @@ function applyAgentOverlays(config) {
12975
13035
  }
12976
13036
  return { config, warnings };
12977
13037
  }
12978
- var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
13038
+ var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
12979
13039
  var init_overlay_loader = __esm(() => {
12980
13040
  init_zod();
12981
13041
  init_overlay_schema();
@@ -12983,6 +13043,7 @@ var init_overlay_loader = __esm(() => {
12983
13043
  import_yaml = __toESM(require_dist(), 1);
12984
13044
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12985
13045
  OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
13046
+ OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12986
13047
  });
12987
13048
 
12988
13049
  // src/config/notion-workspace-acl.ts
@@ -19603,7 +19664,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19603
19664
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
19604
19665
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
19605
19666
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
19606
- "HINDSIGHT_API_TEMPORAL_LANGUAGES"
19667
+ "HINDSIGHT_API_TEMPORAL_LANGUAGES",
19668
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
19607
19669
  ]);
19608
19670
  var HINDSIGHT_WORKER_SLOT_TYPES = [
19609
19671
  "consolidation",
@@ -4480,7 +4480,7 @@ var init_schema = __esm(() => {
4480
4480
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4481
4481
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4482
4482
  }).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."),
4483
- 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).")
4483
+ 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).")
4484
4484
  });
4485
4485
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4486
4486
  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)."),
@@ -12772,6 +12772,20 @@ var init_overlay_schema = __esm(() => {
12772
12772
  // src/config/overlay-loader.ts
12773
12773
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
12774
12774
  import { basename, resolve as resolve3 } from "node:path";
12775
+ function recordReadFailure(agentCfg, failure) {
12776
+ const node = agentCfg;
12777
+ const existing = node[OVERLAY_READ_FAILURES];
12778
+ if (Array.isArray(existing)) {
12779
+ existing.push(failure);
12780
+ return;
12781
+ }
12782
+ Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
12783
+ value: [failure],
12784
+ enumerable: false,
12785
+ configurable: true,
12786
+ writable: false
12787
+ });
12788
+ }
12775
12789
  function deriveOverlayTitle(raw, fileName) {
12776
12790
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
12777
12791
  if (titleFromComment)
@@ -12781,17 +12795,39 @@ function deriveOverlayTitle(raw, fileName) {
12781
12795
  return;
12782
12796
  return base.length > 0 ? base : undefined;
12783
12797
  }
12798
+ function readOverlayFile(agentName, file, agentCfg, warnings) {
12799
+ try {
12800
+ return readFileSync3(file, "utf-8");
12801
+ } catch (err) {
12802
+ const code = err.code;
12803
+ if (code === "ENOENT")
12804
+ return;
12805
+ const w = {
12806
+ agent: agentName,
12807
+ file,
12808
+ reason: `read error: ${err.message}`,
12809
+ code: code ?? "EUNKNOWN"
12810
+ };
12811
+ recordReadFailure(agentCfg, { file, code: w.code });
12812
+ warnings.push(w);
12813
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
12814
+ return;
12815
+ }
12816
+ }
12784
12817
  function overlayDirFor(agentName, subdir) {
12785
12818
  const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
12786
12819
  return resolve3(base);
12787
12820
  }
12788
- function listYamlFiles(dir) {
12821
+ function listYamlFiles(dir, onUnreadableDir) {
12789
12822
  if (!existsSync3(dir))
12790
12823
  return [];
12791
12824
  let entries;
12792
12825
  try {
12793
12826
  entries = readdirSync(dir);
12794
- } catch {
12827
+ } catch (err) {
12828
+ const code = err.code;
12829
+ if (code !== "ENOENT")
12830
+ onUnreadableDir?.(code ?? "EUNKNOWN");
12795
12831
  return [];
12796
12832
  }
12797
12833
  const out = [];
@@ -12829,12 +12865,24 @@ function applyAgentOverlays(config) {
12829
12865
  for (const [agentName, agentCfg] of Object.entries(agents)) {
12830
12866
  try {
12831
12867
  const scheduleDir = overlayDirFor(agentName, "schedule.d");
12832
- const files = listYamlFiles(scheduleDir);
12868
+ const files = listYamlFiles(scheduleDir, (code) => {
12869
+ const w = {
12870
+ agent: agentName,
12871
+ file: scheduleDir,
12872
+ reason: `read error: cannot list overlay directory (${code})`,
12873
+ code
12874
+ };
12875
+ recordReadFailure(agentCfg, { file: scheduleDir, code });
12876
+ warnings.push(w);
12877
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
12878
+ });
12833
12879
  if (files.length > 0) {
12834
12880
  const merged = [...agentCfg.schedule ?? []];
12835
12881
  for (const file of files) {
12882
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12883
+ if (raw === undefined)
12884
+ continue;
12836
12885
  try {
12837
- const raw = readFileSync3(file, "utf-8");
12838
12886
  const parsed = import_yaml.parse(raw);
12839
12887
  const doc = OverlayDocSchema.parse(parsed);
12840
12888
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12869,13 +12917,25 @@ function applyAgentOverlays(config) {
12869
12917
  }
12870
12918
  try {
12871
12919
  const skillsDir = overlayDirFor(agentName, "skills.d");
12872
- const skillFiles = listYamlFiles(skillsDir);
12920
+ const skillFiles = listYamlFiles(skillsDir, (code) => {
12921
+ const w = {
12922
+ agent: agentName,
12923
+ file: skillsDir,
12924
+ reason: `read error: cannot list overlay directory (${code})`,
12925
+ code
12926
+ };
12927
+ recordReadFailure(agentCfg, { file: skillsDir, code });
12928
+ warnings.push(w);
12929
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
12930
+ });
12873
12931
  if (skillFiles.length === 0) {} else {
12874
12932
  const merged = [...agentCfg.skills ?? []];
12875
12933
  const seen = new Set(merged);
12876
12934
  for (const file of skillFiles) {
12935
+ const raw = readOverlayFile(agentName, file, agentCfg, warnings);
12936
+ if (raw === undefined)
12937
+ continue;
12877
12938
  try {
12878
- const raw = readFileSync3(file, "utf-8");
12879
12939
  const parsed = import_yaml.parse(raw);
12880
12940
  const doc = OverlayDocSchema.parse(parsed);
12881
12941
  for (const skillName of doc.skills ?? []) {
@@ -12903,7 +12963,7 @@ function applyAgentOverlays(config) {
12903
12963
  }
12904
12964
  return { config, warnings };
12905
12965
  }
12906
- var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
12966
+ var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
12907
12967
  var init_overlay_loader = __esm(() => {
12908
12968
  init_zod();
12909
12969
  init_overlay_schema();
@@ -12911,6 +12971,7 @@ var init_overlay_loader = __esm(() => {
12911
12971
  import_yaml = __toESM(require_dist(), 1);
12912
12972
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
12913
12973
  OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
12974
+ OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
12914
12975
  });
12915
12976
 
12916
12977
  // src/config/notion-workspace-acl.ts
@@ -20016,7 +20077,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
20016
20077
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
20017
20078
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
20018
20079
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
20019
- "HINDSIGHT_API_TEMPORAL_LANGUAGES"
20080
+ "HINDSIGHT_API_TEMPORAL_LANGUAGES",
20081
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
20020
20082
  ]);
20021
20083
  var HINDSIGHT_WORKER_SLOT_TYPES = [
20022
20084
  "consolidation",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.20.6",
4
+ "version": "0.20.8",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -215,10 +215,26 @@ export function createIpcClient(options: IpcClientOptions): Promise<IpcClientHan
215
215
 
216
216
  function scheduleReconnect(): void {
217
217
  if (closed) return;
218
+ // Single-flight: a broken connection can reach here twice (the
219
+ // Bun.connect failure catch AND the socket close handler both fire
220
+ // for the same attempt). Two live timers mean two parallel
221
+ // doConnect() calls and a spare socket — observed in gateway logs
222
+ // as an instant anonymous connect+disconnect (agent=null) right
223
+ // before the real re-register.
224
+ if (reconnectTimer) return;
218
225
  log(`reconnecting in ${currentDelay}ms`);
219
226
  reconnectTimer = setTimeout(() => {
220
227
  reconnectTimer = null;
221
- if (!closed) doConnect();
228
+ if (!closed) {
229
+ // Swallow the rejection: doConnect's own catch has already
230
+ // logged the failure and scheduled the next retry. Without this,
231
+ // every failed BACKGROUND reconnect (e.g. the gateway restarting
232
+ // out from under a long-lived bridge) surfaced as a process-level
233
+ // unhandledRejection — "Error: Failed to connect" in
234
+ // bridge-crash.log — and pre-#3033 killed the bridge process
235
+ // outright (Claude Code never respawns a dead MCP server).
236
+ doConnect().catch(() => {});
237
+ }
222
238
  }, currentDelay);
223
239
  currentDelay = Math.min(currentDelay * 2, maxReconnectDelayMs);
224
240
  }
@@ -24692,11 +24692,14 @@ function createIpcClient(options) {
24692
24692
  function scheduleReconnect() {
24693
24693
  if (closed)
24694
24694
  return;
24695
+ if (reconnectTimer)
24696
+ return;
24695
24697
  log(`reconnecting in ${currentDelay}ms`);
24696
24698
  reconnectTimer = setTimeout(() => {
24697
24699
  reconnectTimer = null;
24698
- if (!closed)
24699
- doConnect();
24700
+ if (!closed) {
24701
+ doConnect().catch(() => {});
24702
+ }
24700
24703
  }, currentDelay);
24701
24704
  currentDelay = Math.min(currentDelay * 2, maxReconnectDelayMs);
24702
24705
  }