switchroom 0.20.17 → 0.20.19

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.
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.20.17", COMMIT_SHA = "75253052";
2123
+ var VERSION = "0.20.19", COMMIT_SHA = "552a2d21";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -14046,7 +14046,13 @@ var init_schema = __esm(() => {
14046
14046
  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."),
14047
14047
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
14048
14048
  }).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."),
14049
- 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); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE \u2014 the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); and " + "HINDSIGHT_API_BM25_MAX_QUERY_TERMS \u2014 the max number of query tokens the " + "keyword (BM25) recall arm searches for; unset means upstream's 0 = " + "unbounded, which makes a multi-KB consolidation query a disjunction " + "over hundreds of terms; set e.g. 64 to cap it, at the cost of " + "silently dropping the tail of a long query; 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).")
14049
+ recall_pool: exports_external.object({
14050
+ enabled: exports_external.boolean().optional().describe("Turn on the recall/background WORKER SPLIT: a second container, " + "`switchroom-hindsight-recall`, serves recall/reflect only with N " + "uvicorn workers, while the existing `switchroom-hindsight` " + "container keeps pg0, the single background WorkerPoller, the " + "healthcheck and all consolidation/retain processing but moves off " + "the public port onto public+1. The public port (memory.config.url) " + "does not change \u2014 the pool binds it instead. Absent/false (the " + "default) \u2192 the single-container topology, unchanged. Opt-in only. " + "Raises recall throughput ~2.2x on the fleet (2.6\u21925.6 rps)."),
14051
+ workers: exports_external.number().int().positive().optional().describe("uvicorn `--workers` for the recall pool (HINDSIGHT_API_WORKERS). " + "Default 4. Each worker holds its own GPU cross-encoder reranker, " + "so past ~4 workers contend for VRAM rather than adding throughput " + "on a single GPU \u2014 raise only on a bigger GPU or a CPU reranker. " + "The per-worker DB pools are auto-sized down from the pg0 " + "connection budget so N*(write+read)+60 background stays under " + "max_connections=250; a worker count that would overflow that " + "budget is rejected at launch."),
14052
+ db_pool_max_size: exports_external.number().int().positive().optional().describe("Override the per-worker WRITE pool cap " + "(HINDSIGHT_API_DB_POOL_MAX_SIZE). Absent \u2192 auto-sized from the " + "worker count. Set this only to override the auto-sizing; the " + "connection-budget check still applies and rejects an overflow."),
14053
+ read_db_pool_max_size: exports_external.number().int().positive().optional().describe("Override the per-worker READ pool cap " + "(HINDSIGHT_API_READ_DB_POOL_MAX_SIZE). Absent \u2192 auto-sized from " + "the worker count. Set this only to override the auto-sizing; the " + "connection-budget check still applies and rejects an overflow.")
14054
+ }).optional().describe("Optional recall/background container split for the hindsight " + "singleton. Off by default; see `enabled`. Provisioned by " + "`switchroom memory setup` (src/setup/hindsight-recall-pool.ts) so it " + "survives `switchroom apply` \u2014 the hand-applied topology it replaces " + "did not."),
14055
+ 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_QUERY_ANALYZER_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis (upstream #3154, which " + "superseded switchroom's own temporal-language image patch at v0.9.0); " + "comma-separated; switchroom EMITS `en` on every host because upstream's " + "unset default auto-detects across 200+ locales inline on the shared " + "asyncio loop (~165x slower per call), set e.g. `en,es` for 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); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE \u2014 the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); and " + "HINDSIGHT_API_BM25_MAX_QUERY_TERMS \u2014 the max number of query tokens the " + "keyword (BM25) recall arm searches for; unset means upstream's 0 = " + "unbounded, which makes a multi-KB consolidation query a disjunction " + "over hundreds of terms; set e.g. 64 to cap it, at the cost of " + "silently dropping the tail of a long query; and " + "HINDSIGHT_API_EMBEDDINGS_PROVIDER with the four " + "HINDSIGHT_API_EMBEDDINGS_ONNX_{MODEL_ID,POOLING,QUERY_PREFIX," + "PASSAGE_PREFIX} keys \u2014 the ONNX-embeddings cutover: unset keeps " + "upstream's slower `local` (PyTorch) backend, set `onnx` plus the model " + "id/pooling to run the SAME model on ONNX-CPU at a fraction of the " + "latency; the two _PREFIX keys are the query/passage affixes and are " + "set to the empty string for bge parity (vectors are byte-identical " + "between the two backends, so no re-embedding is needed) \u2014 unusually " + "for this block an EMPTY value here is honoured, not dropped, because an " + "affix-free prefix is the intended value; and the v0.9.0 knobs " + "switchroom allowlists for reach but emits no value for \u2014 " + "HINDSIGHT_API_RERANKER_MAX_CANDIDATES_{LOW,MID,HIGH} (per-recall-budget " + "rerank caps; unset/0 falls back to the global " + "HINDSIGHT_API_RERANKER_MAX_CANDIDATES switchroom does pin), " + "HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS (embedding-input truncation " + "ceiling; unset means the model's own limit), " + "HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD (entity-resolution fuzzy " + "match floor; unset means upstream's 0.15) and " + "HINDSIGHT_API_ENABLE_{TEMPORAL_RETRIEVAL,GRAPH_RETRIEVAL,RERANKING} " + "(global per-stage kill switches, all default true \u2014 the emergency lever " + "if one retrieval stage is implicated in a latency incident, at the cost " + "of recall quality); 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).")
14050
14056
  });
14051
14057
  MicrosoftWorkspaceConfigSchema = exports_external.object({
14052
14058
  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)."),
@@ -16136,7 +16142,13 @@ function resolveHindsightPerfOverrides(configEnv, processEnv = process.env) {
16136
16142
  const seenProcess = new Set;
16137
16143
  for (const key of HINDSIGHT_PERF_ENV_KEYS) {
16138
16144
  const fromProcess = processEnv[key];
16139
- if (typeof fromProcess === "string" && fromProcess.trim() !== "") {
16145
+ if (typeof fromProcess !== "string")
16146
+ continue;
16147
+ if (HINDSIGHT_PERF_ALLOW_EMPTY_KEYS.has(key)) {
16148
+ set(out, seenProcess, key, fromProcess);
16149
+ continue;
16150
+ }
16151
+ if (fromProcess.trim() !== "") {
16140
16152
  set(out, seenProcess, key, fromProcess.trim());
16141
16153
  }
16142
16154
  }
@@ -16145,6 +16157,10 @@ function resolveHindsightPerfOverrides(configEnv, processEnv = process.env) {
16145
16157
  for (const [key, raw] of configEntries) {
16146
16158
  if (!HINDSIGHT_PERF_ENV_KEYS.has(key))
16147
16159
  continue;
16160
+ if (HINDSIGHT_PERF_ALLOW_EMPTY_KEYS.has(key)) {
16161
+ set(out, seenConfig, key, String(raw));
16162
+ continue;
16163
+ }
16148
16164
  const value = String(raw).trim();
16149
16165
  if (value === "")
16150
16166
  continue;
@@ -16190,7 +16206,7 @@ function hindsightPerfEnv(caps, overrides = new Map) {
16190
16206
  function findUnmanagedHindsightEnvKeys(configEnv) {
16191
16207
  return Object.keys(configEnv ?? {}).filter((key) => !HINDSIGHT_PERF_ENV_KEYS.has(key) && !HINDSIGHT_PG_ENV_KEYS.has(key)).sort();
16192
16208
  }
16193
- var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150, HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE, HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT = 50, HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2, HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low", HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT, HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true", HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128, HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true", HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250, HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6, HINDSIGHT_DEFAULT_CONSOLIDATION_RESERVED_SLOTS = 1, HINDSIGHT_DEFAULT_GRAPH_SEED_MIN_SIMILARITY = 0.3, HINDSIGHT_DEFAULT_LLM_SUPPORTS_MAX_ITEMS = "false", HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential", HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30, HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION = "pg_search", HINDSIGHT_PERF_DEFAULTS_UNGATED, HINDSIGHT_PERF_DEFAULTS_GPU, HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM, HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS, HINDSIGHT_WORKER_SLOT_TYPES, HINDSIGHT_WORKER_RESERVED_SLOT_ALIASES, HINDSIGHT_PERF_ENV_KEYS;
16209
+ var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150, HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE, HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT = 50, HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2, HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low", HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT, HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true", HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128, HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true", HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 250, HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6, HINDSIGHT_DEFAULT_CONSOLIDATION_RESERVED_SLOTS = 1, HINDSIGHT_DEFAULT_GRAPH_SEED_MIN_SIMILARITY = 0.3, HINDSIGHT_DEFAULT_LLM_SUPPORTS_MAX_ITEMS = "false", HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential", HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30, HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION = "pg_search", HINDSIGHT_DEFAULT_QUERY_ANALYZER_LANGUAGES = "en", HINDSIGHT_PERF_DEFAULTS_UNGATED, HINDSIGHT_PERF_DEFAULTS_GPU, HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM, HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS, HINDSIGHT_PERF_ALLOW_EMPTY_KEYS, HINDSIGHT_WORKER_SLOT_TYPES, HINDSIGHT_WORKER_RESERVED_SLOT_ALIASES, HINDSIGHT_RECALL_POOL_ENV_KEYS, HINDSIGHT_PERF_ENV_KEYS;
16194
16210
  var init_hindsight_perf_defaults = __esm(() => {
16195
16211
  init_hindsight_pg_defaults();
16196
16212
  HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
@@ -16261,7 +16277,11 @@ var init_hindsight_perf_defaults = __esm(() => {
16261
16277
  "HINDSIGHT_API_GRAPH_SEED_MIN_SIMILARITY",
16262
16278
  String(HINDSIGHT_DEFAULT_GRAPH_SEED_MIN_SIMILARITY)
16263
16279
  ],
16264
- ["HINDSIGHT_API_TEXT_SEARCH_EXTENSION", HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION]
16280
+ ["HINDSIGHT_API_TEXT_SEARCH_EXTENSION", HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION],
16281
+ [
16282
+ "HINDSIGHT_API_QUERY_ANALYZER_LANGUAGES",
16283
+ HINDSIGHT_DEFAULT_QUERY_ANALYZER_LANGUAGES
16284
+ ]
16265
16285
  ];
16266
16286
  HINDSIGHT_PERF_DEFAULTS_GPU = [
16267
16287
  ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16],
@@ -16296,10 +16316,26 @@ var init_hindsight_perf_defaults = __esm(() => {
16296
16316
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
16297
16317
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
16298
16318
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
16299
- "HINDSIGHT_API_TEMPORAL_LANGUAGES",
16300
16319
  "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS",
16301
16320
  "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE",
16302
- "HINDSIGHT_API_BM25_MAX_QUERY_TERMS"
16321
+ "HINDSIGHT_API_BM25_MAX_QUERY_TERMS",
16322
+ "HINDSIGHT_API_EMBEDDINGS_PROVIDER",
16323
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID",
16324
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_POOLING",
16325
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX",
16326
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX",
16327
+ "HINDSIGHT_API_RERANKER_MAX_CANDIDATES_LOW",
16328
+ "HINDSIGHT_API_RERANKER_MAX_CANDIDATES_MID",
16329
+ "HINDSIGHT_API_RERANKER_MAX_CANDIDATES_HIGH",
16330
+ "HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS",
16331
+ "HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD",
16332
+ "HINDSIGHT_API_ENABLE_TEMPORAL_RETRIEVAL",
16333
+ "HINDSIGHT_API_ENABLE_GRAPH_RETRIEVAL",
16334
+ "HINDSIGHT_API_ENABLE_RERANKING"
16335
+ ]);
16336
+ HINDSIGHT_PERF_ALLOW_EMPTY_KEYS = new Set([
16337
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX",
16338
+ "HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX"
16303
16339
  ]);
16304
16340
  HINDSIGHT_WORKER_SLOT_TYPES = [
16305
16341
  "consolidation",
@@ -16313,6 +16349,15 @@ var init_hindsight_perf_defaults = __esm(() => {
16313
16349
  `HINDSIGHT_API_WORKER_${type.toUpperCase()}_MAX_SLOTS`,
16314
16350
  `HINDSIGHT_API_WORKER_${type.toUpperCase()}_RESERVED_SLOTS`
16315
16351
  ]));
16352
+ HINDSIGHT_RECALL_POOL_ENV_KEYS = [
16353
+ "HINDSIGHT_API_WORKERS",
16354
+ "HINDSIGHT_API_WORKER_ENABLED",
16355
+ "HINDSIGHT_API_DATABASE_URL",
16356
+ "HINDSIGHT_API_DB_URL",
16357
+ "HINDSIGHT_API_DB_POOL_MAX_SIZE",
16358
+ "HINDSIGHT_API_READ_DB_POOL_MAX_SIZE",
16359
+ "HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP"
16360
+ ];
16316
16361
  HINDSIGHT_PERF_ENV_KEYS = new Set([
16317
16362
  ...[
16318
16363
  ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
@@ -16321,7 +16366,8 @@ var init_hindsight_perf_defaults = __esm(() => {
16321
16366
  ].map(([k]) => k),
16322
16367
  ...HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS,
16323
16368
  ...HINDSIGHT_WORKER_RESERVED_SLOT_ALIASES.keys(),
16324
- ...HINDSIGHT_WORKER_RESERVED_SLOT_ALIASES.values()
16369
+ ...HINDSIGHT_WORKER_RESERVED_SLOT_ALIASES.values(),
16370
+ ...HINDSIGHT_RECALL_POOL_ENV_KEYS
16325
16371
  ]);
16326
16372
  });
16327
16373
 
@@ -16491,7 +16537,7 @@ function resolveCheckedHindsightContextBudget(llm) {
16491
16537
  assertHindsightContextBudgetFits(budget);
16492
16538
  return budget;
16493
16539
  }
16494
- var HINDSIGHT_CLAUDE_CONTEXT_WINDOW = 200000, HINDSIGHT_CONSERVATIVE_CONTEXT_WINDOW = 32768, HINDSIGHT_CONTEXT_SAFETY_FRACTION = 0.2, HINDSIGHT_CONSOLIDATION_PROMPT_OVERHEAD_TOKENS = 2270, HINDSIGHT_CONSOLIDATION_TOKENS_PER_FACT = 2500, HINDSIGHT_RETAIN_PROMPT_ESTIMATE_TOKENS = 8000, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_CEILING = 12, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_FLOOR = 1, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_RETAIN_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_FLOOR = 1024, HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR, HINDSIGHT_REFLECT_MAX_CONTEXT_FLOOR = 2048, HindsightContextBudgetError;
16540
+ var HINDSIGHT_CLAUDE_CONTEXT_WINDOW = 200000, HINDSIGHT_CONSERVATIVE_CONTEXT_WINDOW = 32768, HINDSIGHT_CONTEXT_SAFETY_FRACTION = 0.2, HINDSIGHT_CONSOLIDATION_PROMPT_OVERHEAD_TOKENS = 2270, HINDSIGHT_CONSOLIDATION_TOKENS_PER_FACT = 5000, HINDSIGHT_RETAIN_PROMPT_ESTIMATE_TOKENS = 8000, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_CEILING = 12, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_FLOOR = 1, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_RETAIN_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_FLOOR = 1024, HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR, HINDSIGHT_REFLECT_MAX_CONTEXT_FLOOR = 2048, HindsightContextBudgetError;
16495
16541
  var init_hindsight_context_budget = __esm(() => {
16496
16542
  HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR = HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE + 72;
16497
16543
  HindsightContextBudgetError = class HindsightContextBudgetError extends Error {
@@ -18697,20 +18743,16 @@ function probeDockerAvailability(probe = defaultDockerProbe) {
18697
18743
  function isDockerAvailable(probe = defaultDockerProbe) {
18698
18744
  return probeDockerAvailability(probe) === "ok";
18699
18745
  }
18746
+ function dockerNameMatchesExactly(probe, args, want) {
18747
+ const out = probe(args);
18748
+ return (out ?? "").split(`
18749
+ `).map((n) => n.trim()).some((n) => n === want);
18750
+ }
18700
18751
  function isHindsightRunning(probe = defaultDockerProbe) {
18701
- const out = probe(["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"]);
18702
- return (out ?? "").trim().length > 0;
18752
+ return dockerNameMatchesExactly(probe, ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], HINDSIGHT_CONTAINER_NAME);
18703
18753
  }
18704
18754
  function isHindsightContainerExists(probe = defaultDockerProbe) {
18705
- const out = probe([
18706
- "ps",
18707
- "-a",
18708
- "--filter",
18709
- "name=switchroom-hindsight",
18710
- "--format",
18711
- "{{.Names}}"
18712
- ]);
18713
- return (out ?? "").trim().length > 0;
18755
+ return dockerNameMatchesExactly(probe, ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], HINDSIGHT_CONTAINER_NAME);
18714
18756
  }
18715
18757
  function resolveHindsightPerOpLlm(llm) {
18716
18758
  const out = [];
@@ -19243,11 +19285,28 @@ function hindsightResolvedCapabilities(llm, litellm, gpu, perf) {
19243
19285
  localLlm: hindsightLocalLlmEnabled(llm, litellm, perf?.localLlm)
19244
19286
  };
19245
19287
  }
19246
- function getHindsightStatus() {
19288
+ function getHindsightStatus(exec = (cmd, args) => execFileSync2(cmd, args, { stdio: "pipe", encoding: "utf-8" })) {
19247
19289
  try {
19248
- const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
19249
- const status = output.trim();
19250
- return status.length > 0 ? status : null;
19290
+ const output = exec("docker", [
19291
+ "ps",
19292
+ "-a",
19293
+ "--filter",
19294
+ "name=switchroom-hindsight",
19295
+ "--format",
19296
+ "{{.Names}}\t{{.Status}}"
19297
+ ]);
19298
+ for (const line of output.split(`
19299
+ `)) {
19300
+ const tab = line.indexOf("\t");
19301
+ if (tab === -1)
19302
+ continue;
19303
+ if (line.slice(0, tab).trim() !== HINDSIGHT_CONTAINER_NAME)
19304
+ continue;
19305
+ const status = line.slice(tab + 1).trim();
19306
+ if (status.length > 0)
19307
+ return status;
19308
+ }
19309
+ return null;
19251
19310
  } catch {
19252
19311
  return null;
19253
19312
  }
@@ -19409,7 +19468,7 @@ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFA
19409
19468
  atomicWriteFileSync3(configPath, tail, mode);
19410
19469
  return { added: true, reason: "added" };
19411
19470
  }
19412
- var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_WORKER_ID = "switchroom-hindsight", HINDSIGHT_DATA_VOLUME = "switchroom-hindsight-data", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_DEFAULT_REQUEUE_DEAD_LETTERS = "1", HINDSIGHT_DEFAULT_REQUEUE_MAX = "25", HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384, HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6, HINDSIGHT_RETAIN_CLIENT_DEADLINE_S, HINDSIGHT_DEFAULT_MEM_LIMIT = "16g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, DOCKER_PROBE_TIMEOUT_MS, defaultDockerProbe = (args) => {
19471
+ var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_WORKER_ID = "switchroom-hindsight", HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_DATA_VOLUME = "switchroom-hindsight-data", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_DEFAULT_REQUEUE_DEAD_LETTERS = "1", HINDSIGHT_DEFAULT_REQUEUE_MAX = "25", HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384, HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6, HINDSIGHT_RETAIN_CLIENT_DEADLINE_S, HINDSIGHT_DEFAULT_MEM_LIMIT = "16g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, DOCKER_PROBE_TIMEOUT_MS, defaultDockerProbe = (args) => {
19413
19472
  try {
19414
19473
  return execFileSync2("docker", args, {
19415
19474
  stdio: "pipe",
@@ -32775,6 +32834,9 @@ var init_scaffold = __esm(() => {
32775
32834
  "mcp__hindsight__delete_document",
32776
32835
  "mcp__hindsight__get_mental_model",
32777
32836
  "mcp__hindsight__list_mental_models",
32837
+ "mcp__hindsight__search_knowledge_pages",
32838
+ "mcp__hindsight__get_knowledge_page",
32839
+ "mcp__hindsight__get_knowledge_tree",
32778
32840
  "mcp__hindsight__list_tags",
32779
32841
  "mcp__hindsight__get_operation",
32780
32842
  "mcp__hindsight__list_operations",
@@ -47545,7 +47607,7 @@ function recentUnextracted(docs, withinDays = 30, now = new Date, minTextLength
47545
47607
  var DOCUMENTS_PAGE_LIMIT = 500;
47546
47608
 
47547
47609
  // src/memory/hindsight-tools.ts
47548
- var HINDSIGHT_MIN_API_VERSION = "0.8.6", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
47610
+ var HINDSIGHT_MIN_API_VERSION = "0.9.0", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
47549
47611
  var init_hindsight_tools = __esm(() => {
47550
47612
  EXPECTED_HINDSIGHT_TOOLS = {
47551
47613
  recall: { required: ["query"] },
@@ -47588,7 +47650,7 @@ function buildRepairBankArgv(opts) {
47588
47650
  const script = `if command -v hindsight-admin >/dev/null 2>&1; then exec hindsight-admin "$@"; fi; ` + `exec ${HINDSIGHT_ADMIN_BIN} "$@"`;
47589
47651
  return [
47590
47652
  "exec",
47591
- opts.container ?? HINDSIGHT_CONTAINER_NAME,
47653
+ opts.container ?? HINDSIGHT_CONTAINER_NAME2,
47592
47654
  "sh",
47593
47655
  "-c",
47594
47656
  script,
@@ -47701,7 +47763,7 @@ function classifyRepairOutcome(run) {
47701
47763
  detail: run.dryRun ? `Coverage complete: ${run.summary.alreadyPresent} index(es) present across ` + `${run.summary.banksScanned} bank(s), nothing to create.` : `${run.summary.created} created, ${run.summary.alreadyPresent} already present ` + `across ${run.summary.banksScanned} bank(s). Re-run \`switchroom doctor\` to confirm.`
47702
47764
  };
47703
47765
  }
47704
- var HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
47766
+ var HINDSIGHT_CONTAINER_NAME2 = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
47705
47767
  var init_hindsight_repair = __esm(() => {
47706
47768
  init_hindsight_tools();
47707
47769
  });
@@ -83039,7 +83101,7 @@ __export(exports_server2, {
83039
83101
  TOOLS: () => TOOLS2
83040
83102
  });
83041
83103
  import { randomBytes as randomBytes18 } from "node:crypto";
83042
- import { existsSync as existsSync118 } from "node:fs";
83104
+ import { existsSync as existsSync119 } from "node:fs";
83043
83105
  function selfSocketPath() {
83044
83106
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
83045
83107
  }
@@ -83066,7 +83128,7 @@ async function dispatchTool2(name, args) {
83066
83128
  return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
83067
83129
  }
83068
83130
  const sockPath = selfSocketPath();
83069
- if (!existsSync118(sockPath)) {
83131
+ if (!existsSync119(sockPath)) {
83070
83132
  return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
83071
83133
  }
83072
83134
  let req;
@@ -83317,13 +83379,13 @@ function resolveAuditLogPath() {
83317
83379
  if (process.env.HOSTD_AUDIT_LOG_PATH)
83318
83380
  return process.env.HOSTD_AUDIT_LOG_PATH;
83319
83381
  const bindMounted = "/host-home/.switchroom/host-control-audit.log";
83320
- if (existsSync118(bindMounted))
83382
+ if (existsSync119(bindMounted))
83321
83383
  return bindMounted;
83322
83384
  return defaultAuditLogPath2();
83323
83385
  }
83324
83386
  function getLastUpdateApplyStatus() {
83325
83387
  const path10 = resolveAuditLogPath();
83326
- if (!existsSync118(path10)) {
83388
+ if (!existsSync119(path10)) {
83327
83389
  return errorText2(`get_status: audit log not found at ${path10}. No update_apply has run yet?`);
83328
83390
  }
83329
83391
  let raw;
@@ -83877,6 +83939,18 @@ var init_mapping = __esm(() => {
83877
83939
  job_spec: "keep-my-subscription-honest",
83878
83940
  signature: "litellm-header-passthrough:oauth-leak-scope"
83879
83941
  },
83942
+ "litellm-callback-mount-missing": {
83943
+ failure_mode: "constraint-violation",
83944
+ severity: 3,
83945
+ job_spec: "fleet-stays-healthy",
83946
+ signature: "litellm-callback-mount:missing-bind"
83947
+ },
83948
+ "litellm-passthrough-mount-stale": {
83949
+ failure_mode: "constraint-violation",
83950
+ severity: 3,
83951
+ job_spec: "fleet-stays-healthy",
83952
+ signature: "litellm-passthrough-mount:stale-python-version"
83953
+ },
83880
83954
  "litellm-timeout-budget-drift": {
83881
83955
  failure_mode: "drift",
83882
83956
  severity: 3,
@@ -84079,11 +84153,11 @@ var init_test_agents = __esm(() => {
84079
84153
  });
84080
84154
 
84081
84155
  // src/litellm/header-passthrough-guard.ts
84082
- import { existsSync as existsSync121, readdirSync as readdirSync46 } from "node:fs";
84156
+ import { existsSync as existsSync122, readdirSync as readdirSync46 } from "node:fs";
84083
84157
  function discoverLiveLitellmConfigPath(opts) {
84084
84158
  const servicesDir = opts?.servicesDir ?? COOLIFY_SERVICES_DIR;
84085
84159
  const readdir2 = opts?.readdirFn ?? ((p) => readdirSync46(p));
84086
- const exists = opts?.existsFn ?? existsSync121;
84160
+ const exists = opts?.existsFn ?? existsSync122;
84087
84161
  let entries;
84088
84162
  try {
84089
84163
  entries = readdir2(servicesDir);
@@ -84172,8 +84246,159 @@ var init_header_passthrough_guard = __esm(() => {
84172
84246
  ]);
84173
84247
  });
84174
84248
 
84249
+ // src/litellm/callback-mount-guard.ts
84250
+ function canReadComposeMounts(composeText) {
84251
+ if (composeText == null)
84252
+ return false;
84253
+ return extractComposeBindTargets(composeText) !== null;
84254
+ }
84255
+ function extractCustomCallbackModules(parsed) {
84256
+ const settings = parsed?.litellm_settings;
84257
+ if (settings == null || typeof settings !== "object")
84258
+ return [];
84259
+ const out = [];
84260
+ const seen = new Set;
84261
+ for (const key of CALLBACK_KEYS) {
84262
+ const raw = settings[key];
84263
+ if (!Array.isArray(raw))
84264
+ continue;
84265
+ for (const entry of raw) {
84266
+ if (typeof entry !== "string")
84267
+ continue;
84268
+ const reference = entry.trim();
84269
+ const parts = reference.split(".");
84270
+ if (parts.length !== 2)
84271
+ continue;
84272
+ const [module, attr] = parts;
84273
+ if (!module || !attr)
84274
+ continue;
84275
+ if (seen.has(module))
84276
+ continue;
84277
+ seen.add(module);
84278
+ out.push({ module, reference });
84279
+ }
84280
+ }
84281
+ return out;
84282
+ }
84283
+ function extractComposeBindTargets(composeText) {
84284
+ const targets = new Set;
84285
+ let doc2;
84286
+ try {
84287
+ doc2 = import_yaml28.parse(composeText);
84288
+ } catch {
84289
+ return null;
84290
+ }
84291
+ const services = doc2?.services;
84292
+ if (services == null || typeof services !== "object")
84293
+ return null;
84294
+ for (const svc of Object.values(services)) {
84295
+ const volumes = svc?.volumes;
84296
+ if (!Array.isArray(volumes))
84297
+ continue;
84298
+ for (const v of volumes) {
84299
+ if (typeof v === "string") {
84300
+ const parts = v.split(":");
84301
+ if (parts.length >= 2 && parts[1])
84302
+ targets.add(parts[1].trim());
84303
+ } else if (v != null && typeof v === "object") {
84304
+ const t = v.target;
84305
+ if (typeof t === "string" && t)
84306
+ targets.add(t.trim());
84307
+ }
84308
+ }
84309
+ }
84310
+ return targets;
84311
+ }
84312
+ function detectMissingCallbackMounts(parsedConfig, composeText) {
84313
+ const modules = extractCustomCallbackModules(parsedConfig);
84314
+ if (modules.length === 0 || composeText == null)
84315
+ return [];
84316
+ const targets = extractComposeBindTargets(composeText);
84317
+ if (targets === null)
84318
+ return [];
84319
+ const violations = [];
84320
+ for (const { module, reference } of modules) {
84321
+ const expectedTarget = `${CONTAINER_APP_DIR}/${module}.py`;
84322
+ if (targets.has(expectedTarget))
84323
+ continue;
84324
+ violations.push({
84325
+ module,
84326
+ reference,
84327
+ expectedTarget,
84328
+ detail: `litellm_settings names custom callback '${reference}' but no bind mount targets ` + `${expectedTarget} \u2014 the proxy will abort startup with ` + `ModuleNotFoundError: No module named '${module}'. Coolify regenerates this compose ` + `from services.docker_compose_raw in its database, so the mount must be restored THERE ` + `(a hand edit to the generated file is wiped on the next deploy).`
84329
+ });
84330
+ }
84331
+ return violations;
84332
+ }
84333
+ var import_yaml28, CONTAINER_APP_DIR = "/app", LIVE_COMPOSE_BASENAME = "docker-compose.yml", CALLBACK_KEYS;
84334
+ var init_callback_mount_guard = __esm(() => {
84335
+ import_yaml28 = __toESM(require_dist(), 1);
84336
+ CALLBACK_KEYS = ["callbacks", "success_callback", "failure_callback"];
84337
+ });
84338
+
84339
+ // src/litellm/passthrough-mount-guard.ts
84340
+ function extractVersionedSitePackagesMounts(composeText) {
84341
+ const targets = extractComposeBindTargets(composeText);
84342
+ if (targets === null)
84343
+ return null;
84344
+ const out = [];
84345
+ for (const target of targets) {
84346
+ const m = VERSIONED_SITE_PACKAGES_RE.exec(target);
84347
+ if (m)
84348
+ out.push({ target, declaredPythonVersion: m[1] });
84349
+ }
84350
+ return out;
84351
+ }
84352
+ function detectStalePassthroughMounts(composeText, actualPythonVersion) {
84353
+ if (composeText == null || actualPythonVersion == null)
84354
+ return [];
84355
+ const mounts = extractVersionedSitePackagesMounts(composeText);
84356
+ if (mounts === null)
84357
+ return [];
84358
+ const violations = [];
84359
+ for (const { target, declaredPythonVersion } of mounts) {
84360
+ if (declaredPythonVersion === actualPythonVersion)
84361
+ continue;
84362
+ violations.push({
84363
+ target,
84364
+ declaredPythonVersion,
84365
+ actualPythonVersion,
84366
+ detail: `bind mount targets ${target}, hard-coding CPython ${declaredPythonVersion}, but the live ` + `litellm image ships CPython ${actualPythonVersion} \u2014 its site-packages live under ` + `python${actualPythonVersion}/, so this mount lands at an inert path and the patch is ` + `SILENTLY dropped (no crash, no log; the proxy comes up "healthy" on unpatched code). ` + `Update the mount target to python${actualPythonVersion}/ in services.docker_compose_raw ` + `(a hand edit to the generated compose is wiped on the next Coolify deploy), then redeploy.`
84367
+ });
84368
+ }
84369
+ return violations;
84370
+ }
84371
+ var VERSIONED_SITE_PACKAGES_RE;
84372
+ var init_passthrough_mount_guard = __esm(() => {
84373
+ init_callback_mount_guard();
84374
+ VERSIONED_SITE_PACKAGES_RE = /\/python(\d+\.\d+)\/site-packages\//;
84375
+ });
84376
+
84175
84377
  // src/fleet-health/litellm-config-sensor.ts
84176
- import { readFileSync as readFileSync110, existsSync as existsSync122 } from "node:fs";
84378
+ import { execFileSync as execFileSync35 } from "node:child_process";
84379
+ import { readFileSync as readFileSync110, existsSync as existsSync123 } from "node:fs";
84380
+ import { dirname as dirname50, join as join124 } from "node:path";
84381
+ function resolveLiveLitellmPythonVersion() {
84382
+ try {
84383
+ const ps = execFileSync35("docker", ["ps", "--format", "{{.Names}}\t{{.Image}}"], {
84384
+ encoding: "utf-8",
84385
+ timeout: 5000,
84386
+ stdio: ["ignore", "pipe", "ignore"]
84387
+ });
84388
+ const line = ps.split(`
84389
+ `).find((l) => /litellm-database|(^|\s)litellm/i.test(l) && l.trim().length > 0);
84390
+ if (!line)
84391
+ return null;
84392
+ const name = line.split("\t")[0]?.trim();
84393
+ if (!name)
84394
+ return null;
84395
+ const lsOut = execFileSync35("docker", ["exec", name, "sh", "-c", "ls -d /app/.venv/lib/python*/ 2>/dev/null"], { encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
84396
+ const m = lsOut.match(/python(\d+\.\d+)/);
84397
+ return m ? m[1] : null;
84398
+ } catch {
84399
+ return null;
84400
+ }
84401
+ }
84177
84402
  function resolveLitellmConfigPath(explicit, discoverFn = () => discoverLiveLitellmConfigPath()) {
84178
84403
  if (explicit)
84179
84404
  return explicit;
@@ -84184,7 +84409,7 @@ function resolveLitellmConfigPath(explicit, discoverFn = () => discoverLiveLitel
84184
84409
  }
84185
84410
  function scanLitellmConfig(opts = {}) {
84186
84411
  const path10 = resolveLitellmConfigPath(opts.path, opts.discoverFn);
84187
- const exists = opts.existsFn ?? existsSync122;
84412
+ const exists = opts.existsFn ?? existsSync123;
84188
84413
  const read = opts.readFn ?? ((p) => readFileSync110(p, "utf-8"));
84189
84414
  const log = opts.log ?? (() => {});
84190
84415
  const nowIso = opts.nowIso ?? new Date().toISOString();
@@ -84236,6 +84461,52 @@ function scanLitellmConfig(opts = {}) {
84236
84461
  ts: nowIso
84237
84462
  });
84238
84463
  }
84464
+ const composePath = join124(dirname50(path10), LIVE_COMPOSE_BASENAME);
84465
+ let composeText = null;
84466
+ if (exists(composePath)) {
84467
+ try {
84468
+ composeText = read(composePath);
84469
+ } catch (e) {
84470
+ log(`fleet-health: litellm-config sensor \u2014 callback-mount check SKIPPED, ` + `${composePath} unreadable: ${String(e)}`);
84471
+ }
84472
+ } else {
84473
+ log(`fleet-health: litellm-config sensor \u2014 callback-mount check SKIPPED, ` + `no ${LIVE_COMPOSE_BASENAME} beside ${path10}`);
84474
+ }
84475
+ const missingMounts = detectMissingCallbackMounts(parsed, composeText);
84476
+ if (composeText !== null && !canReadComposeMounts(composeText)) {
84477
+ log(`fleet-health: litellm-config sensor \u2014 callback-mount check SKIPPED, ` + `${composePath} has no readable services/volumes mapping`);
84478
+ } else if (composeText !== null && missingMounts.length === 0) {
84479
+ log(`fleet-health: litellm-config sensor OK \u2014 every custom callback module is ` + `bind-mounted by ${composePath}`);
84480
+ }
84481
+ for (const m of missingMounts) {
84482
+ findings.push({
84483
+ signal: "litellm-callback-mount-missing",
84484
+ agent: LITELLM_PROXY_PSEUDO_AGENT,
84485
+ turn_id: `litellm-callback-mount:${m.module}`,
84486
+ log_pointer: `${composePath}: ${m.detail}`,
84487
+ ts: nowIso
84488
+ });
84489
+ }
84490
+ const versionedMounts = composeText !== null ? extractVersionedSitePackagesMounts(composeText) : null;
84491
+ const hasVersionedMounts = versionedMounts !== null && versionedMounts.length > 0;
84492
+ const pythonVersionFn = opts.pythonVersionFn ?? resolveLiveLitellmPythonVersion;
84493
+ const actualPythonVersion = hasVersionedMounts ? pythonVersionFn() : null;
84494
+ if (hasVersionedMounts && actualPythonVersion === null) {
84495
+ log(`fleet-health: litellm-config sensor \u2014 passthrough-mount check SKIPPED, ` + `could not resolve the live litellm image's CPython version (docker down, no ` + `container, or off-host CI/dev)`);
84496
+ }
84497
+ const staleMounts = detectStalePassthroughMounts(composeText, actualPythonVersion);
84498
+ if (hasVersionedMounts && actualPythonVersion !== null && staleMounts.length === 0) {
84499
+ log(`fleet-health: litellm-config sensor OK \u2014 every versioned site-packages shadow ` + `mount in ${composePath} matches the live image's CPython ${actualPythonVersion}`);
84500
+ }
84501
+ for (const s of staleMounts) {
84502
+ findings.push({
84503
+ signal: "litellm-passthrough-mount-stale",
84504
+ agent: LITELLM_PROXY_PSEUDO_AGENT,
84505
+ turn_id: `litellm-passthrough-mount:${s.declaredPythonVersion}->${s.actualPythonVersion}`,
84506
+ log_pointer: `${composePath}: ${s.detail}`,
84507
+ ts: nowIso
84508
+ });
84509
+ }
84239
84510
  if (findings.length === 0)
84240
84511
  return { status: "ok", path: path10, findings: [] };
84241
84512
  for (const f of findings) {
@@ -84247,6 +84518,8 @@ var LITELLM_PROXY_PSEUDO_AGENT = "litellm-proxy";
84247
84518
  var init_litellm_config_sensor = __esm(() => {
84248
84519
  init_header_passthrough_guard();
84249
84520
  init_timeout_budget();
84521
+ init_callback_mount_guard();
84522
+ init_passthrough_mount_guard();
84250
84523
  });
84251
84524
 
84252
84525
  // src/fleet-health/hindsight-gpu-drift-sensor.ts
@@ -84293,18 +84566,18 @@ __export(exports_scan, {
84293
84566
  import {
84294
84567
  readFileSync as readFileSync111,
84295
84568
  readdirSync as readdirSync47,
84296
- existsSync as existsSync123,
84569
+ existsSync as existsSync124,
84297
84570
  mkdirSync as mkdirSync70,
84298
84571
  writeFileSync as writeFileSync51
84299
84572
  } from "node:fs";
84300
- import { resolve as resolve64, dirname as dirname50 } from "node:path";
84573
+ import { resolve as resolve64, dirname as dirname51 } from "node:path";
84301
84574
  import { homedir as homedir67 } from "node:os";
84302
84575
  function resolveSwitchroomBase(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir67()) {
84303
84576
  return resolve64(home2, ".switchroom");
84304
84577
  }
84305
84578
  function listAgents(base) {
84306
84579
  const dir = resolve64(base, "agents");
84307
- if (!existsSync123(dir))
84580
+ if (!existsSync124(dir))
84308
84581
  return [];
84309
84582
  try {
84310
84583
  return readdirSync47(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
@@ -84333,7 +84606,7 @@ function runScan(opts = {}) {
84333
84606
  let gwText = "";
84334
84607
  let sawArtifact = false;
84335
84608
  try {
84336
- if (existsSync123(turnsPath)) {
84609
+ if (existsSync124(turnsPath)) {
84337
84610
  turnsText = readFileSync111(turnsPath, "utf-8");
84338
84611
  sawArtifact = true;
84339
84612
  }
@@ -84341,7 +84614,7 @@ function runScan(opts = {}) {
84341
84614
  log(`fleet-health: WARN skipping ${agent} turns.jsonl unreadable: ${String(e)}`);
84342
84615
  }
84343
84616
  try {
84344
- if (existsSync123(gwPath)) {
84617
+ if (existsSync124(gwPath)) {
84345
84618
  gwText = readFileSync111(gwPath, "utf-8");
84346
84619
  sawArtifact = true;
84347
84620
  }
@@ -84386,7 +84659,7 @@ function runScan(opts = {}) {
84386
84659
  function readLedgerIfPresent(base) {
84387
84660
  const path10 = ledgerPathForBase(base);
84388
84661
  try {
84389
- if (!existsSync123(path10))
84662
+ if (!existsSync124(path10))
84390
84663
  return null;
84391
84664
  return JSON.parse(readFileSync111(path10, "utf-8"));
84392
84665
  } catch {
@@ -84394,11 +84667,11 @@ function readLedgerIfPresent(base) {
84394
84667
  }
84395
84668
  }
84396
84669
  function ledgerPathForBase(base) {
84397
- return fleetHealthLedgerPath(dirname50(base));
84670
+ return fleetHealthLedgerPath(dirname51(base));
84398
84671
  }
84399
84672
  function writeLedger(base, ledger) {
84400
84673
  const path10 = ledgerPathForBase(base);
84401
- mkdirSync70(dirname50(path10), { recursive: true });
84674
+ mkdirSync70(dirname51(path10), { recursive: true });
84402
84675
  writeFileSync51(path10, JSON.stringify(ledger, null, 2) + `
84403
84676
  `, "utf-8");
84404
84677
  return path10;
@@ -106709,6 +106982,323 @@ function redactMemoryWriteBody(body) {
106709
106982
  init_helpers();
106710
106983
  init_hindsight();
106711
106984
 
106985
+ // src/setup/hindsight-recall-pool.ts
106986
+ init_hindsight();
106987
+ init_hindsight_perf_defaults();
106988
+ import { execFileSync as execFileSync26 } from "node:child_process";
106989
+ var HINDSIGHT_RECALL_POOL_CONTAINER = "switchroom-hindsight-recall";
106990
+ var HINDSIGHT_RECALL_POOL_WORKER_ID = "switchroom-hindsight-recall";
106991
+ var HINDSIGHT_PG_MAX_CONNECTIONS = 250;
106992
+ var HINDSIGHT_BACKGROUND_DB_POOL_MAX_SIZE = 40;
106993
+ var HINDSIGHT_BACKGROUND_READ_DB_POOL_MAX_SIZE = 20;
106994
+ var HINDSIGHT_BACKGROUND_CONNECTION_BUDGET = HINDSIGHT_BACKGROUND_DB_POOL_MAX_SIZE + HINDSIGHT_BACKGROUND_READ_DB_POOL_MAX_SIZE;
106995
+ var HINDSIGHT_PG_SUPERUSER_RESERVED_CONNECTIONS = 3;
106996
+ var HINDSIGHT_RECALL_POOL_HEADROOM = 10;
106997
+ if (HINDSIGHT_RECALL_POOL_HEADROOM < HINDSIGHT_PG_SUPERUSER_RESERVED_CONNECTIONS) {
106998
+ throw new Error(`HINDSIGHT_RECALL_POOL_HEADROOM (${HINDSIGHT_RECALL_POOL_HEADROOM}) must be \u2265 ` + `HINDSIGHT_PG_SUPERUSER_RESERVED_CONNECTIONS ` + `(${HINDSIGHT_PG_SUPERUSER_RESERVED_CONNECTIONS}).`);
106999
+ }
107000
+ var HINDSIGHT_RECALL_POOL_MAX_TOTAL_CONNECTIONS = HINDSIGHT_PG_MAX_CONNECTIONS - HINDSIGHT_RECALL_POOL_HEADROOM;
107001
+ var HINDSIGHT_RECALL_POOL_CONNECTION_BUDGET = HINDSIGHT_RECALL_POOL_MAX_TOTAL_CONNECTIONS - HINDSIGHT_BACKGROUND_CONNECTION_BUDGET;
107002
+ var HINDSIGHT_RECALL_POOL_DEFAULT_WORKERS = 4;
107003
+ var HINDSIGHT_RECALL_POOL_PG_DSN = "postgresql://hindsight:hindsight@127.0.0.1:5432/hindsight";
107004
+ function hindsightBackgroundApiPort(publicApiPort) {
107005
+ return publicApiPort + 1;
107006
+ }
107007
+ function resolveHindsightLaunchPorts(input) {
107008
+ const publicApiPort = input.configUrlPort ?? input.reuseApiPort ?? input.defaultApiPort;
107009
+ const apiPort = input.recallEnabled ? hindsightBackgroundApiPort(publicApiPort) : publicApiPort;
107010
+ return { publicApiPort, basePorts: { apiPort, uiPort: input.uiPort } };
107011
+ }
107012
+ function publicPortIsUnserved(input) {
107013
+ if (input.configUrlPort == null || input.authorityApiPort == null)
107014
+ return false;
107015
+ if (input.poolRunning)
107016
+ return false;
107017
+ return input.authorityApiPort !== input.configUrlPort;
107018
+ }
107019
+ function planStaleRecallPoolConvergence(input) {
107020
+ if (input.recallEnabled)
107021
+ return { action: "none" };
107022
+ if (!input.poolRunning)
107023
+ return { action: "none" };
107024
+ if (input.configUrlPort == null || input.authorityApiPort == null) {
107025
+ return { action: "none" };
107026
+ }
107027
+ if (input.authorityApiPort === input.configUrlPort)
107028
+ return { action: "stop-pool" };
107029
+ return { action: "relaunch-single-on-public", publicApiPort: input.configUrlPort };
107030
+ }
107031
+ function recallPoolDbPoolSizing(workers) {
107032
+ const perWorker = Math.floor(HINDSIGHT_RECALL_POOL_CONNECTION_BUDGET / workers);
107033
+ const dbPoolMaxSize = Math.max(1, Math.floor(perWorker * 0.4));
107034
+ const readDbPoolMaxSize = Math.max(1, perWorker - dbPoolMaxSize);
107035
+ return { dbPoolMaxSize, readDbPoolMaxSize };
107036
+ }
107037
+ function assertRecallPoolConnectionBudget(cfg) {
107038
+ const poolDemand = cfg.workers * (cfg.dbPoolMaxSize + cfg.readDbPoolMaxSize);
107039
+ const total = poolDemand + HINDSIGHT_BACKGROUND_CONNECTION_BUDGET;
107040
+ if (total > HINDSIGHT_RECALL_POOL_MAX_TOTAL_CONNECTIONS) {
107041
+ throw new Error(`hindsight.recall_pool connection budget overflow: ${cfg.workers} workers \u00d7 ` + `(${cfg.dbPoolMaxSize} write + ${cfg.readDbPoolMaxSize} read) = ${poolDemand}, ` + `plus ${HINDSIGHT_BACKGROUND_CONNECTION_BUDGET} background = ${total} > the ` + `${HINDSIGHT_RECALL_POOL_MAX_TOTAL_CONNECTIONS}-connection ceiling ` + `(pg0 max_connections=${HINDSIGHT_PG_MAX_CONNECTIONS} \u2212 ` + `${HINDSIGHT_RECALL_POOL_HEADROOM} headroom, which reserves PG's ` + `${HINDSIGHT_PG_SUPERUSER_RESERVED_CONNECTIONS} superuser slots plus ` + `maintenance). Lower hindsight.recall_pool.workers or its ` + `db_pool_max_size/read_db_pool_max_size.`);
107042
+ }
107043
+ }
107044
+ function resolveRecallPoolConfig(input) {
107045
+ if (!input || input.enabled !== true)
107046
+ return null;
107047
+ const workers = input.workers && input.workers > 0 ? Math.floor(input.workers) : HINDSIGHT_RECALL_POOL_DEFAULT_WORKERS;
107048
+ const autoSized = recallPoolDbPoolSizing(workers);
107049
+ const cfg = {
107050
+ workers,
107051
+ dbPoolMaxSize: input.db_pool_max_size && input.db_pool_max_size > 0 ? Math.floor(input.db_pool_max_size) : autoSized.dbPoolMaxSize,
107052
+ readDbPoolMaxSize: input.read_db_pool_max_size && input.read_db_pool_max_size > 0 ? Math.floor(input.read_db_pool_max_size) : autoSized.readDbPoolMaxSize
107053
+ };
107054
+ assertRecallPoolConnectionBudget(cfg);
107055
+ return cfg;
107056
+ }
107057
+ function backgroundContainerPoolEnv() {
107058
+ return {
107059
+ HINDSIGHT_API_DB_POOL_MAX_SIZE: String(HINDSIGHT_BACKGROUND_DB_POOL_MAX_SIZE),
107060
+ HINDSIGHT_API_READ_DB_POOL_MAX_SIZE: String(HINDSIGHT_BACKGROUND_READ_DB_POOL_MAX_SIZE)
107061
+ };
107062
+ }
107063
+ var HINDSIGHT_SPLIT_MANAGED_DB_POOL_KEYS = [
107064
+ "HINDSIGHT_API_DB_POOL_MAX_SIZE",
107065
+ "HINDSIGHT_API_READ_DB_POOL_MAX_SIZE"
107066
+ ];
107067
+ function splitDbPoolEnvConflicts(operatorEnv, cfg) {
107068
+ if (!operatorEnv)
107069
+ return [];
107070
+ const authority = backgroundContainerPoolEnv();
107071
+ const pool = {
107072
+ HINDSIGHT_API_DB_POOL_MAX_SIZE: String(cfg.dbPoolMaxSize),
107073
+ HINDSIGHT_API_READ_DB_POOL_MAX_SIZE: String(cfg.readDbPoolMaxSize)
107074
+ };
107075
+ const out = [];
107076
+ for (const key of HINDSIGHT_SPLIT_MANAGED_DB_POOL_KEYS) {
107077
+ const operatorValue = operatorEnv[key];
107078
+ if (operatorValue === undefined)
107079
+ continue;
107080
+ out.push({
107081
+ key,
107082
+ operatorValue: String(operatorValue),
107083
+ authorityValue: authority[key],
107084
+ poolValue: pool[key]
107085
+ });
107086
+ }
107087
+ return out;
107088
+ }
107089
+ function splitDbPoolEnvConflictWarning(c) {
107090
+ const knob = c.key === "HINDSIGHT_API_DB_POOL_MAX_SIZE" ? "hindsight.recall_pool.db_pool_max_size" : "hindsight.recall_pool.read_db_pool_max_size";
107091
+ return `hindsight.env.${c.key}=${c.operatorValue} is IGNORED while ` + `hindsight.recall_pool.enabled is true: the split computes this cap for both ` + `containers (authority ${c.authorityValue}, recall pool ${c.poolValue}) so the ` + `pg0 connection budget stays provable. Set \`${knob}\` instead \u2014 it is budget-checked \u2014 ` + `or set hindsight.recall_pool.enabled: false to have hindsight.env win again.`;
107092
+ }
107093
+ function splitContainerPerfEnv(operatorEnv) {
107094
+ return { ...operatorEnv ?? {}, ...backgroundContainerPoolEnv() };
107095
+ }
107096
+ function recallPoolEnvOverrides(cfg, poolPort) {
107097
+ return [
107098
+ ["HINDSIGHT_API_HOST", HINDSIGHT_HOST_NETWORK_BIND_ADDR],
107099
+ ["HINDSIGHT_API_PORT", String(poolPort)],
107100
+ ["HINDSIGHT_API_DATABASE_URL", HINDSIGHT_RECALL_POOL_PG_DSN],
107101
+ ["HINDSIGHT_API_DB_URL", HINDSIGHT_RECALL_POOL_PG_DSN],
107102
+ ["HINDSIGHT_API_WORKERS", String(cfg.workers)],
107103
+ ["HINDSIGHT_API_WORKER_ENABLED", "false"],
107104
+ ["HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP", "false"],
107105
+ ["HINDSIGHT_ENABLE_CP", "false"],
107106
+ ["HINDSIGHT_API_WORKER_ID", HINDSIGHT_RECALL_POOL_WORKER_ID],
107107
+ ["HINDSIGHT_API_DB_POOL_MAX_SIZE", String(cfg.dbPoolMaxSize)],
107108
+ ["HINDSIGHT_API_READ_DB_POOL_MAX_SIZE", String(cfg.readDbPoolMaxSize)]
107109
+ ];
107110
+ }
107111
+ function applyRecallPoolEnvOverrides(base, overrides) {
107112
+ const overrideMap = new Map(overrides);
107113
+ const seen = new Set;
107114
+ const out = [];
107115
+ for (const [k, v] of base) {
107116
+ if (overrideMap.has(k)) {
107117
+ out.push([k, overrideMap.get(k)]);
107118
+ seen.add(k);
107119
+ } else {
107120
+ out.push([k, v]);
107121
+ }
107122
+ }
107123
+ for (const [k, v] of overrides) {
107124
+ if (!seen.has(k))
107125
+ out.push([k, v]);
107126
+ }
107127
+ return out;
107128
+ }
107129
+ function hindsightRecallPoolEnvPairs(opts) {
107130
+ const { cfg, poolPort, litellm, llm, gpu, perf, cpAccessKey } = opts;
107131
+ const base = hindsightContainerEnvPairs({
107132
+ apiPort: poolPort,
107133
+ litellm,
107134
+ llm,
107135
+ gpu,
107136
+ perf,
107137
+ cpAccessKey
107138
+ });
107139
+ return applyRecallPoolEnvOverrides(base, recallPoolEnvOverrides(cfg, poolPort));
107140
+ }
107141
+ function startHindsightRecallPool(opts) {
107142
+ const {
107143
+ cfg,
107144
+ poolPort,
107145
+ imageTag,
107146
+ litellm,
107147
+ llm,
107148
+ gpu,
107149
+ perf,
107150
+ cpAccessKey,
107151
+ mirrorDir,
107152
+ exec = (cmd, args2) => {
107153
+ execFileSync26(cmd, args2, { stdio: "pipe" });
107154
+ }
107155
+ } = opts;
107156
+ assertRecallPoolConnectionBudget(cfg);
107157
+ const envArgs = hindsightRecallPoolEnvPairs({
107158
+ cfg,
107159
+ poolPort,
107160
+ litellm,
107161
+ llm,
107162
+ gpu,
107163
+ perf,
107164
+ cpAccessKey
107165
+ }).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
107166
+ const probe2 = pickHindsightLiteLlmProbeUrl(llm, litellm);
107167
+ const healthCmd = probe2 ? buildLiteLlmAwareHealthCmd(poolPort, probe2) : HINDSIGHT_HEALTHCHECK_CMD.replace("localhost:8888", `localhost:${poolPort}`);
107168
+ const args = [
107169
+ "run",
107170
+ "-d",
107171
+ "--name",
107172
+ HINDSIGHT_RECALL_POOL_CONTAINER,
107173
+ "--restart",
107174
+ "always",
107175
+ "--shm-size",
107176
+ HINDSIGHT_DEFAULT_SHM_SIZE,
107177
+ ...hindsightGpuEnabled(gpu) ? ["--gpus", "all"] : [],
107178
+ "--network",
107179
+ "host",
107180
+ "--user",
107181
+ "hindsight",
107182
+ "--health-cmd",
107183
+ healthCmd,
107184
+ "--health-interval",
107185
+ "30s",
107186
+ "--health-timeout",
107187
+ "5s",
107188
+ "--health-retries",
107189
+ "3",
107190
+ "--health-start-period",
107191
+ "60s",
107192
+ ...mirrorDir ? ["-v", `${HINDSIGHT_CREDS_MIRROR_VOLUME}:${HINDSIGHT_CRED_DIR}`] : ["--tmpfs", `${HINDSIGHT_CRED_DIR}:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`],
107193
+ "-v",
107194
+ `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
107195
+ ...envArgs,
107196
+ hindsightImageRef(imageTag)
107197
+ ];
107198
+ exec("docker", args);
107199
+ }
107200
+ function stopHindsightRecallPool(exec = (cmd, args) => {
107201
+ execFileSync26(cmd, args, { stdio: "pipe" });
107202
+ }) {
107203
+ try {
107204
+ exec("docker", ["update", "--restart=no", HINDSIGHT_RECALL_POOL_CONTAINER]);
107205
+ } catch {}
107206
+ try {
107207
+ exec("docker", ["stop", HINDSIGHT_RECALL_POOL_CONTAINER]);
107208
+ } catch {}
107209
+ try {
107210
+ exec("docker", ["rm", "-f", HINDSIGHT_RECALL_POOL_CONTAINER]);
107211
+ } catch {}
107212
+ }
107213
+ function isHindsightRecallPoolRunning(probe2 = defaultDockerProbe) {
107214
+ return dockerNameMatchesExactly(probe2, ["ps", "--filter", `name=${HINDSIGHT_RECALL_POOL_CONTAINER}`, "--format", "{{.Names}}"], HINDSIGHT_RECALL_POOL_CONTAINER);
107215
+ }
107216
+ var HINDSIGHT_HEALTH_MIN_TIMEOUT_MS = 150000;
107217
+ var HINDSIGHT_HEALTH_PHASE_BUDGET_MS = {
107218
+ apiStartup: 60000,
107219
+ pgFirstBoot: 90000,
107220
+ migrations: 90000,
107221
+ perExtraWorker: 15000
107222
+ };
107223
+ function hindsightHealthTimeoutMs(input) {
107224
+ const p = HINDSIGHT_HEALTH_PHASE_BUDGET_MS;
107225
+ let budget;
107226
+ switch (input.role) {
107227
+ case "authority":
107228
+ case "single-container":
107229
+ budget = p.apiStartup + p.pgFirstBoot + p.migrations;
107230
+ break;
107231
+ case "recall-pool": {
107232
+ const workers = input.workers && input.workers > 0 ? input.workers : 1;
107233
+ budget = p.apiStartup + (workers - 1) * p.perExtraWorker;
107234
+ break;
107235
+ }
107236
+ }
107237
+ return Math.max(HINDSIGHT_HEALTH_MIN_TIMEOUT_MS, budget);
107238
+ }
107239
+ async function waitForHindsightHealthy(port, opts = {}) {
107240
+ const {
107241
+ timeoutMs = opts.role ? hindsightHealthTimeoutMs({ role: opts.role, workers: opts.workers }) : HINDSIGHT_HEALTH_MIN_TIMEOUT_MS,
107242
+ intervalMs = 3000,
107243
+ fetchImpl = fetch,
107244
+ sleep: sleep4 = (ms) => new Promise((r) => setTimeout(r, ms)),
107245
+ now: now2 = () => Date.now()
107246
+ } = opts;
107247
+ const deadline = now2() + timeoutMs;
107248
+ for (;; ) {
107249
+ try {
107250
+ const res = await fetchImpl(`http://127.0.0.1:${port}/health`, {
107251
+ signal: AbortSignal.timeout(4000)
107252
+ });
107253
+ if (res.ok)
107254
+ return true;
107255
+ } catch {}
107256
+ if (now2() >= deadline)
107257
+ return false;
107258
+ await sleep4(intervalMs);
107259
+ }
107260
+ }
107261
+ async function launchRecallPoolHealthGated(opts) {
107262
+ const log = opts.log ?? (() => {});
107263
+ log(` Waiting for the authority container to become healthy on ` + `${opts.authorityApiPort} (pg0 startup)...`);
107264
+ if (!await opts.waitForHealthy(opts.authorityApiPort, "authority")) {
107265
+ return { outcome: "authority-unhealthy" };
107266
+ }
107267
+ opts.stopPool();
107268
+ log(` Starting recall pool (${opts.workers} workers) on ${opts.publicApiPort}...`);
107269
+ opts.startPool();
107270
+ if (await opts.waitForHealthy(opts.publicApiPort, "recall-pool")) {
107271
+ return { outcome: "split" };
107272
+ }
107273
+ const reason = `the recall pool did not become healthy on ${opts.publicApiPort}`;
107274
+ log(` Recall pool unhealthy on ${opts.publicApiPort}; degrading to a single ` + `container on the public port so memory.config.url stays served...`);
107275
+ opts.degradeToSingleContainer();
107276
+ if (await opts.waitForHealthy(opts.publicApiPort, "single-container")) {
107277
+ return { outcome: "degraded-single-container", reason };
107278
+ }
107279
+ return { outcome: "degrade-failed", reason };
107280
+ }
107281
+ async function convergeStaleRecallPool(opts) {
107282
+ const log = opts.log ?? (() => {});
107283
+ switch (opts.plan.action) {
107284
+ case "none":
107285
+ return { outcome: "nothing-to-do" };
107286
+ case "stop-pool":
107287
+ log(" hindsight.recall_pool is disabled but a recall pool container is running; " + "the authority already serves the public port, so removing the orphan pool...");
107288
+ opts.stopPool();
107289
+ return { outcome: "pool-stopped" };
107290
+ case "relaunch-single-on-public": {
107291
+ const publicApiPort = opts.plan.publicApiPort;
107292
+ log(` hindsight.recall_pool is disabled but a recall pool container still holds the ` + `public port ${publicApiPort}; relaunching a single container on it to match the ` + `declared topology...`);
107293
+ opts.relaunchSingleOnPublic(publicApiPort);
107294
+ if (await opts.waitForHealthy(publicApiPort, "single-container")) {
107295
+ return { outcome: "relaunched-single", publicApiPort };
107296
+ }
107297
+ return { outcome: "relaunch-failed", publicApiPort };
107298
+ }
107299
+ }
107300
+ }
107301
+
106712
107302
  // src/setup/hindsight-gpu-guard.ts
106713
107303
  init_hindsight();
106714
107304
  function assessHindsightGpuDrop(args) {
@@ -107086,6 +107676,7 @@ Cross-agent reflection plan
107086
107676
  return;
107087
107677
  }
107088
107678
  console.log(source_default.gray(" Stopping switchroom-hindsight..."));
107679
+ stopHindsightRecallPool();
107089
107680
  stopHindsight();
107090
107681
  console.log(source_default.green(" Hindsight container stopped and removed."));
107091
107682
  return;
@@ -107141,6 +107732,182 @@ Cross-agent reflection plan
107141
107732
  }
107142
107733
  const reusePorts = recreate ? getRunningHindsightPorts() : null;
107143
107734
  if (isHindsightRunning() && !recreate) {
107735
+ let noRecreatePoolCfg = null;
107736
+ try {
107737
+ noRecreatePoolCfg = resolveRecallPoolConfig(getConfig(program3).hindsight?.recall_pool);
107738
+ } catch {
107739
+ noRecreatePoolCfg = null;
107740
+ }
107741
+ if (noRecreatePoolCfg && !isHindsightRecallPoolRunning()) {
107742
+ console.log(source_default.gray(`
107743
+ Authority container is up but the recall pool is not \u2014 restoring the pool...`));
107744
+ const repairConfig = getConfig(program3);
107745
+ const repairGpu = hindsightGpuDecision(resolveHindsightGpuOverride({ flag: opts.gpu, config: repairConfig.hindsight?.gpu }));
107746
+ const repairConfigUrlPort = (() => {
107747
+ try {
107748
+ const p = Number(new URL(repairConfig.memory?.config?.url ?? "").port);
107749
+ return Number.isInteger(p) && p > 0 ? p : undefined;
107750
+ } catch {
107751
+ return;
107752
+ }
107753
+ })();
107754
+ const { publicApiPort: repairPublicPort, basePorts: repairBasePorts } = resolveHindsightLaunchPorts({
107755
+ recallEnabled: true,
107756
+ configUrlPort: repairConfigUrlPort,
107757
+ reuseApiPort: undefined,
107758
+ uiPort: 0,
107759
+ defaultApiPort: HINDSIGHT_DEFAULT_API_PORT
107760
+ });
107761
+ try {
107762
+ const { litellm: repairLitellm } = await resolveLiteLLMForHindsight(repairConfig);
107763
+ const repairLlm = await getResolvedLlm();
107764
+ const repairCpAccessKey = await resolveHindsightCpAccessKey(repairConfig);
107765
+ for (const conflict of splitDbPoolEnvConflicts(repairConfig.hindsight?.env, noRecreatePoolCfg)) {
107766
+ console.log(source_default.yellow(` ! ${splitDbPoolEnvConflictWarning(conflict)}`));
107767
+ }
107768
+ const repairAuthorityEnv = splitContainerPerfEnv(repairConfig.hindsight?.env);
107769
+ const repairResult = await launchRecallPoolHealthGated({
107770
+ publicApiPort: repairPublicPort,
107771
+ authorityApiPort: repairBasePorts.apiPort,
107772
+ workers: noRecreatePoolCfg.workers,
107773
+ waitForHealthy: (p, role) => waitForHindsightHealthy(p, { role, workers: noRecreatePoolCfg.workers }),
107774
+ stopPool: () => stopHindsightRecallPool(),
107775
+ startPool: () => startHindsightRecallPool({
107776
+ cfg: noRecreatePoolCfg,
107777
+ poolPort: repairPublicPort,
107778
+ imageTag: effectiveTag,
107779
+ litellm: repairLitellm,
107780
+ llm: repairLlm,
107781
+ gpu: repairGpu.enabled,
107782
+ perf: {
107783
+ env: repairAuthorityEnv,
107784
+ memLimit: repairConfig.hindsight?.mem_limit
107785
+ },
107786
+ cpAccessKey: repairCpAccessKey,
107787
+ mirrorDir: hindsightConsumerMirrorDir(repairConfig)
107788
+ }),
107789
+ degradeToSingleContainer: () => {
107790
+ const uiPort2 = getRunningHindsightPorts()?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
107791
+ stopHindsightRecallPool();
107792
+ stopHindsight();
107793
+ startHindsight({ apiPort: repairPublicPort, uiPort: uiPort2 }, repairLitellm, effectiveTag, repairLlm, hindsightConsumerMirrorDir(repairConfig), repairGpu.enabled, { env: repairConfig.hindsight?.env, memLimit: repairConfig.hindsight?.mem_limit }, repairCpAccessKey);
107794
+ },
107795
+ log: (m) => console.log(source_default.gray(m))
107796
+ });
107797
+ switch (repairResult.outcome) {
107798
+ case "split":
107799
+ console.log(source_default.green(`
107800
+ Recall pool restored on ${repairPublicPort} ` + `(authority on ${repairBasePorts.apiPort}).
107801
+ `));
107802
+ break;
107803
+ case "degraded-single-container":
107804
+ console.log(source_default.yellow(`
107805
+ ! Recall pool failed (${repairResult.reason}); DEGRADED to a single ` + `container on the public port ${repairPublicPort}. memory.config.url IS ` + `served, but the recall split is OFF \u2014 inspect \`docker logs ` + `${HINDSIGHT_RECALL_POOL_CONTAINER}\` and re-run \`switchroom memory setup ` + `--recreate\` once fixed, or set hindsight.recall_pool.enabled: false.
107806
+ `));
107807
+ break;
107808
+ case "authority-unhealthy":
107809
+ console.error(source_default.red(`
107810
+ The running authority container is not answering /health on ` + `${repairBasePorts.apiPort}; cannot restore the pool. Inspect ` + `\`docker logs switchroom-hindsight\` and re-run \`switchroom memory setup ` + `--recreate\`.
107811
+ `));
107812
+ process.exit(1);
107813
+ break;
107814
+ case "degrade-failed":
107815
+ console.error(source_default.red(`
107816
+ Recall pool restore failed AND the single-container fallback could not ` + `bind the public port ${repairPublicPort}. Inspect \`docker logs ` + `switchroom-hindsight\` and \`docker logs ${HINDSIGHT_RECALL_POOL_CONTAINER}\`, ` + `then re-run \`switchroom memory setup --recreate\`.
107817
+ `));
107818
+ process.exit(1);
107819
+ break;
107820
+ }
107821
+ } catch (err) {
107822
+ console.error(source_default.red(`
107823
+ Failed to restore the recall pool: ${err.message}
107824
+ `));
107825
+ process.exit(1);
107826
+ }
107827
+ const repairUrl = `http://127.0.0.1:${repairPublicPort}/mcp/`;
107828
+ try {
107829
+ if (persistMemoryConfigUrl(getConfigPath(program3), "claude-code", repairUrl)) {
107830
+ console.log(source_default.gray(` Updated ${getConfigPath(program3)} with memory.config.url = ${repairUrl}`));
107831
+ }
107832
+ } catch (err) {
107833
+ console.error(source_default.yellow(` Note: could not auto-update switchroom.yaml: ${err.message}`));
107834
+ }
107835
+ return;
107836
+ }
107837
+ const livePorts = getRunningHindsightPorts();
107838
+ const liveApiPort = livePorts?.apiPort;
107839
+ const liveConfigUrlPort = (() => {
107840
+ try {
107841
+ const p = Number(new URL(getConfig(program3).memory?.config?.url ?? "").port);
107842
+ return Number.isInteger(p) && p > 0 ? p : undefined;
107843
+ } catch {
107844
+ return;
107845
+ }
107846
+ })();
107847
+ const stalePlan = planStaleRecallPoolConvergence({
107848
+ recallEnabled: noRecreatePoolCfg !== null,
107849
+ poolRunning: isHindsightRecallPoolRunning(),
107850
+ authorityApiPort: liveApiPort,
107851
+ configUrlPort: liveConfigUrlPort
107852
+ });
107853
+ if (stalePlan.action !== "none") {
107854
+ const staleConfig = getConfig(program3);
107855
+ const staleGpu = hindsightGpuDecision(resolveHindsightGpuOverride({ flag: opts.gpu, config: staleConfig.hindsight?.gpu }));
107856
+ try {
107857
+ const needsSecrets = stalePlan.action === "relaunch-single-on-public";
107858
+ const staleLitellm = needsSecrets ? (await resolveLiteLLMForHindsight(staleConfig)).litellm : undefined;
107859
+ const staleLlm = needsSecrets ? await getResolvedLlm() : undefined;
107860
+ const staleCpAccessKey = needsSecrets ? await resolveHindsightCpAccessKey(staleConfig) : undefined;
107861
+ const staleResult = await convergeStaleRecallPool({
107862
+ plan: stalePlan,
107863
+ stopPool: () => stopHindsightRecallPool(),
107864
+ relaunchSingleOnPublic: (publicApiPort2) => {
107865
+ stopHindsightRecallPool();
107866
+ stopHindsight();
107867
+ startHindsight({ apiPort: publicApiPort2, uiPort: livePorts?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT }, staleLitellm, effectiveTag, staleLlm, hindsightConsumerMirrorDir(staleConfig), staleGpu.enabled, { env: staleConfig.hindsight?.env, memLimit: staleConfig.hindsight?.mem_limit }, staleCpAccessKey);
107868
+ },
107869
+ waitForHealthy: (p, role) => waitForHindsightHealthy(p, { role }),
107870
+ log: (m) => console.log(source_default.gray(m))
107871
+ });
107872
+ switch (staleResult.outcome) {
107873
+ case "pool-stopped":
107874
+ console.log(source_default.green(`
107875
+ Removed the orphan recall pool (hindsight.recall_pool is disabled).
107876
+ `));
107877
+ break;
107878
+ case "relaunched-single":
107879
+ console.log(source_default.green(`
107880
+ Converged to the declared single-container topology on ` + `${staleResult.publicApiPort}.
107881
+ `));
107882
+ break;
107883
+ case "relaunch-failed":
107884
+ console.error(source_default.red(`
107885
+ hindsight.recall_pool is disabled, but the single container relaunched ` + `on the public port ${staleResult.publicApiPort} did not become healthy \u2014 ` + `nothing is serving fleet memory. Inspect \`docker logs ` + `switchroom-hindsight --tail 50\`, then re-run \`switchroom memory setup ` + `--recreate\`.
107886
+ `));
107887
+ process.exit(1);
107888
+ break;
107889
+ case "nothing-to-do":
107890
+ break;
107891
+ }
107892
+ } catch (err) {
107893
+ console.error(source_default.red(`
107894
+ Failed to converge the stale recall pool: ${err.message}
107895
+ `));
107896
+ process.exit(1);
107897
+ }
107898
+ return;
107899
+ }
107900
+ if (publicPortIsUnserved({
107901
+ configUrlPort: liveConfigUrlPort,
107902
+ authorityApiPort: liveApiPort,
107903
+ poolRunning: isHindsightRecallPoolRunning()
107904
+ })) {
107905
+ console.error(source_default.red(`
107906
+ switchroom-hindsight is running on port ${liveApiPort}, but ` + `memory.config.url points at ${liveConfigUrlPort} and nothing is serving ` + `it \u2014 fleet memory is DOWN.
107907
+ ` + ` Run \`switchroom memory setup --recreate\` to rebind it to ` + `${liveConfigUrlPort}, or set hindsight.recall_pool.enabled: true to restore ` + `the recall pool on the public port.
107908
+ `));
107909
+ process.exit(1);
107910
+ }
107144
107911
  console.log(source_default.green(`
107145
107912
  Hindsight container is already running (switchroom-hindsight).
107146
107913
  `));
@@ -107223,31 +107990,24 @@ Cross-agent reflection plan
107223
107990
  process.exit(1);
107224
107991
  }
107225
107992
  }
107993
+ stopHindsightRecallPool();
107226
107994
  if (isHindsightContainerExists()) {
107227
107995
  console.log(source_default.gray(" Removing existing switchroom-hindsight container..."));
107228
107996
  stopHindsight();
107229
107997
  }
107230
- let ports;
107998
+ let recallPoolCfg;
107999
+ try {
108000
+ recallPoolCfg = resolveRecallPoolConfig(getConfig(program3).hindsight?.recall_pool);
108001
+ } catch (err) {
108002
+ console.log(source_default.yellow(` ! Ignoring hindsight.recall_pool (${err.message}); ` + `starting the single-container topology.`));
108003
+ recallPoolCfg = null;
108004
+ }
108005
+ let uiPort;
107231
108006
  if (reusePorts) {
107232
- ports = reusePorts;
107233
- if (reusePorts.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
107234
- let explicitUrl;
107235
- try {
107236
- explicitUrl = getConfig(program3).memory?.config?.url;
107237
- } catch {
107238
- explicitUrl = undefined;
107239
- }
107240
- if (!explicitUrl) {
107241
- console.log(source_default.yellow(`
107242
- \u26a0 MIGRATION HAZARD: reusing stale host port ${reusePorts.apiPort} ` + `for switchroom-hindsight, but scaffolding now defaults to ` + `${HINDSIGHT_DEFAULT_API_PORT} and no explicit memory.config.url is set.`));
107243
- console.log(source_default.yellow(` Agents may be repointed to a dead URL (http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/) ` + `while the container listens on ${reusePorts.apiPort}.`));
107244
- console.log(source_default.yellow(` Fix: set memory.config.url: http://127.0.0.1:${reusePorts.apiPort}/mcp/ explicitly, ` + `or migrate the container to port ${HINDSIGHT_DEFAULT_API_PORT}.
107245
- `));
107246
- }
107247
- }
108007
+ uiPort = reusePorts.uiPort;
107248
108008
  } else {
107249
108009
  try {
107250
- ports = await pickHindsightPorts();
108010
+ uiPort = (await pickHindsightPorts()).uiPort;
107251
108011
  } catch (err) {
107252
108012
  console.error(source_default.red(`
107253
108013
  ${err.message}
@@ -107255,31 +108015,39 @@ Cross-agent reflection plan
107255
108015
  process.exit(1);
107256
108016
  }
107257
108017
  }
107258
- if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
107259
- console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; ` + `using ${ports.apiPort}/${ports.uiPort} instead.`));
107260
- }
107261
- let conflict = await preflightHindsightPorts(ports);
107262
- if (conflict) {
107263
- const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
107264
- console.log(source_default.yellow(`
107265
- Chosen Hindsight port ${conflict.port} is occupied${heldBy}; ` + `selecting a free port instead of crash-looping.`));
108018
+ const configUrlPort = (() => {
107266
108019
  try {
107267
- ports = await pickHindsightPorts();
107268
- } catch (err) {
107269
- console.error(source_default.red(`
107270
- ${err.message}
107271
- `));
107272
- process.exit(1);
108020
+ const p = Number(new URL(getConfig(program3).memory?.config?.url ?? "").port);
108021
+ return Number.isInteger(p) && p > 0 ? p : undefined;
108022
+ } catch {
108023
+ return;
107273
108024
  }
107274
- conflict = await preflightHindsightPorts(ports);
108025
+ })();
108026
+ const { publicApiPort, basePorts } = resolveHindsightLaunchPorts({
108027
+ recallEnabled: recallPoolCfg != null,
108028
+ configUrlPort,
108029
+ reuseApiPort: reusePorts?.apiPort,
108030
+ uiPort,
108031
+ defaultApiPort: HINDSIGHT_DEFAULT_API_PORT
108032
+ });
108033
+ if (!recallPoolCfg && configUrlPort == null && reusePorts && reusePorts.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
108034
+ console.log(source_default.yellow(`
108035
+ Note: reusing host port ${reusePorts.apiPort} for switchroom-hindsight ` + `(scaffolding default is ${HINDSIGHT_DEFAULT_API_PORT}); memory.config.url will be ` + `pinned to ${reusePorts.apiPort} so agents follow the live port. Migrate deliberately ` + `if you want ${HINDSIGHT_DEFAULT_API_PORT}.
108036
+ `));
108037
+ }
108038
+ if (recallPoolCfg) {
108039
+ console.log(source_default.gray(` recall_pool enabled: authority (pg0 + background) on ${basePorts.apiPort}, ` + `${recallPoolCfg.workers}-worker recall pool on ${publicApiPort}.`));
108040
+ }
108041
+ const portsToBind = recallPoolCfg ? [publicApiPort, basePorts.apiPort, basePorts.uiPort] : [basePorts.apiPort, basePorts.uiPort];
108042
+ for (const p of portsToBind) {
108043
+ const conflict = await preflightHindsightPorts({ apiPort: p, uiPort: p });
107275
108044
  if (conflict) {
107276
- const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
108045
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
107277
108046
  console.error(source_default.red(`
107278
- Refusing to start Hindsight: port ${conflict.port} is still ` + `occupied${stillHeldBy} after reassignment. Free it and retry ` + `\`switchroom memory --start\`.
108047
+ Refusing to start Hindsight: port ${conflict.port} is occupied${heldBy}. ` + `Free it and retry \`switchroom memory setup --recreate\`.
107279
108048
  `));
107280
108049
  process.exit(1);
107281
108050
  }
107282
- console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
107283
108051
  }
107284
108052
  if (opts.provider && opts.provider !== "claude-code") {
107285
108053
  console.log(source_default.gray(` Note: --provider=${opts.provider} ignored. switchroom-hindsight is pinned to the ` + "`claude-code` provider (subscription-honest). The flag is kept for back-compat."));
@@ -107299,23 +108067,83 @@ Cross-agent reflection plan
107299
108067
  for (const drop of await diffDroppedHindsightLlmVaultKeys(hindsightConfig.hindsight?.llm, resolvedLlm)) {
107300
108068
  console.log(source_default.yellow(` ! ${hindsightLlmDroppedKeyWarning(drop)}`));
107301
108069
  }
107302
- startHindsight(ports, litellmCfg, effectiveTag, resolvedLlm, hindsightConsumerMirrorDir(hindsightConfig), gpuDecision.enabled, {
107303
- env: hindsightConfig.hindsight?.env,
108070
+ if (recallPoolCfg) {
108071
+ for (const conflict of splitDbPoolEnvConflicts(hindsightConfig.hindsight?.env, recallPoolCfg)) {
108072
+ console.log(source_default.yellow(` ! ${splitDbPoolEnvConflictWarning(conflict)}`));
108073
+ }
108074
+ }
108075
+ const authorityEnv = recallPoolCfg ? splitContainerPerfEnv(hindsightConfig.hindsight?.env) : hindsightConfig.hindsight?.env;
108076
+ startHindsight(basePorts, litellmCfg, effectiveTag, resolvedLlm, hindsightConsumerMirrorDir(hindsightConfig), gpuDecision.enabled, {
108077
+ env: authorityEnv,
107304
108078
  memLimit: hindsightConfig.hindsight?.mem_limit
107305
108079
  }, cpAccessKey);
107306
108080
  if (litellmCfg) {
107307
108081
  console.log(source_default.gray(" LiteLLM routing enabled for hindsight (--network host)."));
107308
108082
  }
107309
- console.log(source_default.green(`
107310
- Hindsight container started (switchroom-hindsight) on port ${ports.apiPort}.
108083
+ if (recallPoolCfg) {
108084
+ const launchResult = await launchRecallPoolHealthGated({
108085
+ publicApiPort,
108086
+ authorityApiPort: basePorts.apiPort,
108087
+ workers: recallPoolCfg.workers,
108088
+ waitForHealthy: (p, role) => waitForHindsightHealthy(p, { role, workers: recallPoolCfg.workers }),
108089
+ stopPool: () => stopHindsightRecallPool(),
108090
+ startPool: () => startHindsightRecallPool({
108091
+ cfg: recallPoolCfg,
108092
+ poolPort: publicApiPort,
108093
+ imageTag: effectiveTag,
108094
+ litellm: litellmCfg,
108095
+ llm: resolvedLlm,
108096
+ gpu: gpuDecision.enabled,
108097
+ perf: {
108098
+ env: authorityEnv,
108099
+ memLimit: hindsightConfig.hindsight?.mem_limit
108100
+ },
108101
+ cpAccessKey,
108102
+ mirrorDir: hindsightConsumerMirrorDir(hindsightConfig)
108103
+ }),
108104
+ degradeToSingleContainer: () => {
108105
+ stopHindsightRecallPool();
108106
+ stopHindsight();
108107
+ startHindsight({ apiPort: publicApiPort, uiPort: basePorts.uiPort }, litellmCfg, effectiveTag, resolvedLlm, hindsightConsumerMirrorDir(hindsightConfig), gpuDecision.enabled, { env: hindsightConfig.hindsight?.env, memLimit: hindsightConfig.hindsight?.mem_limit }, cpAccessKey);
108108
+ },
108109
+ log: (m) => console.log(source_default.gray(m))
108110
+ });
108111
+ switch (launchResult.outcome) {
108112
+ case "split":
108113
+ console.log(source_default.green(`
108114
+ Hindsight recall split started: authority (pg0 + background) on ` + `${basePorts.apiPort}, ${recallPoolCfg.workers}-worker recall pool on ${publicApiPort}.
108115
+ `));
108116
+ break;
108117
+ case "degraded-single-container":
108118
+ console.log(source_default.yellow(`
108119
+ ! Recall pool failed (${launchResult.reason}); DEGRADED to a single ` + `container on the public port ${publicApiPort}. memory.config.url IS served, ` + `but the recall split is OFF \u2014 inspect \`docker logs ` + `${HINDSIGHT_RECALL_POOL_CONTAINER}\` and re-run \`switchroom memory setup ` + `--recreate\` once fixed, or set hindsight.recall_pool.enabled: false.
108120
+ `));
108121
+ break;
108122
+ case "authority-unhealthy":
108123
+ console.error(source_default.red(`
108124
+ Authority container did not become healthy on ${basePorts.apiPort}. ` + `The recall pool was NOT started and no single container could share its ` + `pg0. Investigate \`docker logs switchroom-hindsight\` and re-run ` + `\`switchroom memory setup --recreate\`.
108125
+ `));
108126
+ process.exit(1);
108127
+ break;
108128
+ case "degrade-failed":
108129
+ console.error(source_default.red(`
108130
+ Recall pool did not become healthy on ${publicApiPort} AND the ` + `single-container fallback also failed to bind the public port. Inspect ` + `\`docker logs switchroom-hindsight\` and \`docker logs ` + `${HINDSIGHT_RECALL_POOL_CONTAINER}\`, then re-run \`switchroom memory setup ` + `--recreate\`.
108131
+ `));
108132
+ process.exit(1);
108133
+ break;
108134
+ }
108135
+ } else {
108136
+ console.log(source_default.green(`
108137
+ Hindsight container started (switchroom-hindsight) on port ${basePorts.apiPort}.
107311
108138
  `));
108139
+ }
107312
108140
  } catch (err) {
107313
108141
  console.error(source_default.red(`
107314
108142
  Failed to start Hindsight: ${err.message}
107315
108143
  `));
107316
108144
  process.exit(1);
107317
108145
  }
107318
- const url = `http://127.0.0.1:${ports.apiPort}/mcp/`;
108146
+ const url = `http://127.0.0.1:${publicApiPort}/mcp/`;
107319
108147
  const configPath = getConfigPath(program3);
107320
108148
  try {
107321
108149
  if (persistMemoryConfigUrl(configPath, provider, url)) {
@@ -107371,7 +108199,7 @@ Cross-agent reflection plan
107371
108199
  }
107372
108200
  console.log();
107373
108201
  }));
107374
- memory.command("repair").description("Verify and rebuild a bank's per-(bank, fact_type) vector index coverage (hindsight >= 0.8.5) \u2014 the fix for a restored/upgraded bank whose recall silently under-returns").option("--bank <id>", "Bank id to repair (mutually exclusive with --all)").option("--agent <name>", "Repair the bank belonging to this agent (resolves the bank id for you)").option("--all", "Repair every bank in the base schema and all discovered tenant schemas").option("--schema <name>", "Limit to a single schema (default: base + discovered tenant schemas)").option("--dry-run", "Report what would be repaired without creating or dropping any index").option("--container <name>", `Hindsight container name (default: ${HINDSIGHT_CONTAINER_NAME})`).action(withConfigError(async (opts) => {
108202
+ memory.command("repair").description("Verify and rebuild a bank's per-(bank, fact_type) vector index coverage (hindsight >= 0.8.5) \u2014 the fix for a restored/upgraded bank whose recall silently under-returns").option("--bank <id>", "Bank id to repair (mutually exclusive with --all)").option("--agent <name>", "Repair the bank belonging to this agent (resolves the bank id for you)").option("--all", "Repair every bank in the base schema and all discovered tenant schemas").option("--schema <name>", "Limit to a single schema (default: base + discovered tenant schemas)").option("--dry-run", "Report what would be repaired without creating or dropping any index").option("--container <name>", `Hindsight container name (default: ${HINDSIGHT_CONTAINER_NAME2})`).action(withConfigError(async (opts) => {
107375
108203
  const config = getConfig(program3);
107376
108204
  if (opts.agent && opts.bank) {
107377
108205
  console.error(source_default.red("Pass either --agent or --bank, not both."));
@@ -112013,8 +112841,8 @@ init_embedded_examples();
112013
112841
  init_atomic();
112014
112842
  init_loader();
112015
112843
  init_scaffold();
112016
- import { existsSync as existsSync85, readFileSync as readFileSync79, mkdirSync as mkdirSync47, statSync as statSync47, writeFileSync as writeFileSync33 } from "node:fs";
112017
- import { execFileSync as execFileSync26 } from "node:child_process";
112844
+ import { existsSync as existsSync86, readFileSync as readFileSync79, mkdirSync as mkdirSync47, statSync as statSync47, writeFileSync as writeFileSync33 } from "node:fs";
112845
+ import { execFileSync as execFileSync27 } from "node:child_process";
112018
112846
  import { resolve as resolve46, dirname as dirname36 } from "node:path";
112019
112847
  init_state();
112020
112848
  init_vault();
@@ -112022,7 +112850,6 @@ init_manager();
112022
112850
  init_onboarding();
112023
112851
  init_gpu_detect();
112024
112852
  init_host_capabilities();
112025
- init_hindsight();
112026
112853
  init_posthog();
112027
112854
 
112028
112855
  // src/cli/setup-posture-rewrite.ts
@@ -112413,6 +113240,9 @@ class SetupVerificationError extends Error {
112413
113240
  init_lifecycle();
112414
113241
  init_timezone();
112415
113242
  init_schema();
113243
+
113244
+ // src/cli/setup-step-ui.ts
113245
+ init_source();
112416
113246
  var STEP_PENDING = source_default.gray("\u25cb");
112417
113247
  var STEP_ACTIVE = source_default.blue("->");
112418
113248
  var STEP_DONE = source_default.green("OK");
@@ -112423,6 +113253,315 @@ function stepHeaderTo(log, num3, title, status) {
112423
113253
  log(`
112424
113254
  ${status} ${source_default.bold(`Step ${num3}:`)} ${title}`);
112425
113255
  }
113256
+
113257
+ // src/cli/setup-memory-backend.ts
113258
+ init_source();
113259
+ init_loader();
113260
+ init_hindsight();
113261
+ import { existsSync as existsSync85 } from "node:fs";
113262
+ var HINDSIGHT_READY_RETRIES = 5;
113263
+ var HINDSIGHT_READY_INTERVAL_MS = 1000;
113264
+ async function resolveLiteLLMForHindsight2(config, deps = {}) {
113265
+ return resolveHindsightLiteLlm(config, deps);
113266
+ }
113267
+ function isMemoryBackendDisabled(config) {
113268
+ const memoryBackend = config.memory?.backend ?? "hindsight";
113269
+ return process.env.SWITCHROOM_MEMORY_BACKEND === "none" || memoryBackend === "none";
113270
+ }
113271
+ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, deps = {}) {
113272
+ stepHeader(6, "Memory backend", STEP_ACTIVE);
113273
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
113274
+ const pickPorts = deps.pickPorts ?? pickHindsightPorts;
113275
+ const preflightPorts = deps.preflightPorts ?? preflightHindsightPorts;
113276
+ if (isMemoryBackendDisabled(config)) {
113277
+ console.log(source_default.gray(" Memory backend disabled (set to 'none')."));
113278
+ console.log(source_default.green(` ${STEP_DONE} Skipped`));
113279
+ return { hindsightExpected: false, optedOut: true };
113280
+ }
113281
+ let setupHindsight = true;
113282
+ if (!nonInteractive) {
113283
+ console.log(source_default.gray(" Hindsight will use Anthropic OAuth via the auth-broker. The fleet's"));
113284
+ console.log(source_default.gray(" active account (auth.active) is shared \u2014 no OpenAI API key is needed."));
113285
+ setupHindsight = await askYesNo(" Set up Hindsight memory? (recommended)", true);
113286
+ }
113287
+ if (!setupHindsight) {
113288
+ console.log(source_default.gray(" Skipping Hindsight setup."));
113289
+ console.log(source_default.green(` ${STEP_DONE} Skipped`));
113290
+ return { hindsightExpected: false, optedOut: false };
113291
+ }
113292
+ if (process.env.HINDSIGHT_API_LLM_API_KEY) {
113293
+ console.log(source_default.gray(" Note: HINDSIGHT_API_LLM_API_KEY is set in your env but is no longer used. " + "You can remove it."));
113294
+ }
113295
+ try {
113296
+ const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
113297
+ const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
113298
+ if (passphrase && existsSync85(vaultPath)) {
113299
+ const { getStringSecret: getStringSecret2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
113300
+ const existing = getStringSecret2(passphrase, vaultPath, "hindsight-api-key");
113301
+ if (existing) {
113302
+ console.log(source_default.gray(" Note: legacy 'hindsight-api-key' is in your vault but is no longer used. You can remove it with `switchroom vault rm hindsight-api-key`."));
113303
+ }
113304
+ }
113305
+ } catch {}
113306
+ {
113307
+ try {
113308
+ const result = await ensureHindsightConsumer(switchroomConfigPath);
113309
+ if (result.added) {
113310
+ console.log(source_default.green(` ${STEP_DONE} Registered auth.consumers[${HINDSIGHT_CONSUMER_NAME}] (follows the fleet active account)`));
113311
+ } else {
113312
+ console.log(source_default.gray(` auth.consumers[${HINDSIGHT_CONSUMER_NAME}] already present.`));
113313
+ }
113314
+ } catch (err) {
113315
+ console.log(source_default.yellow(` Warning: could not write auth.consumers entry: ${err.message}`));
113316
+ }
113317
+ }
113318
+ const dockerState = probeDockerAvailability(deps.dockerProbe);
113319
+ if (dockerState !== "ok") {
113320
+ const noCli = dockerState === "no-cli";
113321
+ const headline = noCli ? "Docker is not installed (no `docker` command on PATH)." : "Docker is installed but its daemon is not responding.";
113322
+ const remedy = noCli ? "Install Docker (https://docs.docker.com/get-docker/), then re-run `switchroom setup`" : "Start Docker (on macOS: open Docker Desktop; on Linux: `sudo systemctl start docker`), then re-run `switchroom setup`";
113323
+ console.log(source_default.red(` x ${headline}`));
113324
+ console.log(source_default.gray(" Hindsight memory runs as a Docker container (and so does every agent)."));
113325
+ console.log(source_default.gray(` ${remedy} \u2014 or re-run with SWITCHROOM_MEMORY_BACKEND=none to skip memory setup.`));
113326
+ throw new Error(`Memory backend setup failed: ${headline} ${remedy}, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
113327
+ }
113328
+ let recallPoolCfg;
113329
+ try {
113330
+ recallPoolCfg = resolveRecallPoolConfig(config.hindsight?.recall_pool);
113331
+ } catch (err) {
113332
+ console.log(source_default.yellow(` ! Ignoring hindsight.recall_pool (${err.message}); starting the single-container topology.`));
113333
+ recallPoolCfg = null;
113334
+ }
113335
+ if (recallPoolCfg) {
113336
+ for (const conflict2 of splitDbPoolEnvConflicts(config.hindsight?.env, recallPoolCfg)) {
113337
+ console.log(source_default.yellow(` ! ${splitDbPoolEnvConflictWarning(conflict2)}`));
113338
+ }
113339
+ }
113340
+ const startPool = deps.startRecallPool ?? startHindsightRecallPool;
113341
+ const waitHealthy = deps.waitForHealthy ?? waitForHindsightHealthy;
113342
+ const waitHealthyFor = (port, role) => waitHealthy(port, { role, workers: recallPoolCfg?.workers });
113343
+ const poolIsRunning = deps.recallPoolRunningProbe ?? isHindsightRecallPoolRunning;
113344
+ const stopPool = deps.stopRecallPool ?? stopHindsightRecallPool;
113345
+ const parseUrlPort = (u) => {
113346
+ try {
113347
+ const p = Number(new URL(u ?? "").port);
113348
+ return Number.isInteger(p) && p > 0 ? p : undefined;
113349
+ } catch {
113350
+ return;
113351
+ }
113352
+ };
113353
+ const resolveLaunchSecrets = async () => {
113354
+ const { litellm: litellmCfg, droppedRef: litellmDroppedRef } = await resolveLiteLLMForHindsight2(config, deps.getViaBrokerStructured ? { getViaBrokerStructured: deps.getViaBrokerStructured } : {});
113355
+ if (litellmDroppedRef) {
113356
+ console.log(source_default.yellow(` ! ${hindsightLiteLlmDroppedKeyWarning(litellmDroppedRef)}`));
113357
+ }
113358
+ const cpAccessKey = await resolveHindsightCpAccessKey(config);
113359
+ if (!cpAccessKey) {
113360
+ console.log(source_default.yellow(` ! ${HINDSIGHT_CP_NO_ACCESS_KEY_WARNING}`));
113361
+ }
113362
+ const resolvedLlm = await resolveHindsightLlmSecrets(config.hindsight?.llm, deps.getViaBrokerStructured ? { getViaBrokerStructured: deps.getViaBrokerStructured } : {});
113363
+ for (const drop of await diffDroppedHindsightLlmVaultKeys(config.hindsight?.llm, resolvedLlm)) {
113364
+ console.log(source_default.yellow(` ! ${hindsightLlmDroppedKeyWarning(drop)}`));
113365
+ }
113366
+ return { litellmCfg, cpAccessKey, resolvedLlm };
113367
+ };
113368
+ const launchRecallPool = async (cfg, publicApiPort2, authorityApiPort, fallbackUiPort, secrets) => {
113369
+ const startContainer = deps.startContainer ?? startHindsight;
113370
+ const result = await launchRecallPoolHealthGated({
113371
+ publicApiPort: publicApiPort2,
113372
+ authorityApiPort,
113373
+ workers: cfg.workers,
113374
+ waitForHealthy: waitHealthyFor,
113375
+ stopPool,
113376
+ startPool: () => startPool({
113377
+ cfg,
113378
+ poolPort: publicApiPort2,
113379
+ litellm: secrets.litellmCfg,
113380
+ llm: secrets.resolvedLlm,
113381
+ perf: {
113382
+ env: splitContainerPerfEnv(config.hindsight?.env),
113383
+ memLimit: config.hindsight?.mem_limit
113384
+ },
113385
+ cpAccessKey: secrets.cpAccessKey,
113386
+ mirrorDir: hindsightConsumerMirrorDir(config)
113387
+ }),
113388
+ degradeToSingleContainer: () => {
113389
+ stopPool();
113390
+ (deps.stopContainer ?? stopHindsight)();
113391
+ startContainer({ apiPort: publicApiPort2, uiPort: fallbackUiPort }, secrets.litellmCfg, undefined, secrets.resolvedLlm, hindsightConsumerMirrorDir(config), undefined, { env: config.hindsight?.env, memLimit: config.hindsight?.mem_limit }, secrets.cpAccessKey);
113392
+ },
113393
+ log: (m) => console.log(source_default.gray(m))
113394
+ });
113395
+ switch (result.outcome) {
113396
+ case "split":
113397
+ return "split";
113398
+ case "degraded-single-container":
113399
+ console.log(source_default.yellow(` ! Recall pool failed (${result.reason}); DEGRADED to a single container on the public port ${publicApiPort2}. memory.config.url IS served, but the recall split is ` + `OFF \u2014 inspect \`docker logs ${HINDSIGHT_RECALL_POOL_CONTAINER}\` and re-run \`switchroom setup\` once fixed, or set hindsight.recall_pool.enabled: false.`));
113400
+ return "degraded";
113401
+ case "authority-unhealthy":
113402
+ throw new Error(`Memory backend setup failed: the authority container did not become healthy on ${authorityApiPort}. Check \`docker logs switchroom-hindsight --tail 50\`, then re-run \`switchroom setup\`, or set hindsight.recall_pool.enabled: false to skip the split.`);
113403
+ case "degrade-failed":
113404
+ throw new Error(`Memory backend setup failed: the recall pool did not become healthy on ${publicApiPort2} AND the single-container fallback also failed to bind the public port. Inspect \`docker logs switchroom-hindsight --tail 50\` and \`docker logs ${HINDSIGHT_RECALL_POOL_CONTAINER}\`, then re-run \`switchroom setup\`, or set hindsight.recall_pool.enabled: false to skip the split.`);
113405
+ }
113406
+ };
113407
+ if (isHindsightRunning(deps.dockerProbe)) {
113408
+ if (recallPoolCfg && !poolIsRunning(deps.dockerProbe)) {
113409
+ console.log(source_default.gray(" Authority container is up but the recall pool is not \u2014 restoring the pool..."));
113410
+ const secrets = await resolveLaunchSecrets();
113411
+ const { publicApiPort: publicApiPort2, basePorts: basePorts2 } = resolveHindsightLaunchPorts({
113412
+ recallEnabled: true,
113413
+ configUrlPort: parseUrlPort(config.memory?.config?.url),
113414
+ reuseApiPort: undefined,
113415
+ uiPort: 0,
113416
+ defaultApiPort: HINDSIGHT_DEFAULT_API_PORT
113417
+ });
113418
+ const topo = await launchRecallPool(recallPoolCfg, publicApiPort2, basePorts2.apiPort, HINDSIGHT_DEFAULT_UI_PORT, secrets);
113419
+ if (topo === "split") {
113420
+ console.log(source_default.green(` ${STEP_DONE} Recall pool restored on ${publicApiPort2} (authority on ${basePorts2.apiPort})`));
113421
+ }
113422
+ return { hindsightExpected: true, optedOut: false };
113423
+ }
113424
+ const staleRunningPorts = (deps.runningPortsProbe ?? getRunningHindsightPorts)();
113425
+ const stalePlan = planStaleRecallPoolConvergence({
113426
+ recallEnabled: recallPoolCfg !== null,
113427
+ poolRunning: poolIsRunning(deps.dockerProbe),
113428
+ authorityApiPort: staleRunningPorts?.apiPort,
113429
+ configUrlPort: parseUrlPort(config.memory?.config?.url)
113430
+ });
113431
+ if (stalePlan.action !== "none") {
113432
+ const staleSecrets = stalePlan.action === "relaunch-single-on-public" ? await resolveLaunchSecrets() : undefined;
113433
+ const startContainer = deps.startContainer ?? startHindsight;
113434
+ const staleResult = await convergeStaleRecallPool({
113435
+ plan: stalePlan,
113436
+ stopPool,
113437
+ relaunchSingleOnPublic: (publicApiPort2) => {
113438
+ stopPool();
113439
+ (deps.stopContainer ?? stopHindsight)();
113440
+ startContainer({
113441
+ apiPort: publicApiPort2,
113442
+ uiPort: staleRunningPorts?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT
113443
+ }, staleSecrets.litellmCfg, undefined, staleSecrets.resolvedLlm, hindsightConsumerMirrorDir(config), undefined, { env: config.hindsight?.env, memLimit: config.hindsight?.mem_limit }, staleSecrets.cpAccessKey);
113444
+ },
113445
+ waitForHealthy: waitHealthyFor,
113446
+ log: (m) => console.log(source_default.gray(m))
113447
+ });
113448
+ if (staleResult.outcome === "relaunch-failed") {
113449
+ const msg = `hindsight.recall_pool is disabled, but the single container relaunched on the public port ${staleResult.publicApiPort} did not become healthy \u2014 nothing is ` + `serving fleet memory.`;
113450
+ console.log(source_default.red(` x ${msg}`));
113451
+ throw new Error(`Memory backend setup failed: ${msg} Inspect \`docker logs switchroom-hindsight --tail 50\`, then re-run \`switchroom setup\`.`);
113452
+ }
113453
+ if (staleResult.outcome === "pool-stopped") {
113454
+ console.log(source_default.green(` ${STEP_DONE} Removed the orphan recall pool (hindsight.recall_pool is disabled)`));
113455
+ } else if (staleResult.outcome === "relaunched-single") {
113456
+ console.log(source_default.green(` ${STEP_DONE} Converged to the declared single-container topology on ${staleResult.publicApiPort}`));
113457
+ }
113458
+ return { hindsightExpected: true, optedOut: false };
113459
+ }
113460
+ const runningPorts = staleRunningPorts;
113461
+ if (publicPortIsUnserved({
113462
+ configUrlPort: parseUrlPort(config.memory?.config?.url),
113463
+ authorityApiPort: runningPorts?.apiPort,
113464
+ poolRunning: poolIsRunning(deps.dockerProbe)
113465
+ })) {
113466
+ const publicPort = parseUrlPort(config.memory?.config?.url);
113467
+ const msg = `switchroom-hindsight is running on port ${runningPorts?.apiPort}, but memory.config.url points at ${publicPort} and nothing is serving it \u2014 ` + `fleet memory is DOWN.`;
113468
+ console.log(source_default.red(` x ${msg}`));
113469
+ throw new Error(`Memory backend setup failed: ${msg} Run \`switchroom memory setup --recreate\` to rebind it to ${publicPort}, or set hindsight.recall_pool.enabled: true to restore the recall pool on the public port.`);
113470
+ }
113471
+ console.log(source_default.green(` ${STEP_DONE} Hindsight container already running (switchroom-hindsight)`));
113472
+ return { hindsightExpected: true, optedOut: false };
113473
+ }
113474
+ if (isHindsightContainerExists(deps.dockerProbe)) {
113475
+ console.log(source_default.gray(" Found stopped switchroom-hindsight container, removing..."));
113476
+ stopHindsight();
113477
+ }
113478
+ stopPool();
113479
+ let ports;
113480
+ try {
113481
+ ports = await pickPorts();
113482
+ } catch (err) {
113483
+ console.log(source_default.red(` ${err.message}`));
113484
+ throw new Error(`Memory backend setup failed: could not allocate Hindsight ports: ${err.message}. Free a port and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
113485
+ }
113486
+ if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
113487
+ console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; using ${ports.apiPort}/${ports.uiPort} instead.`));
113488
+ }
113489
+ let conflict = await preflightPorts(ports);
113490
+ if (conflict) {
113491
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
113492
+ console.log(source_default.yellow(` Chosen Hindsight port ${conflict.port} is occupied${heldBy}; selecting a free port instead of crash-looping.`));
113493
+ try {
113494
+ ports = await pickPorts();
113495
+ } catch (err) {
113496
+ console.log(source_default.red(` ${err.message}`));
113497
+ throw new Error(`Memory backend setup failed: could not allocate Hindsight ports after reassignment: ${err.message}. Free a port and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
113498
+ }
113499
+ conflict = await preflightPorts(ports);
113500
+ if (conflict) {
113501
+ const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
113502
+ const msg = `Refusing to start Hindsight: port ${conflict.port} is still occupied${stillHeldBy} after reassignment.`;
113503
+ console.log(source_default.red(` ${msg}`));
113504
+ throw new Error(`Memory backend setup failed: ${msg} Free it and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
113505
+ }
113506
+ console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
113507
+ }
113508
+ const { publicApiPort, basePorts } = resolveHindsightLaunchPorts({
113509
+ recallEnabled: recallPoolCfg != null,
113510
+ configUrlPort: parseUrlPort(config.memory?.config?.url),
113511
+ reuseApiPort: ports.apiPort,
113512
+ uiPort: ports.uiPort,
113513
+ defaultApiPort: HINDSIGHT_DEFAULT_API_PORT
113514
+ });
113515
+ if (recallPoolCfg) {
113516
+ const bgConflict = await preflightPorts({
113517
+ apiPort: basePorts.apiPort,
113518
+ uiPort: basePorts.apiPort
113519
+ });
113520
+ if (bgConflict) {
113521
+ const heldBy = bgConflict.holder ? ` (held by ${bgConflict.holder})` : "";
113522
+ const msg = `Refusing to start the hindsight recall split: authority port ${bgConflict.port} is occupied${heldBy}.`;
113523
+ console.log(source_default.red(` ${msg}`));
113524
+ throw new Error(`Memory backend setup failed: ${msg} Free it and re-run \`switchroom setup\`, or set hindsight.recall_pool.enabled: false to skip the split.`);
113525
+ }
113526
+ }
113527
+ const spin = spinner("Starting Hindsight Docker container...");
113528
+ let launchSecrets;
113529
+ try {
113530
+ launchSecrets = await resolveLaunchSecrets();
113531
+ const authorityEnv = recallPoolCfg ? splitContainerPerfEnv(config.hindsight?.env) : config.hindsight?.env;
113532
+ const startContainer = deps.startContainer ?? startHindsight;
113533
+ startContainer(basePorts, launchSecrets.litellmCfg, undefined, launchSecrets.resolvedLlm, hindsightConsumerMirrorDir(config), undefined, { env: authorityEnv, memLimit: config.hindsight?.mem_limit }, launchSecrets.cpAccessKey);
113534
+ if (launchSecrets.litellmCfg) {
113535
+ console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
113536
+ }
113537
+ } catch (err) {
113538
+ spin.stop(source_default.red(`Failed to start Hindsight: ${err.message}`));
113539
+ console.log(source_default.gray(` Make sure \`switchroom apply\` has run so the auth-broker consumer socket volume (auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock) exists.`));
113540
+ throw new Error(`Memory backend setup failed: ${err.message}. Fix the cause and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
113541
+ }
113542
+ let running = isHindsightRunning(deps.dockerProbe);
113543
+ const retries = deps.readyRetries ?? HINDSIGHT_READY_RETRIES;
113544
+ for (let i2 = 0;!running && i2 < retries; i2++) {
113545
+ await sleep4(HINDSIGHT_READY_INTERVAL_MS);
113546
+ running = isHindsightRunning(deps.dockerProbe);
113547
+ }
113548
+ if (!running) {
113549
+ spin.stop(source_default.red("Hindsight container did not stay running after start"));
113550
+ throw new Error("Memory backend setup failed: switchroom-hindsight did not stay running after start. Check `docker logs switchroom-hindsight --tail 50`, then re-run `switchroom setup`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.");
113551
+ }
113552
+ spin.stop(source_default.green(`${STEP_DONE} Hindsight container started (switchroom-hindsight)`));
113553
+ if (recallPoolCfg) {
113554
+ const topo = await launchRecallPool(recallPoolCfg, publicApiPort, basePorts.apiPort, basePorts.uiPort, launchSecrets);
113555
+ if (topo === "split") {
113556
+ console.log(source_default.green(` ${STEP_DONE} Hindsight recall split started: authority (pg0 + background) on ${basePorts.apiPort}, ${recallPoolCfg.workers}-worker recall pool on ${publicApiPort}`));
113557
+ }
113558
+ }
113559
+ console.log(source_default.gray(` API: http://localhost:${publicApiPort}/mcp`));
113560
+ console.log(source_default.gray(` UI: http://localhost:${basePorts.uiPort}`));
113561
+ return { hindsightExpected: true, optedOut: false };
113562
+ }
113563
+
113564
+ // src/cli/setup.ts
112426
113565
  function writeSwitchroomYaml(configPath, text) {
112427
113566
  let mode = 420;
112428
113567
  try {
@@ -112525,7 +113664,7 @@ async function stepConfigFile(configPath, nonInteractive) {
112525
113664
  existingConfig = null;
112526
113665
  }
112527
113666
  }
112528
- if (existingConfig && existsSync85(existingConfig)) {
113667
+ if (existingConfig && existsSync86(existingConfig)) {
112529
113668
  if (!nonInteractive) {
112530
113669
  const useExisting = await askYesNo(` Found ${source_default.cyan(existingConfig)}. Use it?`, true);
112531
113670
  if (!useExisting) {
@@ -112671,7 +113810,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
112671
113810
  try {
112672
113811
  const { openVault: openVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
112673
113812
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
112674
- if (existsSync85(vaultPath)) {
113813
+ if (existsSync86(vaultPath)) {
112675
113814
  const secrets = openVault2(passphrase, vaultPath);
112676
113815
  const key = rawToken.replace("vault:", "");
112677
113816
  const entry = secrets[key];
@@ -112699,7 +113838,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
112699
113838
  async function storeTokenInVault(config, vaultRef, token) {
112700
113839
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
112701
113840
  const key = vaultRef.replace("vault:", "");
112702
- if (!existsSync85(vaultPath)) {
113841
+ if (!existsSync86(vaultPath)) {
112703
113842
  console.log(source_default.gray(" Creating encrypted vault..."));
112704
113843
  let passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
112705
113844
  if (!passphrase) {
@@ -112846,145 +113985,6 @@ async function stepCreateTopics(config, botToken, nonInteractive) {
112846
113985
  }
112847
113986
  }
112848
113987
  }
112849
- async function resolveLiteLLMForHindsight2(config, deps = {}) {
112850
- return resolveHindsightLiteLlm(config, deps);
112851
- }
112852
- function isMemoryBackendDisabled(config) {
112853
- const memoryBackend = config.memory?.backend ?? "hindsight";
112854
- return process.env.SWITCHROOM_MEMORY_BACKEND === "none" || memoryBackend === "none";
112855
- }
112856
- async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, deps = {}) {
112857
- stepHeader(6, "Memory backend", STEP_ACTIVE);
112858
- const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
112859
- if (isMemoryBackendDisabled(config)) {
112860
- console.log(source_default.gray(" Memory backend disabled (set to 'none')."));
112861
- console.log(source_default.green(` ${STEP_DONE} Skipped`));
112862
- return { hindsightExpected: false, optedOut: true };
112863
- }
112864
- let setupHindsight = true;
112865
- if (!nonInteractive) {
112866
- console.log(source_default.gray(" Hindsight will use Anthropic OAuth via the auth-broker. The fleet's"));
112867
- console.log(source_default.gray(" active account (auth.active) is shared \u2014 no OpenAI API key is needed."));
112868
- setupHindsight = await askYesNo(" Set up Hindsight memory? (recommended)", true);
112869
- }
112870
- if (!setupHindsight) {
112871
- console.log(source_default.gray(" Skipping Hindsight setup."));
112872
- console.log(source_default.green(` ${STEP_DONE} Skipped`));
112873
- return { hindsightExpected: false, optedOut: false };
112874
- }
112875
- if (process.env.HINDSIGHT_API_LLM_API_KEY) {
112876
- console.log(source_default.gray(" Note: HINDSIGHT_API_LLM_API_KEY is set in your env but is no longer used. You can remove it."));
112877
- }
112878
- try {
112879
- const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
112880
- const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
112881
- if (passphrase && existsSync85(vaultPath)) {
112882
- const existing = getStringSecret(passphrase, vaultPath, "hindsight-api-key");
112883
- if (existing) {
112884
- console.log(source_default.gray(" Note: legacy 'hindsight-api-key' is in your vault but is no longer used. You can remove it with `switchroom vault rm hindsight-api-key`."));
112885
- }
112886
- }
112887
- } catch {}
112888
- {
112889
- try {
112890
- const result = await ensureHindsightConsumer(switchroomConfigPath);
112891
- if (result.added) {
112892
- console.log(source_default.green(` ${STEP_DONE} Registered auth.consumers[${HINDSIGHT_CONSUMER_NAME}] (follows the fleet active account)`));
112893
- } else {
112894
- console.log(source_default.gray(` auth.consumers[${HINDSIGHT_CONSUMER_NAME}] already present.`));
112895
- }
112896
- } catch (err) {
112897
- console.log(source_default.yellow(` Warning: could not write auth.consumers entry: ${err.message}`));
112898
- }
112899
- }
112900
- const dockerState = probeDockerAvailability(deps.dockerProbe);
112901
- if (dockerState !== "ok") {
112902
- const noCli = dockerState === "no-cli";
112903
- const headline = noCli ? "Docker is not installed (no `docker` command on PATH)." : "Docker is installed but its daemon is not responding.";
112904
- const remedy = noCli ? "Install Docker (https://docs.docker.com/get-docker/), then re-run `switchroom setup`" : "Start Docker (on macOS: open Docker Desktop; on Linux: `sudo systemctl start docker`), then re-run `switchroom setup`";
112905
- console.log(source_default.red(` x ${headline}`));
112906
- console.log(source_default.gray(" Hindsight memory runs as a Docker container (and so does every agent)."));
112907
- console.log(source_default.gray(` ${remedy} \u2014 or re-run with SWITCHROOM_MEMORY_BACKEND=none to skip memory setup.`));
112908
- throw new Error(`Memory backend setup failed: ${headline} ${remedy}, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
112909
- }
112910
- if (isHindsightRunning(deps.dockerProbe)) {
112911
- console.log(source_default.green(` ${STEP_DONE} Hindsight container already running (switchroom-hindsight)`));
112912
- return { hindsightExpected: true, optedOut: false };
112913
- }
112914
- if (isHindsightContainerExists(deps.dockerProbe)) {
112915
- console.log(source_default.gray(" Found stopped switchroom-hindsight container, removing..."));
112916
- stopHindsight();
112917
- }
112918
- let ports;
112919
- try {
112920
- ports = await pickHindsightPorts();
112921
- } catch (err) {
112922
- console.log(source_default.red(` ${err.message}`));
112923
- throw new Error(`Memory backend setup failed: could not allocate Hindsight ports: ${err.message}. Free a port and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
112924
- }
112925
- if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
112926
- console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; using ${ports.apiPort}/${ports.uiPort} instead.`));
112927
- }
112928
- let conflict = await preflightHindsightPorts(ports);
112929
- if (conflict) {
112930
- const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
112931
- console.log(source_default.yellow(` Chosen Hindsight port ${conflict.port} is occupied${heldBy}; selecting a free port instead of crash-looping.`));
112932
- try {
112933
- ports = await pickHindsightPorts();
112934
- } catch (err) {
112935
- console.log(source_default.red(` ${err.message}`));
112936
- throw new Error(`Memory backend setup failed: could not allocate Hindsight ports after reassignment: ${err.message}. Free a port and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
112937
- }
112938
- conflict = await preflightHindsightPorts(ports);
112939
- if (conflict) {
112940
- const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
112941
- const msg = `Refusing to start Hindsight: port ${conflict.port} is still occupied${stillHeldBy} after reassignment.`;
112942
- console.log(source_default.red(` ${msg}`));
112943
- throw new Error(`Memory backend setup failed: ${msg} Free it and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
112944
- }
112945
- console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
112946
- }
112947
- const spin = spinner("Starting Hindsight Docker container...");
112948
- try {
112949
- const { litellm: litellmCfg, droppedRef: litellmDroppedRef } = await resolveLiteLLMForHindsight2(config, deps.getViaBrokerStructured ? { getViaBrokerStructured: deps.getViaBrokerStructured } : {});
112950
- if (litellmDroppedRef) {
112951
- console.log(source_default.yellow(` ! ${hindsightLiteLlmDroppedKeyWarning(litellmDroppedRef)}`));
112952
- }
112953
- const cpAccessKey = await resolveHindsightCpAccessKey(config);
112954
- if (!cpAccessKey) {
112955
- console.log(source_default.yellow(` ! ${HINDSIGHT_CP_NO_ACCESS_KEY_WARNING}`));
112956
- }
112957
- const resolvedLlm = await resolveHindsightLlmSecrets(config.hindsight?.llm, deps.getViaBrokerStructured ? { getViaBrokerStructured: deps.getViaBrokerStructured } : {});
112958
- for (const drop of await diffDroppedHindsightLlmVaultKeys(config.hindsight?.llm, resolvedLlm)) {
112959
- console.log(source_default.yellow(` ! ${hindsightLlmDroppedKeyWarning(drop)}`));
112960
- }
112961
- const startContainer = deps.startContainer ?? startHindsight;
112962
- startContainer(ports, litellmCfg, undefined, resolvedLlm, hindsightConsumerMirrorDir(config), undefined, { env: config.hindsight?.env, memLimit: config.hindsight?.mem_limit }, cpAccessKey);
112963
- if (litellmCfg) {
112964
- console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
112965
- }
112966
- } catch (err) {
112967
- spin.stop(source_default.red(`Failed to start Hindsight: ${err.message}`));
112968
- console.log(source_default.gray(` Make sure \`switchroom apply\` has run so the auth-broker consumer socket volume (auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock) exists.`));
112969
- throw new Error(`Memory backend setup failed: ${err.message}. Fix the cause and re-run \`switchroom setup\`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.`);
112970
- }
112971
- let running = isHindsightRunning(deps.dockerProbe);
112972
- const retries = deps.readyRetries ?? HINDSIGHT_READY_RETRIES;
112973
- for (let i2 = 0;!running && i2 < retries; i2++) {
112974
- await sleep4(HINDSIGHT_READY_INTERVAL_MS);
112975
- running = isHindsightRunning(deps.dockerProbe);
112976
- }
112977
- if (!running) {
112978
- spin.stop(source_default.red("Hindsight container did not stay running after start"));
112979
- throw new Error("Memory backend setup failed: switchroom-hindsight did not stay running after start. Check `docker logs switchroom-hindsight --tail 50`, then re-run `switchroom setup`, or set SWITCHROOM_MEMORY_BACKEND=none to skip it.");
112980
- }
112981
- spin.stop(source_default.green(`${STEP_DONE} Hindsight container started (switchroom-hindsight)`));
112982
- console.log(source_default.gray(` API: http://localhost:${ports.apiPort}/mcp`));
112983
- console.log(source_default.gray(` UI: http://localhost:${ports.uiPort}`));
112984
- return { hindsightExpected: true, optedOut: false };
112985
- }
112986
- var HINDSIGHT_READY_RETRIES = 5;
112987
- var HINDSIGHT_READY_INTERVAL_MS = 1000;
112988
113988
  function stepVoiceEngine() {
112989
113989
  stepHeader(7, "Voice engine", STEP_ACTIVE);
112990
113990
  let caps;
@@ -113121,13 +114121,13 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
113121
114121
  return;
113122
114122
  }
113123
114123
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
113124
- if (!existsSync85(vaultPath)) {
114124
+ if (!existsSync86(vaultPath)) {
113125
114125
  console.log(source_default.gray(" Skipping (vault not created yet)."));
113126
114126
  return;
113127
114127
  }
113128
114128
  const credPathRaw = config.vault?.broker?.autoUnlockCredentialPath ?? "~/.switchroom/vault-auto-unlock";
113129
114129
  const credPath = resolvePath(credPathRaw);
113130
- if (config.vault?.broker?.autoUnlock === true && existsSync85(credPath)) {
114130
+ if (config.vault?.broker?.autoUnlock === true && existsSync86(credPath)) {
113131
114131
  console.log(source_default.green(` ${STEP_DONE} Already configured (${credPath})`));
113132
114132
  return;
113133
114133
  }
@@ -113205,7 +114205,7 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
113205
114205
  }
113206
114206
  }
113207
114207
  function persistApprovalAuthTelegramId(configPath) {
113208
- if (!existsSync85(configPath)) {
114208
+ if (!existsSync86(configPath)) {
113209
114209
  throw new ConfigError(`Cannot persist vault.broker.approvalAuth: config not found at ${configPath}`);
113210
114210
  }
113211
114211
  const content = readFileSync79(configPath, "utf-8");
@@ -113216,7 +114216,7 @@ function persistApprovalAuthTelegramId(configPath) {
113216
114216
  return result.kind;
113217
114217
  }
113218
114218
  function persistDangerousMode(config, configPath) {
113219
- if (!existsSync85(configPath)) {
114219
+ if (!existsSync86(configPath)) {
113220
114220
  throw new ConfigError(`Cannot persist dangerous_mode: config not found at ${configPath}`);
113221
114221
  }
113222
114222
  let content = readFileSync79(configPath, "utf-8");
@@ -113327,7 +114327,7 @@ var START_OUTPUT_TAIL_LINES = 12;
113327
114327
  function defaultStartAgent(name) {
113328
114328
  let out = "";
113329
114329
  try {
113330
- out = execFileSync26("switchroom", ["agent", "start", name], {
114330
+ out = execFileSync27("switchroom", ["agent", "start", name], {
113331
114331
  stdio: ["ignore", "pipe", "pipe"],
113332
114332
  encoding: "utf-8"
113333
114333
  });
@@ -113355,7 +114355,7 @@ async function stepVerification(config, nonInteractive, deps = {}, expect = {})
113355
114355
  const findings = [];
113356
114356
  let applyPending = false;
113357
114357
  if (!nonInteractive && agentNames.length > 0) {
113358
- const composeExists = (deps.composeFileExists ?? (() => existsSync85(composeFilePath())))();
114358
+ const composeExists = (deps.composeFileExists ?? (() => existsSync86(composeFilePath())))();
113359
114359
  if (!composeExists) {
113360
114360
  applyPending = true;
113361
114361
  log(source_default.gray(" Not started: `switchroom apply` has not generated the compose file yet."));
@@ -113452,7 +114452,7 @@ init_source();
113452
114452
  init_loader();
113453
114453
  init_lifecycle();
113454
114454
  init_compose_env();
113455
- import { existsSync as existsSync88, mkdirSync as mkdirSync50, readFileSync as readFileSync82, realpathSync as realpathSync8, statSync as statSync49, chownSync as chownSync10 } from "node:fs";
114455
+ import { existsSync as existsSync89, mkdirSync as mkdirSync50, readFileSync as readFileSync82, realpathSync as realpathSync8, statSync as statSync49, chownSync as chownSync10 } from "node:fs";
113456
114456
  import { spawnSync as spawnSync20 } from "node:child_process";
113457
114457
  import { join as join92, dirname as dirname39 } from "node:path";
113458
114458
  import { homedir as homedir48 } from "node:os";
@@ -113899,7 +114899,7 @@ import {
113899
114899
  closeSync as closeSync15,
113900
114900
  copyFileSync as copyFileSync12,
113901
114901
  createWriteStream,
113902
- existsSync as existsSync86,
114902
+ existsSync as existsSync87,
113903
114903
  lstatSync as lstatSync13,
113904
114904
  mkdirSync as mkdirSync48,
113905
114905
  openSync as openSync15,
@@ -113982,7 +114982,7 @@ function defaultSelfUpdateIO() {
113982
114982
  rmSync15(path7, { force: true });
113983
114983
  },
113984
114984
  exists(path7) {
113985
- return existsSync86(path7);
114985
+ return existsSync87(path7);
113986
114986
  },
113987
114987
  dirname(path7) {
113988
114988
  return dirname37(path7);
@@ -114037,7 +115037,7 @@ init_install_cron2();
114037
115037
 
114038
115038
  // src/host-cli-self-heal/install-timer.ts
114039
115039
  import { spawnSync as spawnSync19 } from "node:child_process";
114040
- import { existsSync as existsSync87, mkdirSync as mkdirSync49, readFileSync as readFileSync81, renameSync as renameSync22, writeFileSync as writeFileSync34 } from "node:fs";
115040
+ import { existsSync as existsSync88, mkdirSync as mkdirSync49, readFileSync as readFileSync81, renameSync as renameSync22, writeFileSync as writeFileSync34 } from "node:fs";
114041
115041
  import { dirname as dirname38 } from "node:path";
114042
115042
  var SERVICE_PATH = "/etc/systemd/system/switchroom-self-heal.service";
114043
115043
  var TIMER_PATH = "/etc/systemd/system/switchroom-self-heal.timer";
@@ -114084,7 +115084,7 @@ function renderTimer() {
114084
115084
  `;
114085
115085
  }
114086
115086
  function writeUnitIdempotent(path7, content) {
114087
- if (existsSync87(path7)) {
115087
+ if (existsSync88(path7)) {
114088
115088
  try {
114089
115089
  if (readFileSync81(path7, "utf8") === content)
114090
115090
  return "unchanged";
@@ -114115,7 +115115,7 @@ function resolveOperatorUser(env2) {
114115
115115
  }
114116
115116
  function installSelfHealTimer(deps = {}) {
114117
115117
  const env2 = deps.env ?? process.env;
114118
- const systemdBooted = deps.systemdBooted ?? (() => existsSync87(SYSTEMD_MARKER));
115118
+ const systemdBooted = deps.systemdBooted ?? (() => existsSync88(SYSTEMD_MARKER));
114119
115119
  const geteuid = deps.geteuid ?? (() => typeof process.geteuid === "function" ? process.geteuid() : undefined);
114120
115120
  const homeForUser = deps.homeForUser ?? homeFromPasswd;
114121
115121
  const servicePath = deps.servicePath ?? SERVICE_PATH;
@@ -114200,7 +115200,7 @@ var PACKAGED_INSTALL_KINDS = new Set([
114200
115200
  function runningFromSwitchroomCheckout(scriptPath) {
114201
115201
  let dir = dirname39(scriptPath);
114202
115202
  for (let i2 = 0;i2 < 12; i2++) {
114203
- if (existsSync88(join92(dir, ".git"))) {
115203
+ if (existsSync89(join92(dir, ".git"))) {
114204
115204
  try {
114205
115205
  const pkg = JSON.parse(readFileSync82(join92(dir, "package.json"), "utf-8"));
114206
115206
  if (pkg.name === "switchroom")
@@ -114269,7 +115269,7 @@ function planUpdate(opts) {
114269
115269
  bundleDir: import.meta.dirname,
114270
115270
  execPath: process.execPath,
114271
115271
  scriptPath,
114272
- inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync88("/.dockerenv")
115272
+ inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync89("/.dockerenv")
114273
115273
  });
114274
115274
  if (detection.kind !== "static-binary") {
114275
115275
  say(source_default.gray(` host CLI not self-updated: ${detection.reason}
@@ -114352,7 +115352,7 @@ function planUpdate(opts) {
114352
115352
  steps.push({
114353
115353
  name: "pull-images",
114354
115354
  description: "Pull broker / kernel / agent images from GHCR",
114355
- skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync88(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
115355
+ skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync89(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
114356
115356
  run: () => {
114357
115357
  const r = runner("docker", [
114358
115358
  "compose",
@@ -114587,7 +115587,7 @@ function planUpdate(opts) {
114587
115587
  bundleDir: import.meta.dirname,
114588
115588
  execPath: process.execPath,
114589
115589
  scriptPath,
114590
- inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync88("/.dockerenv")
115590
+ inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync89("/.dockerenv")
114591
115591
  });
114592
115592
  const detail = `no shipped skills/ payload found for this install ` + `(${detection.kind}; ${describeShippedAssetSearch(resolution)})`;
114593
115593
  if (PACKAGED_INSTALL_KINDS.has(detection.kind)) {
@@ -114627,10 +115627,10 @@ function planUpdate(opts) {
114627
115627
  if (opts.syncBundledSkillsFn)
114628
115628
  return;
114629
115629
  const dest = join92(homedir48(), ".switchroom", "skills", "_bundled");
114630
- if (!existsSync88(dest)) {
115630
+ if (!existsSync89(dest)) {
114631
115631
  return;
114632
115632
  }
114633
- const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !existsSync88(join92(dest, key)));
115633
+ const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !existsSync89(join92(dest, key)));
114634
115634
  if (missing.length > 0) {
114635
115635
  throw new Error(`verify-bundled-skills: builtin default skill(s) missing from the pool after sync: ` + `${missing.join(", ")}. These ship in the CLI package and must exist in ${dest}. ` + `This is a broken sync or a packaging regression \u2014 the pool is not converged.`);
114636
115636
  }
@@ -114745,7 +115745,7 @@ function defaultStatusProbe(composePath) {
114745
115745
  let dir = dirname39(scriptPath);
114746
115746
  for (let i2 = 0;i2 < 8; i2++) {
114747
115747
  const pkgPath = join92(dir, "package.json");
114748
- if (existsSync88(pkgPath)) {
115748
+ if (existsSync89(pkgPath)) {
114749
115749
  try {
114750
115750
  const pkg = JSON.parse(readFileSync82(pkgPath, "utf-8"));
114751
115751
  if (typeof pkg.version === "string")
@@ -114768,7 +115768,7 @@ function defaultStatusProbe(composePath) {
114768
115768
  warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
114769
115769
  }
114770
115770
  const services = [];
114771
- if (!existsSync88(composePath)) {
115771
+ if (!existsSync89(composePath)) {
114772
115772
  warnings.push(`compose file not found at ${composePath}; service status unknown`);
114773
115773
  return { cliVersion, cliBuiltAt, services, warnings };
114774
115774
  }
@@ -115036,7 +116036,7 @@ import { homedir as homedir50 } from "node:os";
115036
116036
 
115037
116037
  // src/cli/rollout-pin-journal.ts
115038
116038
  import {
115039
- existsSync as existsSync89,
116039
+ existsSync as existsSync90,
115040
116040
  readFileSync as readFileSync83,
115041
116041
  writeFileSync as writeFileSync35,
115042
116042
  renameSync as renameSync23,
@@ -115100,7 +116100,7 @@ function readPinJournal(configPath, warn = (m) => process.stderr.write(m)) {
115100
116100
  try {
115101
116101
  raw = readFileSync83(p, "utf8");
115102
116102
  } catch (e) {
115103
- if (existsSync89(p)) {
116103
+ if (existsSync90(p)) {
115104
116104
  warn(`\u26a0\ufe0f rollout pin journal: ${p} exists but could not be read ` + `(${e.message}). A provisional \`release.pin\` may be ` + `uncommitted \u2014 verify it host-side before the next reconcile.
115105
116105
  `);
115106
116106
  }
@@ -115172,7 +116172,7 @@ function commitPinPersist(configPath) {
115172
116172
  unlinkSync17(p);
115173
116173
  return null;
115174
116174
  } catch (e) {
115175
- if (!existsSync89(p))
116175
+ if (!existsSync90(p))
115176
116176
  return null;
115177
116177
  return `rollout pin journal: FAILED to clear ${p} after a SUCCESSFUL roll ` + `(${e.message}). Delete it host-side \u2014 while it exists, ` + `recovery may revert a proven \`release.pin\`.`;
115178
116178
  }
@@ -116100,7 +117100,7 @@ init_helpers();
116100
117100
  init_lifecycle();
116101
117101
  init_resolve_version();
116102
117102
  import { execSync as execSync3 } from "node:child_process";
116103
- import { existsSync as existsSync90, readFileSync as readFileSync85 } from "node:fs";
117103
+ import { existsSync as existsSync91, readFileSync as readFileSync85 } from "node:fs";
116104
117104
  import { dirname as dirname41, join as join94 } from "node:path";
116105
117105
  function getClaudeCodeVersion() {
116106
117106
  try {
@@ -116152,10 +117152,10 @@ function locateSwitchroomInstallDir() {
116152
117152
  let dir = import.meta.dirname;
116153
117153
  for (let i2 = 0;i2 < 10 && dir && dir !== "/"; i2++) {
116154
117154
  const pkgPath = join94(dir, "package.json");
116155
- if (existsSync90(pkgPath)) {
117155
+ if (existsSync91(pkgPath)) {
116156
117156
  try {
116157
117157
  const pkg = JSON.parse(readFileSync85(pkgPath, "utf-8"));
116158
- if (pkg.name === "switchroom" && existsSync90(join94(dir, ".git"))) {
117158
+ if (pkg.name === "switchroom" && existsSync91(join94(dir, ".git"))) {
116159
117159
  return dir;
116160
117160
  }
116161
117161
  } catch {}
@@ -116336,7 +117336,7 @@ init_merge();
116336
117336
 
116337
117337
  // src/agents/session-retention.ts
116338
117338
  import {
116339
- existsSync as existsSync91,
117339
+ existsSync as existsSync92,
116340
117340
  readdirSync as readdirSync31,
116341
117341
  statSync as statSync52,
116342
117342
  unlinkSync as unlinkSync18
@@ -116347,7 +117347,7 @@ var DEFAULT_SESSION_RETENTION_MAX_AGE_DAYS = 30;
116347
117347
  var MIN_KEEP = 2;
116348
117348
  function collectSessionJsonl(claudeConfigDir) {
116349
117349
  const projects = join95(claudeConfigDir, "projects");
116350
- if (!existsSync91(projects))
117350
+ if (!existsSync92(projects))
116351
117351
  return [];
116352
117352
  const found = [];
116353
117353
  const walk2 = (dir) => {
@@ -116479,7 +117479,7 @@ function registerHandoffCommand(program3) {
116479
117479
  // src/issues/store.ts
116480
117480
  import {
116481
117481
  closeSync as closeSync16,
116482
- existsSync as existsSync92,
117482
+ existsSync as existsSync93,
116483
117483
  mkdirSync as mkdirSync52,
116484
117484
  openSync as openSync16,
116485
117485
  readdirSync as readdirSync32,
@@ -116518,7 +117518,7 @@ var ISSUES_FILE = "issues.jsonl";
116518
117518
  var ISSUES_LOCK = "issues.lock";
116519
117519
  function readAll(stateDir) {
116520
117520
  const path7 = join96(stateDir, ISSUES_FILE);
116521
- if (!existsSync92(path7))
117521
+ if (!existsSync93(path7))
116522
117522
  return [];
116523
117523
  let raw;
116524
117524
  try {
@@ -116595,7 +117595,7 @@ function record(stateDir, input, nowFn = Date.now) {
116595
117595
  });
116596
117596
  }
116597
117597
  function resolve51(stateDir, fingerprint, nowFn = Date.now) {
116598
- if (!existsSync92(join96(stateDir, ISSUES_FILE)))
117598
+ if (!existsSync93(join96(stateDir, ISSUES_FILE)))
116599
117599
  return 0;
116600
117600
  return withLock(stateDir, () => {
116601
117601
  const all = readAll(stateDir);
@@ -116613,7 +117613,7 @@ function resolve51(stateDir, fingerprint, nowFn = Date.now) {
116613
117613
  });
116614
117614
  }
116615
117615
  function resolveAllBySource(stateDir, source, nowFn = Date.now) {
116616
- if (!existsSync92(join96(stateDir, ISSUES_FILE)))
117616
+ if (!existsSync93(join96(stateDir, ISSUES_FILE)))
116617
117617
  return 0;
116618
117618
  return withLock(stateDir, () => {
116619
117619
  const all = readAll(stateDir);
@@ -116631,7 +117631,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
116631
117631
  });
116632
117632
  }
116633
117633
  function prune(stateDir, opts = {}) {
116634
- if (!existsSync92(join96(stateDir, ISSUES_FILE)))
117634
+ if (!existsSync93(join96(stateDir, ISSUES_FILE)))
116635
117635
  return 0;
116636
117636
  return withLock(stateDir, () => {
116637
117637
  const all = readAll(stateDir);
@@ -116981,14 +117981,14 @@ function relTime(deltaMs) {
116981
117981
 
116982
117982
  // src/cli/deps.ts
116983
117983
  init_source();
116984
- import { existsSync as existsSync95 } from "node:fs";
117984
+ import { existsSync as existsSync96 } from "node:fs";
116985
117985
  import { homedir as homedir53 } from "node:os";
116986
117986
  import { join as join99, resolve as resolve52 } from "node:path";
116987
117987
 
116988
117988
  // src/deps/python.ts
116989
117989
  import { createHash as createHash18 } from "node:crypto";
116990
117990
  import {
116991
- existsSync as existsSync93,
117991
+ existsSync as existsSync94,
116992
117992
  mkdirSync as mkdirSync53,
116993
117993
  readFileSync as readFileSync87,
116994
117994
  rmSync as rmSync16,
@@ -116996,7 +117996,7 @@ import {
116996
117996
  } from "node:fs";
116997
117997
  import { dirname as dirname42, join as join97 } from "node:path";
116998
117998
  import { homedir as homedir51 } from "node:os";
116999
- import { execFileSync as execFileSync27 } from "node:child_process";
117999
+ import { execFileSync as execFileSync28 } from "node:child_process";
117000
118000
 
117001
118001
  class PythonEnvError extends Error {
117002
118002
  stderr;
@@ -117016,7 +118016,7 @@ function ensurePythonEnv(opts) {
117016
118016
  const { skillName, requirementsPath, force = false } = opts;
117017
118017
  const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
117018
118018
  const hostPython = opts.pythonBin ?? "python3";
117019
- if (!existsSync93(requirementsPath)) {
118019
+ if (!existsSync94(requirementsPath)) {
117020
118020
  throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
117021
118021
  }
117022
118022
  const venvDir = join97(cacheRoot, skillName);
@@ -117025,7 +118025,7 @@ function ensurePythonEnv(opts) {
117025
118025
  const pythonBin = join97(binDir, "python");
117026
118026
  const pipBin = join97(binDir, "pip");
117027
118027
  const targetHash = hashFile(requirementsPath);
117028
- if (!force && existsSync93(stampPath) && existsSync93(pythonBin)) {
118028
+ if (!force && existsSync94(stampPath) && existsSync94(pythonBin)) {
117029
118029
  const existingHash = readFileSync87(stampPath, "utf8").trim();
117030
118030
  if (existingHash === targetHash) {
117031
118031
  return {
@@ -117038,12 +118038,12 @@ function ensurePythonEnv(opts) {
117038
118038
  };
117039
118039
  }
117040
118040
  }
117041
- if (existsSync93(venvDir)) {
118041
+ if (existsSync94(venvDir)) {
117042
118042
  rmSync16(venvDir, { recursive: true, force: true });
117043
118043
  }
117044
118044
  mkdirSync53(dirname42(venvDir), { recursive: true });
117045
118045
  try {
117046
- execFileSync27(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
118046
+ execFileSync28(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
117047
118047
  } catch (err) {
117048
118048
  const e = err;
117049
118049
  throw new PythonEnvError(`Failed to create venv for skill "${skillName}" with ${hostPython}: ${e.message}`, e.stderr?.toString());
@@ -117055,7 +118055,7 @@ function ensurePythonEnv(opts) {
117055
118055
  delete childEnv.PIP_TARGET;
117056
118056
  delete childEnv.PIP_PREFIX;
117057
118057
  delete childEnv.PYTHONUSERBASE;
117058
- execFileSync27(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
118058
+ execFileSync28(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
117059
118059
  } catch (err) {
117060
118060
  const e = err;
117061
118061
  throw new PythonEnvError(`Failed to install requirements for skill "${skillName}": ${e.message}`, e.stderr?.toString());
@@ -117076,7 +118076,7 @@ function ensurePythonEnv(opts) {
117076
118076
  import { createHash as createHash19 } from "node:crypto";
117077
118077
  import {
117078
118078
  copyFileSync as copyFileSync13,
117079
- existsSync as existsSync94,
118079
+ existsSync as existsSync95,
117080
118080
  mkdirSync as mkdirSync54,
117081
118081
  readFileSync as readFileSync88,
117082
118082
  rmSync as rmSync17,
@@ -117084,7 +118084,7 @@ import {
117084
118084
  } from "node:fs";
117085
118085
  import { dirname as dirname43, join as join98 } from "node:path";
117086
118086
  import { homedir as homedir52 } from "node:os";
117087
- import { execFileSync as execFileSync28 } from "node:child_process";
118087
+ import { execFileSync as execFileSync29 } from "node:child_process";
117088
118088
 
117089
118089
  class NodeEnvError extends Error {
117090
118090
  stderr;
@@ -117116,7 +118116,7 @@ function hashDepInputs(packageJsonPath) {
117116
118116
  hasher.update(readFileSync88(packageJsonPath));
117117
118117
  for (const lockName of ALL_LOCKFILES) {
117118
118118
  const lockPath = join98(sourceDir, lockName);
117119
- if (existsSync94(lockPath)) {
118119
+ if (existsSync95(lockPath)) {
117120
118120
  hasher.update(`
117121
118121
  `);
117122
118122
  hasher.update(lockName);
@@ -117131,7 +118131,7 @@ function ensureNodeEnv(opts) {
117131
118131
  const { skillName, packageJsonPath, force = false } = opts;
117132
118132
  const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
117133
118133
  const installer = opts.installer ?? "bun";
117134
- if (!existsSync94(packageJsonPath)) {
118134
+ if (!existsSync95(packageJsonPath)) {
117135
118135
  throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
117136
118136
  }
117137
118137
  const sourceDir = dirname43(packageJsonPath);
@@ -117140,7 +118140,7 @@ function ensureNodeEnv(opts) {
117140
118140
  const nodeModulesDir = join98(envDir, "node_modules");
117141
118141
  const binDir = join98(nodeModulesDir, ".bin");
117142
118142
  const targetHash = hashDepInputs(packageJsonPath);
117143
- if (!force && existsSync94(stampPath) && existsSync94(nodeModulesDir)) {
118143
+ if (!force && existsSync95(stampPath) && existsSync95(nodeModulesDir)) {
117144
118144
  const existingHash = readFileSync88(stampPath, "utf8").trim();
117145
118145
  if (existingHash === targetHash) {
117146
118146
  return {
@@ -117152,7 +118152,7 @@ function ensureNodeEnv(opts) {
117152
118152
  };
117153
118153
  }
117154
118154
  }
117155
- if (existsSync94(envDir)) {
118155
+ if (existsSync95(envDir)) {
117156
118156
  rmSync17(envDir, { recursive: true, force: true });
117157
118157
  }
117158
118158
  mkdirSync54(envDir, { recursive: true });
@@ -117160,7 +118160,7 @@ function ensureNodeEnv(opts) {
117160
118160
  let copiedLockfile = false;
117161
118161
  for (const lockName of LOCKFILES_FOR[installer]) {
117162
118162
  const lockPath = join98(sourceDir, lockName);
117163
- if (existsSync94(lockPath)) {
118163
+ if (existsSync95(lockPath)) {
117164
118164
  copyFileSync13(lockPath, join98(envDir, lockName));
117165
118165
  copiedLockfile = true;
117166
118166
  }
@@ -117168,10 +118168,10 @@ function ensureNodeEnv(opts) {
117168
118168
  try {
117169
118169
  if (installer === "bun") {
117170
118170
  const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
117171
- execFileSync28("bun", args, { cwd: envDir, stdio: "pipe" });
118171
+ execFileSync29("bun", args, { cwd: envDir, stdio: "pipe" });
117172
118172
  } else {
117173
118173
  const args = copiedLockfile ? ["ci"] : ["install"];
117174
- execFileSync28("npm", args, { cwd: envDir, stdio: "pipe" });
118174
+ execFileSync29("npm", args, { cwd: envDir, stdio: "pipe" });
117175
118175
  }
117176
118176
  } catch (err) {
117177
118177
  const e = err;
@@ -117196,22 +118196,22 @@ function registerDepsCommand(program3) {
117196
118196
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
117197
118197
  deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
117198
118198
  const skillsRoot = builtinSkillsRoot();
117199
- if (!existsSync95(skillsRoot)) {
118199
+ if (!existsSync96(skillsRoot)) {
117200
118200
  console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
117201
118201
  process.exit(1);
117202
118202
  }
117203
118203
  const skillDir = join99(skillsRoot, skill);
117204
- if (!existsSync95(skillDir)) {
118204
+ if (!existsSync96(skillDir)) {
117205
118205
  console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
117206
118206
  process.exit(1);
117207
118207
  }
117208
118208
  const requirementsPath = join99(skillDir, "requirements.txt");
117209
118209
  const packageJsonPath = join99(skillDir, "package.json");
117210
- const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync95(requirementsPath));
117211
- const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync95(packageJsonPath));
118210
+ const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync96(requirementsPath));
118211
+ const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync96(packageJsonPath));
117212
118212
  let did = 0;
117213
118213
  if (wantPython) {
117214
- if (!existsSync95(requirementsPath)) {
118214
+ if (!existsSync96(requirementsPath)) {
117215
118215
  console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
117216
118216
  process.exit(1);
117217
118217
  }
@@ -117235,7 +118235,7 @@ function registerDepsCommand(program3) {
117235
118235
  }
117236
118236
  }
117237
118237
  if (wantNode) {
117238
- if (!existsSync95(packageJsonPath)) {
118238
+ if (!existsSync96(packageJsonPath)) {
117239
118239
  console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
117240
118240
  process.exit(1);
117241
118241
  }
@@ -117268,7 +118268,7 @@ function registerDepsCommand(program3) {
117268
118268
  // src/cli/workspace.ts
117269
118269
  init_helpers();
117270
118270
  init_loader();
117271
- import { existsSync as existsSync96 } from "node:fs";
118271
+ import { existsSync as existsSync97 } from "node:fs";
117272
118272
  import { resolve as resolve53, sep as sep5 } from "node:path";
117273
118273
  import { spawnSync as spawnSync23 } from "node:child_process";
117274
118274
 
@@ -118045,7 +119045,7 @@ function registerWorkspaceCommand(program3) {
118045
119045
  if (!dir)
118046
119046
  return;
118047
119047
  const gitDir = resolve53(dir, ".git");
118048
- if (!existsSync96(gitDir)) {
119048
+ if (!existsSync97(gitDir)) {
118049
119049
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
118050
119050
  `);
118051
119051
  return;
@@ -118099,7 +119099,7 @@ function registerWorkspaceCommand(program3) {
118099
119099
  if (!dir)
118100
119100
  return;
118101
119101
  const gitDir = resolve53(dir, ".git");
118102
- if (!existsSync96(gitDir)) {
119102
+ if (!existsSync97(gitDir)) {
118103
119103
  process.stdout.write(`Workspace is not a git repository.
118104
119104
  `);
118105
119105
  return;
@@ -118124,7 +119124,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
118124
119124
  const agentsDir = resolveAgentsDir(config);
118125
119125
  const agentDir = resolve53(agentsDir, agentName);
118126
119126
  const dir = resolveAgentWorkspaceDir(agentDir);
118127
- if (!existsSync96(dir)) {
119127
+ if (!existsSync97(dir)) {
118128
119128
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
118129
119129
  `);
118130
119130
  return;
@@ -118160,7 +119160,7 @@ function safeParseInt(value, fallback) {
118160
119160
  init_helpers();
118161
119161
  init_loader();
118162
119162
  init_merge();
118163
- import { copyFileSync as copyFileSync14, existsSync as existsSync97, readFileSync as readFileSync89, writeFileSync as writeFileSync39 } from "node:fs";
119163
+ import { copyFileSync as copyFileSync14, existsSync as existsSync98, readFileSync as readFileSync89, writeFileSync as writeFileSync39 } from "node:fs";
118164
119164
  import { join as join100, resolve as resolve54 } from "node:path";
118165
119165
  init_scaffold();
118166
119166
  init_profiles();
@@ -118178,7 +119178,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
118178
119178
  const agentsDir = resolveAgentsDir(config);
118179
119179
  const agentDir = resolve54(agentsDir, agentName);
118180
119180
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
118181
- if (!existsSync97(workspaceDir)) {
119181
+ if (!existsSync98(workspaceDir)) {
118182
119182
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
118183
119183
  process.exit(1);
118184
119184
  }
@@ -118204,7 +119204,7 @@ function registerSoulCommand(program3) {
118204
119204
  const t = resolveSoulTargetOrExit(program3, agentName);
118205
119205
  if (!t)
118206
119206
  return;
118207
- if (!existsSync97(t.soulPath)) {
119207
+ if (!existsSync98(t.soulPath)) {
118208
119208
  console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
118209
119209
  process.exit(1);
118210
119210
  }
@@ -118219,7 +119219,7 @@ function registerSoulCommand(program3) {
118219
119219
  console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
118220
119220
  process.exit(1);
118221
119221
  }
118222
- const exists = existsSync97(t.soulPath);
119222
+ const exists = existsSync98(t.soulPath);
118223
119223
  if (exists && !opts.yes) {
118224
119224
  if (!isInteractive()) {
118225
119225
  console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
@@ -118234,7 +119234,7 @@ function registerSoulCommand(program3) {
118234
119234
  let backupPath;
118235
119235
  if (exists) {
118236
119236
  backupPath = `${t.soulPath}.bak`;
118237
- if (existsSync97(backupPath)) {
119237
+ if (existsSync98(backupPath)) {
118238
119238
  backupPath = `${t.soulPath}.bak.${Date.now()}`;
118239
119239
  }
118240
119240
  copyFileSync14(t.soulPath, backupPath);
@@ -118253,7 +119253,7 @@ function registerSoulCommand(program3) {
118253
119253
  // src/cli/debug.ts
118254
119254
  init_helpers();
118255
119255
  init_loader();
118256
- import { existsSync as existsSync98, readFileSync as readFileSync90, readdirSync as readdirSync33, statSync as statSync54 } from "node:fs";
119256
+ import { existsSync as existsSync99, readFileSync as readFileSync90, readdirSync as readdirSync33, statSync as statSync54 } from "node:fs";
118257
119257
  import { resolve as resolve55, join as join101 } from "node:path";
118258
119258
  import { createHash as createHash20 } from "node:crypto";
118259
119259
  init_merge();
@@ -118266,7 +119266,7 @@ function estimateTokens(bytes) {
118266
119266
  }
118267
119267
  function readMcpServerNames(agentDir) {
118268
119268
  const mcpPath = join101(agentDir, ".mcp.json");
118269
- if (!existsSync98(mcpPath))
119269
+ if (!existsSync99(mcpPath))
118270
119270
  return [];
118271
119271
  try {
118272
119272
  const parsed = JSON.parse(readFileSync90(mcpPath, "utf-8"));
@@ -118280,7 +119280,7 @@ function sha2562(content) {
118280
119280
  }
118281
119281
  function findLatestTranscriptJsonl(claudeConfigDir) {
118282
119282
  const projectsDir = join101(claudeConfigDir, "projects");
118283
- if (!existsSync98(projectsDir))
119283
+ if (!existsSync99(projectsDir))
118284
119284
  return;
118285
119285
  try {
118286
119286
  const entries = readdirSync33(projectsDir, { withFileTypes: true });
@@ -118290,7 +119290,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
118290
119290
  continue;
118291
119291
  const projectPath = join101(projectsDir, entry.name);
118292
119292
  const transcriptPath = join101(projectPath, "transcript.jsonl");
118293
- if (!existsSync98(transcriptPath))
119293
+ if (!existsSync99(transcriptPath))
118294
119294
  continue;
118295
119295
  const stat3 = statSync54(transcriptPath);
118296
119296
  if (!latest || stat3.mtimeMs > latest.mtime) {
@@ -118353,7 +119353,7 @@ function registerDebugCommand(program3) {
118353
119353
  }
118354
119354
  const agentsDir = resolveAgentsDir(config);
118355
119355
  const agentDir = resolve55(agentsDir, agentName);
118356
- if (!existsSync98(agentDir)) {
119356
+ if (!existsSync99(agentDir)) {
118357
119357
  console.error(`Agent directory not found: ${agentDir}`);
118358
119358
  process.exit(1);
118359
119359
  }
@@ -118408,7 +119408,7 @@ function registerDebugCommand(program3) {
118408
119408
  }
118409
119409
  console.log(`=== Append System Prompt (per-session) ===
118410
119410
  `);
118411
- const handoffContent = existsSync98(handoffPath) ? readFileSync90(handoffPath, "utf-8") : "";
119411
+ const handoffContent = existsSync99(handoffPath) ? readFileSync90(handoffPath, "utf-8") : "";
118412
119412
  if (handoffContent.trim().length > 0) {
118413
119413
  console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
118414
119414
  console.log(handoffContent);
@@ -118419,7 +119419,7 @@ function registerDebugCommand(program3) {
118419
119419
  }
118420
119420
  console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
118421
119421
  `);
118422
- const claudeMdContent = existsSync98(claudeMdPath) ? readFileSync90(claudeMdPath, "utf-8") : "";
119422
+ const claudeMdContent = existsSync99(claudeMdPath) ? readFileSync90(claudeMdPath, "utf-8") : "";
118423
119423
  if (claudeMdContent.trim().length > 0) {
118424
119424
  console.log(`(${formatBytes(claudeMdContent.length)})`);
118425
119425
  console.log(claudeMdContent);
@@ -118430,7 +119430,7 @@ function registerDebugCommand(program3) {
118430
119430
  }
118431
119431
  console.log(`=== Persona (SOUL.md) ===
118432
119432
  `);
118433
- const soulMdContent = existsSync98(soulMdPath) ? readFileSync90(soulMdPath, "utf-8") : existsSync98(workspaceSoulMdPath) ? readFileSync90(workspaceSoulMdPath, "utf-8") : "";
119433
+ const soulMdContent = existsSync99(soulMdPath) ? readFileSync90(soulMdPath, "utf-8") : existsSync99(workspaceSoulMdPath) ? readFileSync90(workspaceSoulMdPath, "utf-8") : "";
118434
119434
  if (soulMdContent.trim().length > 0) {
118435
119435
  console.log(`(${formatBytes(soulMdContent.length)})`);
118436
119436
  console.log(soulMdContent);
@@ -118494,8 +119494,8 @@ function registerDebugCommand(program3) {
118494
119494
  const fleetDir = join101(agentsDir, "..", "fleet");
118495
119495
  const fleetInvPath = join101(fleetDir, "switchroom-invariants.md");
118496
119496
  const fleetClaudePath = join101(fleetDir, "CLAUDE.md");
118497
- const fleetInvBytes = existsSync98(fleetInvPath) ? readFileSync90(fleetInvPath, "utf-8").length : 0;
118498
- const fleetClaudeBytes = existsSync98(fleetClaudePath) ? readFileSync90(fleetClaudePath, "utf-8").length : 0;
119497
+ const fleetInvBytes = existsSync99(fleetInvPath) ? readFileSync90(fleetInvPath, "utf-8").length : 0;
119498
+ const fleetClaudeBytes = existsSync99(fleetClaudePath) ? readFileSync90(fleetClaudePath, "utf-8").length : 0;
118499
119499
  const fleetBytes = fleetInvBytes + fleetClaudeBytes;
118500
119500
  const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
118501
119501
  console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
@@ -118527,8 +119527,8 @@ function registerDebugCommand(program3) {
118527
119527
  init_source();
118528
119528
 
118529
119529
  // src/worktree/claim.ts
118530
- import { execFileSync as execFileSync29 } from "node:child_process";
118531
- import { closeSync as closeSync17, mkdirSync as mkdirSync56, openSync as openSync17, existsSync as existsSync100, unlinkSync as unlinkSync21 } from "node:fs";
119530
+ import { execFileSync as execFileSync30 } from "node:child_process";
119531
+ import { closeSync as closeSync17, mkdirSync as mkdirSync56, openSync as openSync17, existsSync as existsSync101, unlinkSync as unlinkSync21 } from "node:fs";
118532
119532
  import { join as join103, resolve as resolve57 } from "node:path";
118533
119533
  import { homedir as homedir55 } from "node:os";
118534
119534
  import { randomBytes as randomBytes16 } from "node:crypto";
@@ -118540,7 +119540,7 @@ import {
118540
119540
  readFileSync as readFileSync91,
118541
119541
  readdirSync as readdirSync34,
118542
119542
  unlinkSync as unlinkSync20,
118543
- existsSync as existsSync99,
119543
+ existsSync as existsSync100,
118544
119544
  renameSync as renameSync25
118545
119545
  } from "node:fs";
118546
119546
  import { join as join102, resolve as resolve56 } from "node:path";
@@ -118654,7 +119654,7 @@ function expandHome(p) {
118654
119654
  }
118655
119655
  async function claimWorktree(input, codeRepos) {
118656
119656
  const repoPath = resolveRepoPath(input.repo, codeRepos);
118657
- if (!existsSync100(repoPath)) {
119657
+ if (!existsSync101(repoPath)) {
118658
119658
  throw new Error(`Repository path does not exist: ${repoPath}`);
118659
119659
  }
118660
119660
  let concurrencyCap = DEFAULT_CONCURRENCY;
@@ -118696,7 +119696,7 @@ async function claimWorktree(input, codeRepos) {
118696
119696
  releaseLock();
118697
119697
  }
118698
119698
  try {
118699
- execFileSync29("git", ["worktree", "add", "-b", branch, worktreePath], {
119699
+ execFileSync30("git", ["worktree", "add", "-b", branch, worktreePath], {
118700
119700
  cwd: repoPath,
118701
119701
  stdio: "pipe"
118702
119702
  });
@@ -118709,8 +119709,8 @@ async function claimWorktree(input, codeRepos) {
118709
119709
  }
118710
119710
 
118711
119711
  // src/worktree/release.ts
118712
- import { execFileSync as execFileSync30 } from "node:child_process";
118713
- import { existsSync as existsSync101 } from "node:fs";
119712
+ import { execFileSync as execFileSync31 } from "node:child_process";
119713
+ import { existsSync as existsSync102 } from "node:fs";
118714
119714
  function releaseWorktree(input) {
118715
119715
  const { id } = input;
118716
119716
  const record2 = readRecord(id);
@@ -118718,9 +119718,9 @@ function releaseWorktree(input) {
118718
119718
  return { released: true };
118719
119719
  }
118720
119720
  let gitSuccess = true;
118721
- if (existsSync101(record2.path)) {
119721
+ if (existsSync102(record2.path)) {
118722
119722
  try {
118723
- execFileSync30("git", ["worktree", "remove", "--force", record2.path], {
119723
+ execFileSync31("git", ["worktree", "remove", "--force", record2.path], {
118724
119724
  cwd: record2.repo,
118725
119725
  stdio: "pipe"
118726
119726
  });
@@ -118756,8 +119756,8 @@ function listWorktrees() {
118756
119756
  }
118757
119757
 
118758
119758
  // src/worktree/reaper.ts
118759
- import { execFileSync as execFileSync31 } from "node:child_process";
118760
- import { existsSync as existsSync102 } from "node:fs";
119759
+ import { execFileSync as execFileSync32 } from "node:child_process";
119760
+ import { existsSync as existsSync103 } from "node:fs";
118761
119761
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
118762
119762
  function reapSkipReasonText(action) {
118763
119763
  switch (action) {
@@ -118774,7 +119774,7 @@ function reapSkipReasonText(action) {
118774
119774
  function probePathInUse(path10) {
118775
119775
  let probeRan = false;
118776
119776
  try {
118777
- execFileSync31("fuser", [path10], { stdio: "pipe" });
119777
+ execFileSync32("fuser", [path10], { stdio: "pipe" });
118778
119778
  return "in-use";
118779
119779
  } catch (err) {
118780
119780
  if (err.code !== "ENOENT") {
@@ -118782,7 +119782,7 @@ function probePathInUse(path10) {
118782
119782
  }
118783
119783
  }
118784
119784
  try {
118785
- const out = execFileSync31("lsof", ["-t", path10], {
119785
+ const out = execFileSync32("lsof", ["-t", path10], {
118786
119786
  stdio: ["ignore", "pipe", "ignore"]
118787
119787
  }).toString().trim();
118788
119788
  probeRan = true;
@@ -118797,7 +119797,7 @@ function probePathInUse(path10) {
118797
119797
  }
118798
119798
  function hasUncommittedChanges(_repoPath, worktreePath) {
118799
119799
  try {
118800
- const out = execFileSync31("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
119800
+ const out = execFileSync32("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
118801
119801
  return out.trim().length > 0;
118802
119802
  } catch {
118803
119803
  return true;
@@ -118805,7 +119805,7 @@ function hasUncommittedChanges(_repoPath, worktreePath) {
118805
119805
  }
118806
119806
  function defaultRemoveWorktree(repoPath, worktreePath) {
118807
119807
  try {
118808
- execFileSync31("git", ["worktree", "remove", "--force", worktreePath], {
119808
+ execFileSync32("git", ["worktree", "remove", "--force", worktreePath], {
118809
119809
  cwd: repoPath,
118810
119810
  stdio: "pipe"
118811
119811
  });
@@ -118818,7 +119818,7 @@ function planReaper(nowMs, deps = {}) {
118818
119818
  const plan = [];
118819
119819
  for (const record2 of listRecords()) {
118820
119820
  const heartbeatAge = now2 - new Date(record2.heartbeatAt).getTime();
118821
- const worktreeExists = existsSync102(record2.path);
119821
+ const worktreeExists = existsSync103(record2.path);
118822
119822
  if (!worktreeExists) {
118823
119823
  plan.push({
118824
119824
  record: record2,
@@ -118897,9 +119897,9 @@ function runReaper(nowMs, deps = {}) {
118897
119897
  }
118898
119898
 
118899
119899
  // src/worktree/gc.ts
118900
- import { execFileSync as execFileSync32 } from "node:child_process";
119900
+ import { execFileSync as execFileSync33 } from "node:child_process";
118901
119901
  import {
118902
- existsSync as existsSync103,
119902
+ existsSync as existsSync104,
118903
119903
  readFileSync as readFileSync92,
118904
119904
  readdirSync as readdirSync35,
118905
119905
  statSync as statSync55,
@@ -118920,7 +119920,7 @@ function repoRootFromWorktreeGitdir(gitdir) {
118920
119920
  return gitdir.slice(0, idx);
118921
119921
  }
118922
119922
  function isSwitchroomRemote(originUrl) {
118923
- return /[:/]switchroom\/switchroom(\.git)?\/?$/.test(originUrl.trim());
119923
+ return /[:/](?:switchroom|mekenthompson)\/switchroom(\.git)?\/?$/.test(originUrl.trim());
118924
119924
  }
118925
119925
  function parseWorktreeList(porcelain) {
118926
119926
  const entries = [];
@@ -119024,7 +120024,7 @@ function classifyTaskTree(i2) {
119024
120024
  return "skip-unmerged";
119025
120025
  return "reap";
119026
120026
  }
119027
- var defaultExec3 = (file, args, cwd) => execFileSync32(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
120027
+ var defaultExec3 = (file, args, cwd) => execFileSync33(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
119028
120028
  function defaultPrSignal(repo, branch, exec) {
119029
120029
  try {
119030
120030
  const out = exec("gh", [
@@ -119078,7 +120078,7 @@ function trashRoot() {
119078
120078
  return resolve58(process.env.SWITCHROOM_WORKTREE_TRASH ?? join104(homedir56(), ".switchroom", "worktree-gc-trash"));
119079
120079
  }
119080
120080
  function planGc(roots, deps = {}, taskTreeRoots = []) {
119081
- const exists = deps.existsSync ?? existsSync103;
120081
+ const exists = deps.existsSync ?? existsSync104;
119082
120082
  const readDir = deps.readDir ?? ((p) => readdirSync35(p));
119083
120083
  const readFile4 = deps.readFile ?? ((p) => readFileSync92(p, "utf8"));
119084
120084
  const stat3 = deps.stat ?? ((p) => statSync55(p));
@@ -119397,7 +120397,7 @@ function selectPurgeTargets(entries, olderThanDays) {
119397
120397
  return entries.filter((e) => e.ageDays >= olderThanDays).map((e) => e.path);
119398
120398
  }
119399
120399
  function listTrashEntries(nowMs, deps = {}) {
119400
- const exists = deps.existsSync ?? existsSync103;
120400
+ const exists = deps.existsSync ?? existsSync104;
119401
120401
  const readDir = deps.readDir ?? ((p) => readdirSync35(p));
119402
120402
  const root = trashRoot();
119403
120403
  if (!exists(root))
@@ -119450,7 +120450,7 @@ function defaultTaskTreeRoots() {
119450
120450
  for (const name of names.sort()) {
119451
120451
  for (const sub of ["home/work", "home/workspace"]) {
119452
120452
  const r = join104(agentsDir, name, sub);
119453
- if (existsSync103(r))
120453
+ if (existsSync104(r))
119454
120454
  roots.push(r);
119455
120455
  }
119456
120456
  }
@@ -120461,7 +121461,7 @@ function registerM365McpLauncherCommand(program3) {
120461
121461
  // src/cli/notion-mcp-launcher.ts
120462
121462
  init_scaffold_integration();
120463
121463
  import { spawn as spawn7 } from "node:child_process";
120464
- import { existsSync as existsSync104, mkdirSync as mkdirSync60, writeFileSync as writeFileSync43 } from "node:fs";
121464
+ import { existsSync as existsSync105, mkdirSync as mkdirSync60, writeFileSync as writeFileSync43 } from "node:fs";
120465
121465
  import { dirname as dirname45 } from "node:path";
120466
121466
  var HEARTBEAT_WRITE_INTERVAL_MS = 30 * 1000;
120467
121467
  var DEFAULT_HEARTBEAT_PATH = "/state/agent/notion-launcher.heartbeat.json";
@@ -120473,7 +121473,7 @@ function buildNotionMcpArgs(opts) {
120473
121473
  function defaultWriteHeartbeat(path10, contents) {
120474
121474
  try {
120475
121475
  const dir = dirname45(path10);
120476
- if (!existsSync104(dir))
121476
+ if (!existsSync105(dir))
120477
121477
  mkdirSync60(dir, { recursive: true });
120478
121478
  writeFileSync43(path10, contents);
120479
121479
  } catch {}
@@ -120589,6 +121589,92 @@ import { mkdirSync as mkdirSync61, readFileSync as readFileSync93, renameSync as
120589
121589
  import { tmpdir as tmpdir5 } from "node:os";
120590
121590
  import { join as join107 } from "node:path";
120591
121591
  import { createInterface as createInterface7 } from "node:readline";
121592
+
121593
+ // src/memory/hindsight-knowledge-admin.ts
121594
+ var KNOWLEDGE_SEARCH_LIMIT_MIN = 1;
121595
+ var KNOWLEDGE_SEARCH_LIMIT_MAX = 50;
121596
+ var KNOWLEDGE_SEARCH_LIMIT_DEFAULT = 10;
121597
+ function clampKnowledgeSearchLimit(limit) {
121598
+ if (limit === undefined || !Number.isFinite(limit)) {
121599
+ return KNOWLEDGE_SEARCH_LIMIT_DEFAULT;
121600
+ }
121601
+ return Math.min(KNOWLEDGE_SEARCH_LIMIT_MAX, Math.max(KNOWLEDGE_SEARCH_LIMIT_MIN, Math.trunc(limit)));
121602
+ }
121603
+ var KNOWLEDGE_ADMIN_TIMEOUT_MS = 15000;
121604
+ var KNOWLEDGE_PAGE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
121605
+
121606
+ class KnowledgeAdmin {
121607
+ opts;
121608
+ constructor(opts) {
121609
+ this.opts = opts;
121610
+ }
121611
+ get fetchImpl() {
121612
+ return this.opts.fetchImpl ?? fetch;
121613
+ }
121614
+ knowledgeBasePath() {
121615
+ const base = this.opts.apiBaseUrl.replace(/\/+$/, "");
121616
+ return `${base}/v1/default/banks/${encodeURIComponent(this.opts.bankId)}/knowledge-base`;
121617
+ }
121618
+ async get(url) {
121619
+ const ctl = new AbortController;
121620
+ const timer = setTimeout(() => ctl.abort(), this.opts.timeoutMs ?? KNOWLEDGE_ADMIN_TIMEOUT_MS);
121621
+ try {
121622
+ return await this.fetchImpl(url, {
121623
+ method: "GET",
121624
+ headers: {
121625
+ accept: "application/json",
121626
+ ...this.opts.bankId ? { "X-Bank-Id": this.opts.bankId } : {}
121627
+ },
121628
+ signal: ctl.signal
121629
+ });
121630
+ } finally {
121631
+ clearTimeout(timer);
121632
+ }
121633
+ }
121634
+ async search(args) {
121635
+ const params = new URLSearchParams({
121636
+ q: args.query,
121637
+ limit: String(clampKnowledgeSearchLimit(args.limit))
121638
+ });
121639
+ const res = await this.get(`${this.knowledgeBasePath()}/search?${params}`);
121640
+ if (!res.ok) {
121641
+ throw new Error(`searching knowledge pages in bank '${this.opts.bankId}' failed: HTTP ${res.status}`);
121642
+ }
121643
+ const parsed = await res.json();
121644
+ const results = Array.isArray(parsed.results) ? parsed.results : [];
121645
+ return {
121646
+ results,
121647
+ total: typeof parsed.total === "number" ? parsed.total : results.length
121648
+ };
121649
+ }
121650
+ async getPage(args) {
121651
+ if (!KNOWLEDGE_PAGE_ID_PATTERN.test(args.page_id)) {
121652
+ throw new Error(`'${args.page_id}' is not a knowledge page id. Ids look like ` + `'kp-<hex>' (letters, digits, '-' and '_' only) and come from ` + `search_knowledge_pages or get_knowledge_tree.`);
121653
+ }
121654
+ const res = await this.get(`${this.knowledgeBasePath()}/pages/${encodeURIComponent(args.page_id)}`);
121655
+ if (res.status === 404) {
121656
+ throw new Error(`no knowledge page '${args.page_id}' in bank '${this.opts.bankId}'. ` + `Page ids come from search_knowledge_pages or get_knowledge_tree.`);
121657
+ }
121658
+ if (!res.ok) {
121659
+ throw new Error(`reading knowledge page '${args.page_id}' in bank ` + `'${this.opts.bankId}' failed: HTTP ${res.status}`);
121660
+ }
121661
+ const page = await res.json();
121662
+ if (typeof page.markdown !== "string") {
121663
+ throw new Error(`knowledge page '${args.page_id}' in bank '${this.opts.bankId}' came ` + `back with no markdown body \u2014 the response had no string 'markdown' ` + `field, so there is nothing to read.`);
121664
+ }
121665
+ return page;
121666
+ }
121667
+ async tree() {
121668
+ const res = await this.get(`${this.knowledgeBasePath()}/tree`);
121669
+ if (!res.ok) {
121670
+ throw new Error(`listing the knowledge tree in bank '${this.opts.bankId}' failed: HTTP ${res.status}`);
121671
+ }
121672
+ const parsed = await res.json();
121673
+ return { roots: Array.isArray(parsed.roots) ? parsed.roots : [] };
121674
+ }
121675
+ }
121676
+
121677
+ // src/cli/hindsight-mcp-shim.ts
120592
121678
  init_hindsight();
120593
121679
  var SHIM_SUPPORTED_PROTOCOL_VERSIONS = [
120594
121680
  "2025-06-18",
@@ -120657,9 +121743,80 @@ var SYNTHESIZED_TOOL_TABLE = {
120657
121743
  description: "Exact name of the directive to reactivate."
120658
121744
  }
120659
121745
  }
121746
+ },
121747
+ search_knowledge_pages: {
121748
+ description: "Search YOUR OWN knowledge pages \u2014 the curated, continuously-refreshed " + "summaries of what this bank knows \u2014 with hybrid full-text + semantic " + "search. Reach for this before re-deriving a standing answer from raw " + "recall: a page is already synthesized, where recall returns fragments. " + "Returns ranked hits with a relevance snippet and a page id; read the " + "whole page with get_knowledge_page. Read-only, and only ever your own " + "memory bank. An empty knowledge base returns no results, not an error.",
121749
+ required: ["query"],
121750
+ props: {
121751
+ query: {
121752
+ type: "string",
121753
+ description: "What to look for, in natural language."
121754
+ },
121755
+ limit: {
121756
+ type: "integer",
121757
+ minimum: KNOWLEDGE_SEARCH_LIMIT_MIN,
121758
+ maximum: KNOWLEDGE_SEARCH_LIMIT_MAX,
121759
+ description: `Maximum hits to return (${KNOWLEDGE_SEARCH_LIMIT_MIN}-` + `${KNOWLEDGE_SEARCH_LIMIT_MAX}, default ${KNOWLEDGE_SEARCH_LIMIT_DEFAULT}).`
121760
+ }
121761
+ }
121762
+ },
121763
+ get_knowledge_page: {
121764
+ description: "Read one of YOUR OWN knowledge pages in full, by the page id returned " + "by search_knowledge_pages or get_knowledge_tree. Returns the complete " + "markdown document (YAML frontmatter + synthesized body). Prefer this " + "over rebuilding the same understanding from scratch. Read-only: it " + "returns the page as it currently stands and never triggers a refresh.",
121765
+ required: ["page_id"],
121766
+ props: {
121767
+ page_id: {
121768
+ type: "string",
121769
+ description: "Id of the page to read, from search_knowledge_pages or " + "get_knowledge_tree."
121770
+ }
121771
+ }
121772
+ },
121773
+ get_knowledge_tree: {
121774
+ description: "List YOUR OWN knowledge base as a folder/page tree \u2014 every page's id, " + "name, source query, tags and staleness flag. Call this to see what " + "this bank already knows before reading anything else; is_stale=true " + "means the page MAY be behind newer memories, not that it is wrong. " + "Read-only, and only ever your own memory bank.",
121775
+ required: [],
121776
+ props: {}
120660
121777
  }
120661
121778
  };
120662
121779
  var SYNTHESIZED_TOOL_NAMES = Object.keys(SYNTHESIZED_TOOL_TABLE);
121780
+ var INTEGER_STRING_PATTERN = /^-?(?:0|[1-9]\d*)$/;
121781
+ function coerceSynthesizedArg(toolName, key, propSchema, value) {
121782
+ const schema = propSchema ?? {};
121783
+ if (schema.type !== "integer") {
121784
+ if (typeof value !== "string" || value.length === 0) {
121785
+ return {
121786
+ ok: false,
121787
+ text: `${toolName}: '${key}' must be a non-empty string.`
121788
+ };
121789
+ }
121790
+ return { ok: true, value };
121791
+ }
121792
+ const min = schema.minimum;
121793
+ const max = schema.maximum;
121794
+ const range = min !== undefined && max !== undefined ? ` between ${min} and ${max}` : "";
121795
+ let n;
121796
+ if (typeof value === "number") {
121797
+ n = value;
121798
+ } else if (typeof value === "string" && INTEGER_STRING_PATTERN.test(value)) {
121799
+ n = Number(value);
121800
+ } else {
121801
+ return {
121802
+ ok: false,
121803
+ text: `${toolName}: '${key}' must be an integer${range}.`
121804
+ };
121805
+ }
121806
+ if (!Number.isSafeInteger(n)) {
121807
+ return {
121808
+ ok: false,
121809
+ text: `${toolName}: '${key}' must be an integer${range}.`
121810
+ };
121811
+ }
121812
+ if (min !== undefined && n < min || max !== undefined && n > max) {
121813
+ return {
121814
+ ok: false,
121815
+ text: `${toolName}: '${key}' must be an integer${range} \u2014 got ${n}. ` + `Re-issue the call with a value in range.`
121816
+ };
121817
+ }
121818
+ return { ok: true, value: n };
121819
+ }
120663
121820
  function buildSynthesizedToolsList() {
120664
121821
  return Object.entries(SYNTHESIZED_TOOL_TABLE).map(([name, { description, required, props }]) => ({
120665
121822
  name,
@@ -120738,6 +121895,52 @@ var DEFAULT_RECALL_MAX_TOKENS = 1024;
120738
121895
  var DEFAULT_REFLECT_MAX_TOKENS = 1024;
120739
121896
  var DEFAULT_RECALL_BUDGET = "low";
120740
121897
  var DEFAULT_REFLECT_BUDGET = "mid";
121898
+ var KNOWLEDGE_RESPONSE_MAX_CHARS = 16384;
121899
+ var KNOWLEDGE_TREE_MARKER_RESERVE = 256;
121900
+ function capKnowledgeResponse(text) {
121901
+ if (text.length <= KNOWLEDGE_RESPONSE_MAX_CHARS)
121902
+ return text;
121903
+ const omitted = text.length - KNOWLEDGE_RESPONSE_MAX_CHARS;
121904
+ return text.slice(0, KNOWLEDGE_RESPONSE_MAX_CHARS) + `
121905
+
121906
+ \u2026truncated at ${KNOWLEDGE_RESPONSE_MAX_CHARS} chars \u2014 ${omitted} ` + `chars omitted. This is a PARTIAL read; narrow it with ` + `search_knowledge_pages rather than treating the above as the whole.`;
121907
+ }
121908
+ function countKnowledgeNodes(nodes) {
121909
+ let n = 0;
121910
+ for (const node of nodes)
121911
+ n += 1 + countKnowledgeNodes(node.children ?? []);
121912
+ return n;
121913
+ }
121914
+ function takeKnowledgeNodes(nodes, budget) {
121915
+ const out = [];
121916
+ for (const node of nodes) {
121917
+ if (budget.left <= 0)
121918
+ break;
121919
+ budget.left -= 1;
121920
+ out.push(node.children === undefined ? node : { ...node, children: takeKnowledgeNodes(node.children, budget) });
121921
+ }
121922
+ return out;
121923
+ }
121924
+ function renderKnowledgeTree(roots) {
121925
+ const total = countKnowledgeNodes(roots);
121926
+ const whole = JSON.stringify(roots);
121927
+ if (whole.length <= KNOWLEDGE_RESPONSE_MAX_CHARS)
121928
+ return whole;
121929
+ const charBudget = KNOWLEDGE_RESPONSE_MAX_CHARS - KNOWLEDGE_TREE_MARKER_RESERVE;
121930
+ let allowed = total;
121931
+ let kept = takeKnowledgeNodes(roots, { left: allowed });
121932
+ let json = JSON.stringify(kept);
121933
+ while (json.length > charBudget && allowed > 0) {
121934
+ allowed = Math.max(0, Math.min(allowed - 1, Math.floor(allowed * (charBudget / json.length))));
121935
+ kept = takeKnowledgeNodes(roots, { left: allowed });
121936
+ json = JSON.stringify(kept);
121937
+ }
121938
+ const shown = countKnowledgeNodes(kept);
121939
+ if (shown === total)
121940
+ return json;
121941
+ return json + `
121942
+ \u2026${total - shown} of ${total} nodes omitted \u2014 the knowledge tree was ` + `truncated to fit the response budget. This is a PARTIAL listing; use ` + `search_knowledge_pages to find a specific page.`;
121943
+ }
120741
121944
  var VALID_BUDGETS = new Set(["low", "mid", "high"]);
120742
121945
  function resolveClampMaxTokens(envVal, fallback) {
120743
121946
  if (envVal === undefined || envVal === "")
@@ -121072,6 +122275,17 @@ class HindsightShim {
121072
122275
  }
121073
122276
  return this.directiveAdminCache;
121074
122277
  }
122278
+ knowledgeAdminCache = null;
122279
+ get knowledgeAdmin() {
122280
+ if (!this.knowledgeAdminCache) {
122281
+ this.knowledgeAdminCache = new KnowledgeAdmin({
122282
+ apiBaseUrl: this.opts.apiBaseUrl ?? this.opts.url.replace(/\/mcp\/?$/, ""),
122283
+ bankId: this.opts.bankId,
122284
+ ...this.opts.fetchImpl ? { fetchImpl: this.opts.fetchImpl } : {}
122285
+ });
122286
+ }
122287
+ return this.knowledgeAdminCache;
122288
+ }
121075
122289
  async synthesizedCall(name, rawArgs) {
121076
122290
  const fail4 = (text) => ({
121077
122291
  content: [{ type: "text", text }],
@@ -121084,26 +122298,23 @@ class HindsightShim {
121084
122298
  if (unknown.length > 0) {
121085
122299
  return fail4(`${name} does not accept ${unknown.map((u) => `'${u}'`).join(", ")}. ` + `Accepted arguments: ${[...allowed].join(", ")}. ` + (unknown.includes("bank_id") ? "This tool always operates on your own memory bank; there is no " + "way to target another agent's bank through it." : ""));
121086
122300
  }
122301
+ const clean3 = {};
121087
122302
  for (const key of Object.keys(args)) {
121088
- if (typeof args[key] !== "string" || args[key].length === 0) {
121089
- return fail4(`${name}: '${key}' must be a non-empty string.`);
121090
- }
122303
+ const coerced = coerceSynthesizedArg(name, key, spec.props[key], args[key]);
122304
+ if (!coerced.ok)
122305
+ return fail4(coerced.text);
122306
+ clean3[key] = coerced.value;
121091
122307
  }
121092
122308
  for (const req of spec.required) {
121093
- if (args[req] === undefined) {
122309
+ if (clean3[req] === undefined) {
121094
122310
  return fail4(`${name} requires '${req}'.`);
121095
122311
  }
121096
122312
  }
121097
122313
  if (!this.opts.bankId) {
121098
- return fail4(`${name} is unavailable: this agent has no HINDSIGHT_BANK_ID, so ` + "there is no bank to pin the change to.");
122314
+ return fail4(`${name} is unavailable: this agent has no HINDSIGHT_BANK_ID, so ` + "there is no bank to pin it to.");
121099
122315
  }
121100
122316
  try {
121101
- const text = name === "deactivate_directive" ? await this.directiveAdmin.deactivate({
121102
- name: args.name,
121103
- ...typeof args.superseded_by === "string" ? { supersededBy: args.superseded_by } : {}
121104
- }) : await this.directiveAdmin.reactivate({
121105
- name: args.name
121106
- });
122317
+ const text = await this.runSynthesized(name, clean3);
121107
122318
  return { content: [{ type: "text", text }], isError: false };
121108
122319
  } catch (err) {
121109
122320
  if (err instanceof DirectivePairInconsistentError) {
@@ -121112,6 +122323,42 @@ class HindsightShim {
121112
122323
  return fail4(`${name} failed: ${String(err)}`);
121113
122324
  }
121114
122325
  }
122326
+ async runSynthesized(name, args) {
122327
+ switch (name) {
122328
+ case "deactivate_directive":
122329
+ return this.directiveAdmin.deactivate({
122330
+ name: args.name,
122331
+ ...typeof args.superseded_by === "string" ? { supersededBy: args.superseded_by } : {}
122332
+ });
122333
+ case "reactivate_directive":
122334
+ return this.directiveAdmin.reactivate({ name: args.name });
122335
+ case "search_knowledge_pages": {
122336
+ const res = await this.knowledgeAdmin.search({
122337
+ query: args.query,
122338
+ ...typeof args.limit === "number" ? { limit: args.limit } : {}
122339
+ });
122340
+ if (res.results.length === 0) {
122341
+ return "No knowledge pages matched that query in your memory bank. " + "The bank may have no pages yet \u2014 get_knowledge_tree lists what " + "exists.";
122342
+ }
122343
+ return capKnowledgeResponse(JSON.stringify({ total: res.total, results: res.results }, null, 2));
122344
+ }
122345
+ case "get_knowledge_page": {
122346
+ const page = await this.knowledgeAdmin.getPage({
122347
+ page_id: args.page_id
122348
+ });
122349
+ return capKnowledgeResponse(page.markdown);
122350
+ }
122351
+ case "get_knowledge_tree": {
122352
+ const tree = await this.knowledgeAdmin.tree();
122353
+ if (tree.roots.length === 0) {
122354
+ return "Your knowledge base has no pages yet. Pages are synthesized from " + "mental models \u2014 propose one to start it.";
122355
+ }
122356
+ return renderKnowledgeTree(tree.roots);
122357
+ }
122358
+ default:
122359
+ throw new Error(`${name} is advertised as a synthesized tool but has no dispatch arm`);
122360
+ }
122361
+ }
121115
122362
  async toolsCall(params) {
121116
122363
  const called = params;
121117
122364
  if (called?.name && SYNTHESIZED_TOOL_NAMES.includes(called.name)) {
@@ -121891,7 +123138,7 @@ function runRedactStdin() {
121891
123138
  }
121892
123139
 
121893
123140
  // src/cli/status-ask.ts
121894
- import { readFileSync as readFileSync95, existsSync as existsSync105, readdirSync as readdirSync37 } from "node:fs";
123141
+ import { readFileSync as readFileSync95, existsSync as existsSync106, readdirSync as readdirSync37 } from "node:fs";
121895
123142
  import { join as join108 } from "node:path";
121896
123143
  import { homedir as homedir57 } from "node:os";
121897
123144
 
@@ -122214,7 +123461,7 @@ function runReport(opts) {
122214
123461
  function resolveSources(explicitPath) {
122215
123462
  if (explicitPath != null && explicitPath.trim() !== "") {
122216
123463
  const trimmed = explicitPath.trim();
122217
- if (!existsSync105(trimmed)) {
123464
+ if (!existsSync106(trimmed)) {
122218
123465
  process.stderr.write(`status-ask report: ${trimmed}: file not found
122219
123466
  `);
122220
123467
  process.exit(1);
@@ -122230,7 +123477,7 @@ function resolveSources(explicitPath) {
122230
123477
  } catch {
122231
123478
  agentsDir = join108(homedir57(), ".switchroom", "agents");
122232
123479
  }
122233
- if (!existsSync105(agentsDir))
123480
+ if (!existsSync106(agentsDir))
122234
123481
  return [];
122235
123482
  const sources = [];
122236
123483
  let entries;
@@ -122241,7 +123488,7 @@ function resolveSources(explicitPath) {
122241
123488
  }
122242
123489
  for (const name of entries) {
122243
123490
  const path10 = join108(agentsDir, name, "runtime-metrics.jsonl");
122244
- if (existsSync105(path10)) {
123491
+ if (existsSync106(path10)) {
122245
123492
  sources.push({ path: path10, agent: name });
122246
123493
  }
122247
123494
  }
@@ -122272,7 +123519,7 @@ init_paths();
122272
123519
  import {
122273
123520
  chownSync as chownSync12,
122274
123521
  closeSync as closeSync18,
122275
- existsSync as existsSync106,
123522
+ existsSync as existsSync107,
122276
123523
  fsyncSync as fsyncSync8,
122277
123524
  mkdirSync as mkdirSync62,
122278
123525
  openSync as openSync18,
@@ -122402,7 +123649,7 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
122402
123649
  const paths = overlayPathsFor(agent, opts);
122403
123650
  return withAgentLock(paths, () => {
122404
123651
  const finalPath = join109(paths.skillsDir, `${slug}.yaml`);
122405
- if (!existsSync106(finalPath))
123652
+ if (!existsSync107(finalPath))
122406
123653
  return false;
122407
123654
  unlinkSync22(finalPath);
122408
123655
  return true;
@@ -122410,7 +123657,7 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
122410
123657
  }
122411
123658
  function listSkillsOverlayEntries(agent, opts = {}) {
122412
123659
  const paths = overlayPathsFor(agent, opts);
122413
- if (!existsSync106(paths.skillsDir))
123660
+ if (!existsSync107(paths.skillsDir))
122414
123661
  return [];
122415
123662
  const out = [];
122416
123663
  for (const name of readdirSync38(paths.skillsDir)) {
@@ -122429,7 +123676,7 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
122429
123676
  const paths = overlayPathsFor(agent, opts);
122430
123677
  return withAgentLock(paths, () => {
122431
123678
  const finalPath = join109(paths.scheduleDir, `${slug}.yaml`);
122432
- if (!existsSync106(finalPath))
123679
+ if (!existsSync107(finalPath))
122433
123680
  return false;
122434
123681
  unlinkSync22(finalPath);
122435
123682
  return true;
@@ -122437,7 +123684,7 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
122437
123684
  }
122438
123685
  function listOverlayEntries(agent, opts = {}) {
122439
123686
  const paths = overlayPathsFor(agent, opts);
122440
- if (!existsSync106(paths.scheduleDir))
123687
+ if (!existsSync107(paths.scheduleDir))
122441
123688
  return [];
122442
123689
  const out = [];
122443
123690
  for (const name of readdirSync38(paths.scheduleDir)) {
@@ -122665,7 +123912,7 @@ function reconcileAgentCronOnly(agent) {
122665
123912
  // src/cli/agent-config-pending.ts
122666
123913
  import {
122667
123914
  closeSync as closeSync19,
122668
- existsSync as existsSync107,
123915
+ existsSync as existsSync108,
122669
123916
  fsyncSync as fsyncSync9,
122670
123917
  mkdirSync as mkdirSync63,
122671
123918
  openSync as openSync19,
@@ -122722,7 +123969,7 @@ function stagePendingScheduleEntry(opts) {
122722
123969
  }
122723
123970
  function listPendingScheduleEntries(agent, opts = {}) {
122724
123971
  const dir = pendingDir(agent, opts);
122725
- if (!existsSync107(dir))
123972
+ if (!existsSync108(dir))
122726
123973
  return [];
122727
123974
  const out = [];
122728
123975
  for (const name of readdirSync39(dir).sort()) {
@@ -122731,7 +123978,7 @@ function listPendingScheduleEntries(agent, opts = {}) {
122731
123978
  const stageId = name.slice(0, -".meta.json".length);
122732
123979
  const metaPath = join110(dir, name);
122733
123980
  const yamlPath = join110(dir, `${stageId}.yaml`);
122734
- if (!existsSync107(yamlPath))
123981
+ if (!existsSync108(yamlPath))
122735
123982
  continue;
122736
123983
  try {
122737
123984
  const meta = JSON.parse(readFileSync97(metaPath, "utf-8"));
@@ -122750,7 +123997,7 @@ function commitPendingScheduleEntry(opts) {
122750
123997
  const slug = match.meta.entry.name ?? match.stageId;
122751
123998
  const paths = overlayPathsFor(opts.agent, { root: opts.root });
122752
123999
  const finalPath = join110(paths.scheduleDir, `${slug}.yaml`);
122753
- if (existsSync107(finalPath)) {
124000
+ if (existsSync108(finalPath)) {
122754
124001
  return { committed: false, reason: "slug_collision" };
122755
124002
  }
122756
124003
  renameSync29(match.yamlPath, finalPath);
@@ -122772,8 +124019,8 @@ function denyPendingScheduleEntry(opts) {
122772
124019
  }
122773
124020
 
122774
124021
  // src/cli/agent-config-write.ts
122775
- import { existsSync as existsSync108, readFileSync as readFileSync98 } from "node:fs";
122776
- import { execFileSync as execFileSync33 } from "node:child_process";
124022
+ import { existsSync as existsSync109, readFileSync as readFileSync98 } from "node:fs";
124023
+ import { execFileSync as execFileSync34 } from "node:child_process";
122777
124024
 
122778
124025
  // src/scheduler/schedule-report.ts
122779
124026
  var TIER_WEIGHT = { poll: 0, action: 0, cheap: 1, main: 5 };
@@ -123162,7 +124409,7 @@ function scheduleRemove(opts) {
123162
124409
  }
123163
124410
  let priorContent = null;
123164
124411
  try {
123165
- if (existsSync108(match.path))
124412
+ if (existsSync109(match.path))
123166
124413
  priorContent = readFileSync98(match.path, "utf-8");
123167
124414
  } catch {}
123168
124415
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
@@ -123366,10 +124613,10 @@ function registerAgentConfigWriteCommands(program3) {
123366
124613
  }
123367
124614
  let blob;
123368
124615
  if (opts.jsonl) {
123369
- blob = existsSync108(opts.jsonl) ? readFileSync98(opts.jsonl, "utf-8") : "";
124616
+ blob = existsSync109(opts.jsonl) ? readFileSync98(opts.jsonl, "utf-8") : "";
123370
124617
  } else {
123371
124618
  try {
123372
- blob = execFileSync33("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
124619
+ blob = execFileSync34("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
123373
124620
  encoding: "utf-8",
123374
124621
  stdio: ["ignore", "pipe", "ignore"]
123375
124622
  });
@@ -123398,7 +124645,7 @@ function registerAgentConfigWriteCommands(program3) {
123398
124645
 
123399
124646
  // src/cli/agent-config-skill-write.ts
123400
124647
  var import_yaml24 = __toESM(require_dist(), 1);
123401
- import { existsSync as existsSync109 } from "node:fs";
124648
+ import { existsSync as existsSync110 } from "node:fs";
123402
124649
  init_reconcile_default_skills();
123403
124650
  init_agent_config();
123404
124651
  var import_yaml25 = __toESM(require_dist(), 1);
@@ -123478,7 +124725,7 @@ function skillInstall(opts) {
123478
124725
  }
123479
124726
  const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
123480
124727
  const skillPath = join111(poolDir, skillName);
123481
- if (!existsSync109(skillPath)) {
124728
+ if (!existsSync110(skillPath)) {
123482
124729
  return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
123483
124730
  }
123484
124731
  const yamlText = import_yaml24.stringify({ skills: [skillName] });
@@ -123648,7 +124895,7 @@ init_source();
123648
124895
  import {
123649
124896
  chmodSync as chmodSync18,
123650
124897
  closeSync as closeSync20,
123651
- existsSync as existsSync110,
124898
+ existsSync as existsSync111,
123652
124899
  lstatSync as lstatSync14,
123653
124900
  mkdirSync as mkdirSync64,
123654
124901
  mkdtempSync as mkdtempSync5,
@@ -123857,7 +125104,7 @@ function validatePayload(name, files) {
123857
125104
  function diffSummary(currentDir, files) {
123858
125105
  const lines = [];
123859
125106
  const currentFiles = {};
123860
- if (existsSync110(currentDir)) {
125107
+ if (existsSync111(currentDir)) {
123861
125108
  const walk2 = (sub) => {
123862
125109
  for (const ent of readdirSync40(sub, { withFileTypes: true })) {
123863
125110
  const full = join112(sub, ent.name);
@@ -123893,7 +125140,7 @@ function diffSummary(currentDir, files) {
123893
125140
  `);
123894
125141
  }
123895
125142
  function writePayload(poolDir, name, files) {
123896
- if (!existsSync110(poolDir)) {
125143
+ if (!existsSync111(poolDir)) {
123897
125144
  mkdirSync64(poolDir, { recursive: true, mode: 493 });
123898
125145
  }
123899
125146
  const target = join112(poolDir, name);
@@ -123943,9 +125190,9 @@ function writePayload(poolDir, name, files) {
123943
125190
  try {
123944
125191
  rmSync20(staging, { recursive: true, force: true });
123945
125192
  } catch {}
123946
- if (oldRename && existsSync110(oldRename)) {
125193
+ if (oldRename && existsSync111(oldRename)) {
123947
125194
  try {
123948
- if (existsSync110(target)) {
125195
+ if (existsSync111(target)) {
123949
125196
  rmSync20(target, { recursive: true, force: true });
123950
125197
  }
123951
125198
  renameSync30(oldRename, target);
@@ -123966,7 +125213,7 @@ function registerSkillCommand(program3) {
123966
125213
  files = loadFromStdin();
123967
125214
  } else {
123968
125215
  const fromPath = resolve60(opts.from);
123969
- if (!existsSync110(fromPath)) {
125216
+ if (!existsSync111(fromPath)) {
123970
125217
  fail4(`--from path does not exist: ${opts.from}`);
123971
125218
  }
123972
125219
  const st = statSync58(fromPath);
@@ -124027,7 +125274,7 @@ init_source();
124027
125274
  import {
124028
125275
  chmodSync as chmodSync19,
124029
125276
  closeSync as closeSync21,
124030
- existsSync as existsSync111,
125277
+ existsSync as existsSync112,
124031
125278
  lstatSync as lstatSync15,
124032
125279
  mkdirSync as mkdirSync65,
124033
125280
  mkdtempSync as mkdtempSync6,
@@ -124050,14 +125297,14 @@ var PERSONAL_SKILLS_SUBPATH2 = "personal-skills";
124050
125297
  function resolveConfigSkillsDir(agent) {
124051
125298
  const override = process.env.SWITCHROOM_CONFIG_DIR;
124052
125299
  const candidate = override ? resolve61(override) : join113(homedir59(), ".switchroom-config");
124053
- if (!existsSync111(candidate))
125300
+ if (!existsSync112(candidate))
124054
125301
  return null;
124055
125302
  return join113(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH2);
124056
125303
  }
124057
125304
  var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
124058
125305
  function sweepMirrorPriors(configSkillsRoot) {
124059
125306
  try {
124060
- if (!existsSync111(configSkillsRoot))
125307
+ if (!existsSync112(configSkillsRoot))
124061
125308
  return;
124062
125309
  const now2 = Date.now();
124063
125310
  for (const ent of readdirSync41(configSkillsRoot)) {
@@ -124093,7 +125340,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
124093
125340
  }
124094
125341
  if (liveSkillDir === null) {
124095
125342
  sweepMirrorPriors(configSkillsRoot);
124096
- if (existsSync111(dest)) {
125343
+ if (existsSync112(dest)) {
124097
125344
  const trash = join113(configSkillsRoot, `.${name}-trash-${Date.now()}`);
124098
125345
  renameSync31(dest, trash);
124099
125346
  }
@@ -124118,7 +125365,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
124118
125365
  };
124119
125366
  walk2(liveSkillDir, staging);
124120
125367
  chmodSync19(staging, 493);
124121
- if (existsSync111(dest)) {
125368
+ if (existsSync112(dest)) {
124122
125369
  const prior = join113(configSkillsRoot, `.${name}-prior-${Date.now()}`);
124123
125370
  renameSync31(dest, prior);
124124
125371
  }
@@ -124159,7 +125406,7 @@ function trashDir(agentsRoot, agent) {
124159
125406
  }
124160
125407
  function countPersonalSkills(agentsRoot, agent) {
124161
125408
  const skillsDir = join113(agentsRoot, agent, ".claude", "skills");
124162
- if (!existsSync111(skillsDir))
125409
+ if (!existsSync112(skillsDir))
124163
125410
  return 0;
124164
125411
  let n = 0;
124165
125412
  for (const ent of readdirSync41(skillsDir, { withFileTypes: true })) {
@@ -124268,7 +125515,7 @@ function behavioralValidate(files) {
124268
125515
  }
124269
125516
  function sweepTrash(agentsRoot, agent) {
124270
125517
  const trash = trashDir(agentsRoot, agent);
124271
- if (!existsSync111(trash))
125518
+ if (!existsSync112(trash))
124272
125519
  return;
124273
125520
  const now2 = Date.now();
124274
125521
  for (const ent of readdirSync41(trash, { withFileTypes: true })) {
@@ -124331,9 +125578,9 @@ function writePersonalSkill(targetDir, files) {
124331
125578
  try {
124332
125579
  rmSync21(staging, { recursive: true, force: true });
124333
125580
  } catch {}
124334
- if (oldRename && existsSync111(oldRename)) {
125581
+ if (oldRename && existsSync112(oldRename)) {
124335
125582
  try {
124336
- if (existsSync111(targetDir)) {
125583
+ if (existsSync112(targetDir)) {
124337
125584
  rmSync21(targetDir, { recursive: true, force: true });
124338
125585
  }
124339
125586
  renameSync31(oldRename, targetDir);
@@ -124399,7 +125646,7 @@ function loadFiles(opts) {
124399
125646
  return loadFromStdin2();
124400
125647
  }
124401
125648
  const p = resolve61(opts.from);
124402
- if (!existsSync111(p)) {
125649
+ if (!existsSync112(p)) {
124403
125650
  fail5(`--from path does not exist: ${opts.from}`);
124404
125651
  }
124405
125652
  const st = statSync59(p);
@@ -124461,7 +125708,7 @@ function resolveCloneSource(source, opts) {
124461
125708
  const slug = m[2];
124462
125709
  const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
124463
125710
  const dir = join113(root, slug);
124464
- if (!existsSync111(dir)) {
125711
+ if (!existsSync112(dir)) {
124465
125712
  fail5(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
124466
125713
  }
124467
125714
  const st = lstatSync15(dir);
@@ -124608,7 +125855,7 @@ function listPersonalAction(opts) {
124608
125855
  sweepTrash(agentsRoot, agent);
124609
125856
  const skillsDir = join113(agentsRoot, agent, ".claude", "skills");
124610
125857
  const personal = [];
124611
- if (existsSync111(skillsDir)) {
125858
+ if (existsSync112(skillsDir)) {
124612
125859
  for (const ent of readdirSync41(skillsDir, { withFileTypes: true })) {
124613
125860
  if (!ent.isDirectory())
124614
125861
  continue;
@@ -124794,10 +126041,10 @@ function registerSelfImproveProposeSkillCommand(program3) {
124794
126041
  import { createConnection as createConnection5 } from "node:net";
124795
126042
  import { homedir as homedir61 } from "node:os";
124796
126043
  import { join as join118, resolve as resolve62, sep as sep6 } from "node:path";
124797
- import { existsSync as existsSync116, readFileSync as readFileSync106, realpathSync as realpathSync10 } from "node:fs";
126044
+ import { existsSync as existsSync117, readFileSync as readFileSync106, realpathSync as realpathSync10 } from "node:fs";
124798
126045
 
124799
126046
  // src/self-improve/apply-guard.ts
124800
- import { existsSync as existsSync114, lstatSync as lstatSync16, readFileSync as readFileSync104, statSync as statSync61 } from "node:fs";
126047
+ import { existsSync as existsSync115, lstatSync as lstatSync16, readFileSync as readFileSync104, statSync as statSync61 } from "node:fs";
124801
126048
 
124802
126049
  // src/self-improve/config.ts
124803
126050
  function intEnv(name, def) {
@@ -124818,7 +126065,7 @@ function resolveSelfImproveConfig() {
124818
126065
  }
124819
126066
 
124820
126067
  // src/self-improve/eval-gate.ts
124821
- import { existsSync as existsSync112, readFileSync as readFileSync102, readdirSync as readdirSync42, statSync as statSync60 } from "node:fs";
126068
+ import { existsSync as existsSync113, readFileSync as readFileSync102, readdirSync as readdirSync42, statSync as statSync60 } from "node:fs";
124822
126069
  import { join as join115 } from "node:path";
124823
126070
  import { spawnSync as spawnSync26 } from "node:child_process";
124824
126071
  function evalsJsonPath(skillDir) {
@@ -124826,7 +126073,7 @@ function evalsJsonPath(skillDir) {
124826
126073
  }
124827
126074
  function skillHasEvals(skillDir) {
124828
126075
  const p = evalsJsonPath(skillDir);
124829
- if (!existsSync112(p))
126076
+ if (!existsSync113(p))
124830
126077
  return false;
124831
126078
  try {
124832
126079
  const parsed = JSON.parse(readFileSync102(p, "utf-8"));
@@ -124837,7 +126084,7 @@ function skillHasEvals(skillDir) {
124837
126084
  }
124838
126085
  function aggregateBenchmark(benchmarkDir, repoRoot, pythonBin = process.env.SWITCHROOM_PYTHON ?? "python3") {
124839
126086
  const script = join115(repoRoot, "skills", "skill-creator", "scripts", "aggregate_benchmark.py");
124840
- if (!existsSync112(script)) {
126087
+ if (!existsSync113(script)) {
124841
126088
  return { ok: false, error: `aggregate_benchmark.py not found at ${script}` };
124842
126089
  }
124843
126090
  const r = spawnSync26(pythonBin, [script, benchmarkDir], {
@@ -124869,7 +126116,7 @@ function isBenchmarkDir(dir) {
124869
126116
  if (hasEvalDirs(dir))
124870
126117
  return true;
124871
126118
  const runs = join115(dir, "runs");
124872
- return existsSync112(runs) && hasEvalDirs(runs);
126119
+ return existsSync113(runs) && hasEvalDirs(runs);
124873
126120
  }
124874
126121
  function resolveBenchmarkDir(baseDir, explicit) {
124875
126122
  if (explicit != null && explicit.length > 0) {
@@ -124948,7 +126195,7 @@ function floatEnv(name, def) {
124948
126195
  import {
124949
126196
  closeSync as closeSync22,
124950
126197
  copyFileSync as copyFileSync15,
124951
- existsSync as existsSync113,
126198
+ existsSync as existsSync114,
124952
126199
  mkdirSync as mkdirSync66,
124953
126200
  openSync as openSync22,
124954
126201
  readFileSync as readFileSync103,
@@ -125095,7 +126342,7 @@ function caseFingerprint(prompt) {
125095
126342
  }
125096
126343
  function readEvalsDoc(skillDir) {
125097
126344
  const p = evalsJsonPath(skillDir);
125098
- if (!existsSync113(p))
126345
+ if (!existsSync114(p))
125099
126346
  return null;
125100
126347
  try {
125101
126348
  const parsed = JSON.parse(readFileSync103(p, "utf-8"));
@@ -125119,7 +126366,7 @@ function appendEvalCase(skillDir, slug, input) {
125119
126366
  return { ok: false, reason: parsed.error };
125120
126367
  const ec = parsed.case;
125121
126368
  const existingDoc = readEvalsDoc(skillDir);
125122
- if (existingDoc === null && existsSync113(evalsJsonPath(skillDir))) {
126369
+ if (existingDoc === null && existsSync114(evalsJsonPath(skillDir))) {
125123
126370
  return {
125124
126371
  ok: false,
125125
126372
  reason: "evals.json exists but is unparseable or malformed \u2014 refusing to " + "overwrite it (fix or remove the file first)"
@@ -125151,7 +126398,7 @@ function heldOutPath(stateDir) {
125151
126398
  }
125152
126399
  function heldOutHasFingerprint(stateDir, slug, fp) {
125153
126400
  const path10 = heldOutPath(stateDir);
125154
- if (!existsSync113(path10))
126401
+ if (!existsSync114(path10))
125155
126402
  return false;
125156
126403
  let text;
125157
126404
  try {
@@ -125203,7 +126450,7 @@ function baselineEvalsPath(stateDir, slug) {
125203
126450
  }
125204
126451
  function evalsSha256(skillDir) {
125205
126452
  const p = evalsJsonPath(skillDir);
125206
- if (!existsSync113(p))
126453
+ if (!existsSync114(p))
125207
126454
  return null;
125208
126455
  try {
125209
126456
  return createHash21("sha256").update(readFileSync103(p)).digest("hex");
@@ -125213,7 +126460,7 @@ function evalsSha256(skillDir) {
125213
126460
  }
125214
126461
  function readManifest(stateDir) {
125215
126462
  const p = manifestPath(stateDir);
125216
- if (!existsSync113(p))
126463
+ if (!existsSync114(p))
125217
126464
  return {};
125218
126465
  try {
125219
126466
  const parsed = JSON.parse(readFileSync103(p, "utf-8"));
@@ -125255,7 +126502,7 @@ function ownsSkill(skillDir) {
125255
126502
  // src/self-improve/eval-case-proposals.ts
125256
126503
  import {
125257
126504
  closeSync as closeSync23,
125258
- existsSync as existsSync115,
126505
+ existsSync as existsSync116,
125259
126506
  mkdirSync as mkdirSync67,
125260
126507
  openSync as openSync23,
125261
126508
  readFileSync as readFileSync105,
@@ -125271,7 +126518,7 @@ function isRecord2(v) {
125271
126518
  }
125272
126519
  function readEvalCaseProposals(stateDir) {
125273
126520
  const p = proposalsPath(stateDir);
125274
- if (!existsSync115(p))
126521
+ if (!existsSync116(p))
125275
126522
  return [];
125276
126523
  let raw;
125277
126524
  try {
@@ -125468,7 +126715,7 @@ function registerApply(parent) {
125468
126715
  }));
125469
126716
  process.exit(0);
125470
126717
  }
125471
- if (!existsSync116(proposal.skill_dir)) {
126718
+ if (!existsSync117(proposal.skill_dir)) {
125472
126719
  fail7(`skill dir gone: ${proposal.skill_dir}`);
125473
126720
  }
125474
126721
  const { dir, resolvedDir } = authorizeApplyTarget(proposal.skill_dir, proposal.skill_slug);
@@ -125581,7 +126828,7 @@ function registerSelfImproveBenchCommand(program3) {
125581
126828
  init_esm();
125582
126829
  init_helpers();
125583
126830
  var import_yaml26 = __toESM(require_dist(), 1);
125584
- import { existsSync as existsSync117, readdirSync as readdirSync44, readFileSync as readFileSync107, statSync as statSync62 } from "node:fs";
126831
+ import { existsSync as existsSync118, readdirSync as readdirSync44, readFileSync as readFileSync107, statSync as statSync62 } from "node:fs";
125585
126832
  import { homedir as homedir63 } from "node:os";
125586
126833
  import { join as join120, resolve as resolve63 } from "node:path";
125587
126834
  var PERSONAL_PREFIX2 = "personal-";
@@ -125598,7 +126845,7 @@ function defaultBundledRoot2() {
125598
126845
  }
125599
126846
  function readSkillFrontmatter(skillDir) {
125600
126847
  const mdPath = join120(skillDir, "SKILL.md");
125601
- if (!existsSync117(mdPath))
126848
+ if (!existsSync118(mdPath))
125602
126849
  return null;
125603
126850
  let content;
125604
126851
  try {
@@ -125642,7 +126889,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
125642
126889
  if (!AGENT_NAME_RE3.test(agent))
125643
126890
  return [];
125644
126891
  const skillsDir = join120(agentsRoot, agent, ".claude/skills");
125645
- if (!existsSync117(skillsDir))
126892
+ if (!existsSync118(skillsDir))
125646
126893
  return [];
125647
126894
  const out = [];
125648
126895
  let entries;
@@ -125680,7 +126927,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
125680
126927
  return out;
125681
126928
  }
125682
126929
  function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
125683
- if (!existsSync117(sharedRoot))
126930
+ if (!existsSync118(sharedRoot))
125684
126931
  return [];
125685
126932
  const out = [];
125686
126933
  let entries;
@@ -125718,7 +126965,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
125718
126965
  return out;
125719
126966
  }
125720
126967
  function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
125721
- if (!existsSync117(bundledRoot))
126968
+ if (!existsSync118(bundledRoot))
125722
126969
  return [];
125723
126970
  const out = [];
125724
126971
  let entries;
@@ -125877,7 +127124,7 @@ init_helpers();
125877
127124
  init_docker_socket();
125878
127125
  init_operator_uid();
125879
127126
  import {
125880
- existsSync as existsSync119,
127127
+ existsSync as existsSync120,
125881
127128
  mkdirSync as mkdirSync68,
125882
127129
  readdirSync as readdirSync45,
125883
127130
  writeFileSync as writeFileSync49,
@@ -126260,7 +127507,7 @@ function resolveHostdSkillsTarget(hostHome) {
126260
127507
  console.warn(`switchroom hostd install: ~/.switchroom/skills is a symlink whose target ` + `does not resolve (dangling) \u2014 skipping the skills bind mount. Bundled ` + `skills will be unavailable to rollout/update until the symlink is fixed.`);
126261
127508
  return;
126262
127509
  }
126263
- if (!existsSync119(target)) {
127510
+ if (!existsSync120(target)) {
126264
127511
  console.warn(`switchroom hostd install: ~/.switchroom/skills resolves to "${target}", ` + `which does not exist \u2014 skipping the skills bind mount. Bundled skills ` + `will be unavailable to rollout/update until the symlink target exists.`);
126265
127512
  return;
126266
127513
  }
@@ -126274,7 +127521,7 @@ function hostdComposePath() {
126274
127521
  }
126275
127522
  function backupExistingCompose() {
126276
127523
  const p = hostdComposePath();
126277
- if (!existsSync119(p))
127524
+ if (!existsSync120(p))
126278
127525
  return null;
126279
127526
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
126280
127527
  const bak = `${p}.bak-${ts}`;
@@ -126373,7 +127620,7 @@ function doStatus() {
126373
127620
  const composeYml = hostdComposePath();
126374
127621
  console.log(source_default.bold("switchroom-hostd"));
126375
127622
  console.log("");
126376
- if (!existsSync119(composeYml)) {
127623
+ if (!existsSync120(composeYml)) {
126377
127624
  console.log(source_default.yellow(" compose: not installed"));
126378
127625
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
126379
127626
  return;
@@ -126394,14 +127641,14 @@ function doStatus() {
126394
127641
  } else {
126395
127642
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
126396
127643
  }
126397
- if (existsSync119(dir)) {
127644
+ if (existsSync120(dir)) {
126398
127645
  const entries = [];
126399
127646
  try {
126400
127647
  for (const name of readdirSync45(dir)) {
126401
127648
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
126402
127649
  continue;
126403
127650
  const sockPath = join121(dir, name, "sock");
126404
- if (existsSync119(sockPath)) {
127651
+ if (existsSync120(sockPath)) {
126405
127652
  const st = statSync63(sockPath);
126406
127653
  if ((st.mode & 61440) === 49152) {
126407
127654
  entries.push(`${name} \u2192 ${sockPath}`);
@@ -126420,7 +127667,7 @@ function doStatus() {
126420
127667
  }
126421
127668
  function doUninstall() {
126422
127669
  const composeYml = hostdComposePath();
126423
- if (!existsSync119(composeYml)) {
127670
+ if (!existsSync120(composeYml)) {
126424
127671
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
126425
127672
  return;
126426
127673
  }
@@ -126444,7 +127691,7 @@ function registerHostdCommand(program3) {
126444
127691
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
126445
127692
  hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
126446
127693
  const logPath = opts.path ?? defaultAuditLogPath2();
126447
- if (!existsSync119(logPath)) {
127694
+ if (!existsSync120(logPath)) {
126448
127695
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
126449
127696
  The log is created when hostd handles its first privileged-verb request.`));
126450
127697
  return;
@@ -126493,7 +127740,7 @@ init_source();
126493
127740
  init_helpers();
126494
127741
  init_operator_uid();
126495
127742
  init_compose();
126496
- import { chownSync as chownSync13, existsSync as existsSync120, mkdirSync as mkdirSync69, writeFileSync as writeFileSync50, copyFileSync as copyFileSync17 } from "node:fs";
127743
+ import { chownSync as chownSync13, existsSync as existsSync121, mkdirSync as mkdirSync69, writeFileSync as writeFileSync50, copyFileSync as copyFileSync17 } from "node:fs";
126497
127744
  import { homedir as homedir65 } from "node:os";
126498
127745
  import { join as join122 } from "node:path";
126499
127746
  import { spawnSync as spawnSync30 } from "node:child_process";
@@ -126604,7 +127851,7 @@ function webdComposePath() {
126604
127851
  }
126605
127852
  function backupExistingCompose2() {
126606
127853
  const p = webdComposePath();
126607
- if (!existsSync120(p))
127854
+ if (!existsSync121(p))
126608
127855
  return null;
126609
127856
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
126610
127857
  const bak = `${p}.bak-${ts}`;
@@ -126712,7 +127959,7 @@ function doStatus2() {
126712
127959
  const composeYml = webdComposePath();
126713
127960
  console.log(source_default.bold("switchroom-web"));
126714
127961
  console.log("");
126715
- if (!existsSync120(composeYml)) {
127962
+ if (!existsSync121(composeYml)) {
126716
127963
  console.log(source_default.yellow(" compose: not installed"));
126717
127964
  console.log(source_default.dim(" run `switchroom webd install` to set up."));
126718
127965
  return;
@@ -126736,7 +127983,7 @@ function doStatus2() {
126736
127983
  }
126737
127984
  function doUninstall2() {
126738
127985
  const composeYml = webdComposePath();
126739
- if (!existsSync120(composeYml)) {
127986
+ if (!existsSync121(composeYml)) {
126740
127987
  console.log(source_default.yellow(" No web-service install detected (no compose file at this path)."));
126741
127988
  return;
126742
127989
  }
@@ -127222,9 +128469,9 @@ function printDeepDiveBrief(targets) {
127222
128469
 
127223
128470
  // src/cli/hindsight-watch.ts
127224
128471
  init_source();
127225
- import { existsSync as existsSync125, readdirSync as readdirSync50 } from "node:fs";
128472
+ import { existsSync as existsSync126, readdirSync as readdirSync50 } from "node:fs";
127226
128473
  import { homedir as homedir69 } from "node:os";
127227
- import { join as join125, resolve as resolve66 } from "node:path";
128474
+ import { join as join126, resolve as resolve66 } from "node:path";
127228
128475
 
127229
128476
  // src/host-control/config-degraded.ts
127230
128477
  import { connect as connect3 } from "node:net";
@@ -127421,16 +128668,16 @@ function readLlmSignals(series) {
127421
128668
  }
127422
128669
 
127423
128670
  // src/hindsight-watch/recall-log.ts
127424
- import { closeSync as closeSync24, existsSync as existsSync124, openSync as openSync24, readdirSync as readdirSync48, readSync as readSync6, statSync as statSync64 } from "node:fs";
127425
- import { join as join124 } from "node:path";
128671
+ import { closeSync as closeSync24, existsSync as existsSync125, openSync as openSync24, readdirSync as readdirSync48, readSync as readSync6, statSync as statSync64 } from "node:fs";
128672
+ import { join as join125 } from "node:path";
127426
128673
  var RECALL_WINDOW_ROWS = 200;
127427
128674
  var RECALL_MAX_TAIL_BYTES = 1024 * 1024;
127428
128675
  var RECALL_MAX_ROW_AGE_MS = 24 * 60 * 60 * 1000;
127429
128676
  function recallLogPath2(agentDir) {
127430
- return join124(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
128677
+ return join125(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
127431
128678
  }
127432
128679
  function readRecallLogTail2(path10, windowRows = RECALL_WINDOW_ROWS) {
127433
- if (!existsSync124(path10))
128680
+ if (!existsSync125(path10))
127434
128681
  return [];
127435
128682
  let text;
127436
128683
  let truncatedHead = false;
@@ -127575,7 +128822,7 @@ function probeRecallLogs(agentsDir, now2, windowRows = RECALL_WINDOW_ROWS, maxAg
127575
128822
  const all = [];
127576
128823
  let agents = 0;
127577
128824
  for (const name of names) {
127578
- const rows = withinWindow(readRecallLogTail2(recallLogPath2(join124(agentsDir, name)), windowRows), now2, maxAgeMs);
128825
+ const rows = withinWindow(readRecallLogTail2(recallLogPath2(join125(agentsDir, name)), windowRows), now2, maxAgeMs);
127579
128826
  if (rows.length > 0)
127580
128827
  agents++;
127581
128828
  all.push(...rows);
@@ -128633,7 +129880,7 @@ function registerHindsightWatchCommand(program3) {
128633
129880
  process.exitCode = runInstallCron(opts.cronUser);
128634
129881
  return;
128635
129882
  }
128636
- const agentsDir = opts.agentsDir ?? process.env.SWITCHROOM_AGENTS_DIR ?? join125(process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir69(), ".switchroom", "agents");
129883
+ const agentsDir = opts.agentsDir ?? process.env.SWITCHROOM_AGENTS_DIR ?? join126(process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir69(), ".switchroom", "agents");
128637
129884
  const statePath = opts.state ?? defaultStatePath2();
128638
129885
  const log = (m) => {
128639
129886
  process.stderr.write(`${m}
@@ -128749,7 +129996,7 @@ function collectRelayCandidates(agentsDir) {
128749
129996
  const candidates = [];
128750
129997
  for (const name of readdirSync50(agentsDir).sort()) {
128751
129998
  const sock = resolve66(agentsDir, name, "telegram", "gateway.sock");
128752
- if (existsSync125(sock))
129999
+ if (existsSync126(sock))
128753
130000
  candidates.push({ agent: name, sock });
128754
130001
  }
128755
130002
  return orderRelayCandidates(candidates);
@@ -128772,7 +130019,7 @@ async function notifyOperator(agentsDir, text, log) {
128772
130019
  // src/cli/hindsight-bench.ts
128773
130020
  init_source();
128774
130021
  import { mkdirSync as mkdirSync71, readFileSync as readFileSync113, writeFileSync as writeFileSync52 } from "node:fs";
128775
- import { dirname as dirname51, resolve as resolve67 } from "node:path";
130022
+ import { dirname as dirname52, resolve as resolve67 } from "node:path";
128776
130023
 
128777
130024
  // src/hindsight-bench/anonymise.ts
128778
130025
  function pseudonym(n, width) {
@@ -129900,7 +131147,7 @@ function readResult(path10) {
129900
131147
  }
129901
131148
  function writeOut(path10, body) {
129902
131149
  const abs = resolve67(path10);
129903
- mkdirSync71(dirname51(abs), { recursive: true });
131150
+ mkdirSync71(dirname52(abs), { recursive: true });
129904
131151
  writeFileSync52(abs, body);
129905
131152
  process.stderr.write(`${source_default.green("\u2713")} wrote ${abs}
129906
131153
  `);
@@ -130105,13 +131352,13 @@ async function runMeasureMode(opts) {
130105
131352
 
130106
131353
  // src/cli/openrouter-watch.ts
130107
131354
  init_source();
130108
- import { existsSync as existsSync127, readdirSync as readdirSync51 } from "node:fs";
131355
+ import { existsSync as existsSync128, readdirSync as readdirSync51 } from "node:fs";
130109
131356
  import { homedir as homedir71 } from "node:os";
130110
- import { join as join126, resolve as resolve69 } from "node:path";
131357
+ import { join as join127, resolve as resolve69 } from "node:path";
130111
131358
 
130112
131359
  // src/openrouter/install-cron.ts
130113
- import { existsSync as existsSync126, mkdirSync as mkdirSync72, readFileSync as readFileSync114, renameSync as renameSync33, writeFileSync as writeFileSync53 } from "node:fs";
130114
- import { dirname as dirname52 } from "node:path";
131360
+ import { existsSync as existsSync127, mkdirSync as mkdirSync72, readFileSync as readFileSync114, renameSync as renameSync33, writeFileSync as writeFileSync53 } from "node:fs";
131361
+ import { dirname as dirname53 } from "node:path";
130115
131362
  var CRON_PATH3 = "/etc/cron.d/openrouter-watch";
130116
131363
  var CRON_SCHEDULE2 = "17 * * * *";
130117
131364
  var CRON_LOG_PATH3 = "/var/log/openrouter-watch.log";
@@ -130128,14 +131375,14 @@ function renderCron3(opts) {
130128
131375
  function installCron3(opts) {
130129
131376
  const path10 = opts.path ?? CRON_PATH3;
130130
131377
  const content = renderCron3(opts);
130131
- if (existsSync126(path10)) {
131378
+ if (existsSync127(path10)) {
130132
131379
  try {
130133
131380
  if (readFileSync114(path10, "utf8") === content) {
130134
131381
  return { status: "unchanged", path: path10, content };
130135
131382
  }
130136
131383
  } catch {}
130137
131384
  }
130138
- mkdirSync72(dirname52(path10), { recursive: true });
131385
+ mkdirSync72(dirname53(path10), { recursive: true });
130139
131386
  const tmp = `${path10}.${process.pid}.tmp`;
130140
131387
  writeFileSync53(tmp, content, { mode: 420 });
130141
131388
  renameSync33(tmp, path10);
@@ -130144,7 +131391,7 @@ function installCron3(opts) {
130144
131391
  // src/openrouter/state.ts
130145
131392
  import { mkdirSync as mkdirSync73, readFileSync as readFileSync115, renameSync as renameSync34, writeFileSync as writeFileSync54 } from "node:fs";
130146
131393
  import { homedir as homedir70 } from "node:os";
130147
- import { dirname as dirname53, resolve as resolve68 } from "node:path";
131394
+ import { dirname as dirname54, resolve as resolve68 } from "node:path";
130148
131395
  function defaultStatePath3(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir70()) {
130149
131396
  return resolve68(home2, ".switchroom", "openrouter-watch", "state.json");
130150
131397
  }
@@ -130171,7 +131418,7 @@ function loadState2(path10) {
130171
131418
  return out;
130172
131419
  }
130173
131420
  function saveState2(path10, state) {
130174
- mkdirSync73(dirname53(path10), { recursive: true, mode: 448 });
131421
+ mkdirSync73(dirname54(path10), { recursive: true, mode: 448 });
130175
131422
  const tmp = `${path10}.${process.pid}.tmp`;
130176
131423
  const payload = { v: 1, ...state };
130177
131424
  writeFileSync54(tmp, JSON.stringify(payload, null, 2) + `
@@ -130262,7 +131509,7 @@ function registerOpenRouterWatchCommand(program3) {
130262
131509
  process.exitCode = runInstallCron2(opts.cronUser);
130263
131510
  return;
130264
131511
  }
130265
- const agentsDir = opts.agentsDir ?? process.env.SWITCHROOM_AGENTS_DIR ?? join126(process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir71(), ".switchroom", "agents");
131512
+ const agentsDir = opts.agentsDir ?? process.env.SWITCHROOM_AGENTS_DIR ?? join127(process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir71(), ".switchroom", "agents");
130266
131513
  const statePath = opts.state ?? defaultStatePath3();
130267
131514
  const log = (m) => {
130268
131515
  process.stderr.write(`${m}
@@ -130361,7 +131608,7 @@ async function notifyOperator2(agentsDir, text, log) {
130361
131608
  try {
130362
131609
  for (const name of readdirSync51(agentsDir).sort()) {
130363
131610
  const sock = resolve69(agentsDir, name, "telegram", "gateway.sock");
130364
- if (existsSync127(sock))
131611
+ if (existsSync128(sock))
130365
131612
  candidates.push({ agent: name, sock });
130366
131613
  }
130367
131614
  } catch (e) {
@@ -130382,7 +131629,7 @@ init_sync();
130382
131629
  init_install_cron2();
130383
131630
  init_flock();
130384
131631
  import { homedir as homedir72 } from "node:os";
130385
- import { join as join127 } from "node:path";
131632
+ import { join as join128 } from "node:path";
130386
131633
  function switchroomHome() {
130387
131634
  return process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir72();
130388
131635
  }
@@ -130394,9 +131641,9 @@ function resolveSyncOptions(configPath, opts) {
130394
131641
  cfg = loaded.config_repo;
130395
131642
  } catch {}
130396
131643
  const home2 = switchroomHome();
130397
- const repoPath = opts.path ? resolvePath(opts.path) : cfg.path ? resolvePath(cfg.path) : join127(home2, ".switchroom-config");
130398
- const livePath = process.env.SWITCHROOM_CONFIG ?? join127(home2, ".switchroom", "switchroom.yaml");
130399
- const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join127(home2, ".switchroom", "agents");
131644
+ const repoPath = opts.path ? resolvePath(opts.path) : cfg.path ? resolvePath(cfg.path) : join128(home2, ".switchroom-config");
131645
+ const livePath = process.env.SWITCHROOM_CONFIG ?? join128(home2, ".switchroom", "switchroom.yaml");
131646
+ const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join128(home2, ".switchroom", "agents");
130400
131647
  const push = opts.push === false ? false : cfg.push ?? true;
130401
131648
  const remote = opts.remote ?? cfg.remote ?? "origin";
130402
131649
  const requirePrivate = opts.requirePrivate ?? cfg.require_private ?? true;