switchroom 0.19.41 → 0.19.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/agent-scheduler/index.js +22 -2
  2. package/dist/auth-broker/index.js +22 -2
  3. package/dist/cli/notion-write-pretool.mjs +22 -2
  4. package/dist/cli/switchroom.js +772 -234
  5. package/dist/host-control/main.js +23 -3
  6. package/dist/vault/approvals/kernel-server.js +22 -2
  7. package/dist/vault/broker/server.js +22 -2
  8. package/package.json +1 -1
  9. package/telegram-plugin/dist/gateway/gateway.js +97 -83
  10. package/telegram-plugin/gateway/approval-callback-consume-record.test.ts +132 -0
  11. package/telegram-plugin/gateway/gateway.ts +7 -7
  12. package/telegram-plugin/gateway/liveness-wiring.ts +12 -0
  13. package/telegram-plugin/gateway/model-command.ts +21 -109
  14. package/telegram-plugin/gateway/stream-render.ts +57 -0
  15. package/telegram-plugin/tests/framework-fallback-drains-parked.test.ts +134 -0
  16. package/telegram-plugin/tests/helpers/liveness-wiring-fixture.ts +3 -0
  17. package/vendor/hindsight-memory/CHANGELOG.md +28 -0
  18. package/vendor/hindsight-memory/docs/measurements/subagent-volume-gate-3994.md +84 -0
  19. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +254 -40
  20. package/vendor/hindsight-memory/scripts/lib/content.py +11 -2
  21. package/vendor/hindsight-memory/scripts/recall.py +18 -3
  22. package/vendor/hindsight-memory/scripts/subagent_retain.py +59 -38
  23. package/vendor/hindsight-memory/scripts/tests/data/replay_volume_gate_3994.py +295 -0
  24. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +109 -0
  25. package/vendor/hindsight-memory/scripts/tests/test_overlap_tokens.py +135 -0
  26. package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +20 -23
  27. package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +48 -0
  28. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +94 -1
  29. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain_learnings.py +98 -42
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.41", COMMIT_SHA = "e77ee9c0";
2123
+ var VERSION = "0.19.42", COMMIT_SHA = "e288dac2";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -13613,6 +13613,9 @@ var init_observation_scopes = __esm(() => {
13613
13613
  function isValidTimezone(value) {
13614
13614
  return TIMEZONE_REGEX.test(value);
13615
13615
  }
13616
+ function resolveMemoryProfile(agentConfig) {
13617
+ return agentConfig?.memory?.profile ?? agentConfig?.extends ?? DEFAULT_PROFILE;
13618
+ }
13616
13619
  var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13617
13620
  var init_schema = __esm(() => {
13618
13621
  init_zod();
@@ -13735,6 +13738,7 @@ var init_schema = __esm(() => {
13735
13738
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13736
13739
  file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13737
13740
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13741
+ profile: exports_external.string().optional().describe("Memory profile bank this agent's curated memory defaults key off \u2014 " + "the built-in disposition + observations_mission in PROFILE_MEMORY_DEFAULTS. " + "Decouples the memory profile from `extends` (the filesystem persona " + "profile), so an agent on `extends: default` can opt into the `coding` " + "memory bundle via `memory.profile: coding` without inheriting the coding " + "persona. Resolution: memory.profile \u2192 extends \u2192 DEFAULT_PROFILE (see " + "resolveMemoryProfile). Unset \u21d2 byte-identical to keying off `extends`."),
13738
13742
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize " + "results. NOTE: this is an alias for the Hindsight engine's " + "`reflect_mission` field (verified live: switchroom's bank_mission " + "lands in `config.reflect_mission`). Prefer `reflect_mission` going " + "forward; `bank_mission` is retained for back-compat. If both are " + "set, `reflect_mission` wins. Cascade: override."),
13739
13743
  reflect_mission: exports_external.string().optional().describe("Mission/context steering Hindsight Reflect operations (the bank's " + "'who am I / what matters' framing applied during recall). The " + "engine-accurate name for what `bank_mission` sets. Cascade: override."),
13740
13744
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain. Cascade: override."),
@@ -14028,6 +14032,8 @@ var init_schema = __esm(() => {
14028
14032
  provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
14029
14033
  model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
14030
14034
  context_window: exports_external.number().int().positive().optional().describe("GLOBAL context window (tokens) of the backend serving hindsight's " + "LLM ops \u2014 the declared size switchroom derives every token budget " + "from. Set this to the real window of whatever you point " + "`hindsight.llm` at (e.g. 32768 for a llama.cpp slot launched with " + "`-c 65536 -np 2`, 131072 for a large-window OpenRouter model). " + "Absent \u2192 a per-provider default: 200000 for `claude-code`, a " + "conservative 32768 for everything else. Overflowing a local " + "backend's window does NOT error \u2014 llama.cpp context-shift silently " + "drops the system prompt and the model answers conversationally " + "with HTTP 200 \u2014 so this value is what makes the failure " + "detectable at setup time instead of never."),
14035
+ base_url: exports_external.string().min(1).optional().describe("GLOBAL LLM base URL (upstream `HINDSIGHT_API_LLM_BASE_URL`). The " + "default endpoint every op inherits absent a per-op " + "`hindsight.llm.<op>.base_url`. Optional passthrough; unset \u2192 the " + "engine's provider default. When this points at host loopback the " + "hindsight container is forced onto host networking, same as a " + "per-op base URL, so the endpoint stays reachable (#3687)."),
14036
+ api_key: exports_external.string().min(1).optional().describe("GLOBAL LLM API key (upstream `HINDSIGHT_API_LLM_API_KEY`). Literal " + "or `vault:` reference. The default credential every op inherits " + "absent a per-op `hindsight.llm.<op>.api_key`; the engine reads it as " + "a plain env fallback. Optional passthrough (#3687)."),
14031
14037
  retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
14032
14038
  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."),
14033
14039
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
@@ -14164,6 +14170,7 @@ var init_schema = __esm(() => {
14164
14170
  auto_recall: exports_external.boolean().optional(),
14165
14171
  file: exports_external.boolean().optional(),
14166
14172
  isolation: exports_external.enum(["default", "strict"]).optional(),
14173
+ profile: exports_external.string().optional(),
14167
14174
  directive_capture_nudge: exports_external.boolean().optional(),
14168
14175
  observation_scopes: ObservationScopesSchema,
14169
14176
  observation_scope_strategy: ObservationScopeStrategySchema,
@@ -14205,7 +14212,23 @@ var init_schema = __esm(() => {
14205
14212
  max_query_chars: exports_external.number().int().min(1).optional(),
14206
14213
  parallel: exports_external.boolean().optional(),
14207
14214
  additional_banks: exports_external.array(exports_external.string()).optional(),
14208
- sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
14215
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional(),
14216
+ types: exports_external.array(exports_external.string()).optional(),
14217
+ skip_trivial: exports_external.boolean().optional(),
14218
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional()
14219
+ }).optional(),
14220
+ retain: exports_external.object({
14221
+ every_n_turns: exports_external.number().int().min(1).optional(),
14222
+ overlap_turns: exports_external.number().int().min(0).optional()
14223
+ }).optional(),
14224
+ bank_mission: exports_external.string().optional(),
14225
+ reflect_mission: exports_external.string().optional(),
14226
+ retain_mission: exports_external.string().optional(),
14227
+ observations_mission: exports_external.string().optional(),
14228
+ disposition: exports_external.object({
14229
+ skepticism: exports_external.number().int().min(1).max(5).optional(),
14230
+ literalism: exports_external.number().int().min(1).max(5).optional(),
14231
+ empathy: exports_external.number().int().min(1).max(5).optional()
14209
14232
  }).optional()
14210
14233
  }).optional(),
14211
14234
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
@@ -15594,6 +15617,43 @@ var init_thinking_effort_risk = __esm(() => {
15594
15617
  RISKY_EFFORTS = new Set(["medium", "high", "xhigh", "max"]);
15595
15618
  });
15596
15619
 
15620
+ // src/agents/model-aliases.ts
15621
+ function expandSrAlias(arg) {
15622
+ return SR_MODEL_ALIASES[arg.toLowerCase()] ?? arg;
15623
+ }
15624
+ function expandClaudeAlias(arg) {
15625
+ return CLAUDE_MODEL_ALIASES[arg.trim().toLowerCase()] ?? arg;
15626
+ }
15627
+ function canonicalModelToken(arg) {
15628
+ const trimmed = arg.trim();
15629
+ const lower = trimmed.toLowerCase();
15630
+ return lower.startsWith("claude-") ? lower : trimmed;
15631
+ }
15632
+ function expandModelAlias(arg) {
15633
+ return canonicalModelToken(expandSrAlias(expandClaudeAlias(arg)));
15634
+ }
15635
+ var CLAUDE_MODEL_ALIASES, SR_MODEL_ALIASES;
15636
+ var init_model_aliases = __esm(() => {
15637
+ CLAUDE_MODEL_ALIASES = {
15638
+ opus48: "claude-opus-4-8",
15639
+ "opus-4-8": "claude-opus-4-8"
15640
+ };
15641
+ SR_MODEL_ALIASES = {
15642
+ flash: "sr-gemini-2.5-flash",
15643
+ gemini: "sr-gemini-2.5-pro",
15644
+ deepseek: "sr-deepseek-v3",
15645
+ r1: "sr-deepseek-r1",
15646
+ glm: "sr-glm-5",
15647
+ codex: "sr-codex-5.5",
15648
+ grok: "sr-grok-4.5",
15649
+ kimi: "sr-kimi-k3",
15650
+ gpt: "sr-gpt-5.6-sol",
15651
+ sol: "sr-gpt-5.6-sol",
15652
+ terra: "sr-gpt-5.6-terra",
15653
+ luna: "sr-gpt-5.6-luna"
15654
+ };
15655
+ });
15656
+
15597
15657
  // telegram-plugin/gateway/model-command.ts
15598
15658
  function isClaudeModel(name) {
15599
15659
  const lower = name.toLowerCase();
@@ -15603,14 +15663,11 @@ function isClaudeModel(name) {
15603
15663
  return true;
15604
15664
  return lower.startsWith("claude-");
15605
15665
  }
15606
- var MODEL_ALIASES, CLAUDE_MODEL_ALIASES;
15666
+ var MODEL_ALIASES;
15607
15667
  var init_model_command = __esm(() => {
15608
15668
  init_thinking_effort_risk();
15669
+ init_model_aliases();
15609
15670
  MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
15610
- CLAUDE_MODEL_ALIASES = {
15611
- opus48: "claude-opus-4-8",
15612
- "opus-4-8": "claude-opus-4-8"
15613
- };
15614
15671
  });
15615
15672
 
15616
15673
  // src/litellm/timeout-budget.ts
@@ -17466,11 +17523,11 @@ var init_protocol = __esm(() => {
17466
17523
  OkRevokeGrantResponseSchema,
17467
17524
  OkApprovalRequestResponseSchema,
17468
17525
  OkApprovalLookupResponseSchema,
17526
+ OkApprovalConsumeRecordResponseSchema,
17469
17527
  OkApprovalConsumeResponseSchema,
17470
17528
  OkApprovalRevokeResponseSchema,
17471
17529
  OkApprovalListResponseSchema,
17472
17530
  OkApprovalRecordResponseSchema,
17473
- OkApprovalConsumeRecordResponseSchema,
17474
17531
  ErrorResponseSchema
17475
17532
  ]);
17476
17533
  });
@@ -18433,6 +18490,18 @@ function resolveHindsightPerOpLlm(llm) {
18433
18490
  }
18434
18491
  return out;
18435
18492
  }
18493
+ function resolveHindsightGlobalLlmExtras(llm) {
18494
+ const out = [];
18495
+ if (!llm)
18496
+ return out;
18497
+ const baseUrl = llm.base_url?.trim();
18498
+ const apiKey = llm.api_key?.trim();
18499
+ if (baseUrl)
18500
+ out.push(["HINDSIGHT_API_LLM_BASE_URL", baseUrl]);
18501
+ if (apiKey)
18502
+ out.push(["HINDSIGHT_API_LLM_API_KEY", apiKey]);
18503
+ return out;
18504
+ }
18436
18505
  function resolveHindsightLlm(llm, litellm) {
18437
18506
  const provider = llm?.provider?.trim() || "claude-code";
18438
18507
  const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
@@ -18449,6 +18518,7 @@ function collectHindsightLlmBaseUrls(llm, litellm) {
18449
18518
  out.push(v);
18450
18519
  };
18451
18520
  push(litellm?.baseUrl);
18521
+ push(llm?.base_url);
18452
18522
  for (const [k, v] of resolveHindsightPerOpLlm(llm)) {
18453
18523
  if (k.endsWith("_BASE_URL"))
18454
18524
  push(v);
@@ -18618,6 +18688,7 @@ function hindsightContainerEnvPairs(opts) {
18618
18688
  const pairs = [
18619
18689
  ["HINDSIGHT_API_LLM_PROVIDER", llmProvider],
18620
18690
  ["HINDSIGHT_API_LLM_MODEL", llmModel],
18691
+ ...resolveHindsightGlobalLlmExtras(llm),
18621
18692
  ...perOpLlm,
18622
18693
  ["HINDSIGHT_API_MCP_STATELESS", String(HINDSIGHT_DEFAULT_MCP_STATELESS)],
18623
18694
  ...hindsightLlmBudgetEnv(llm),
@@ -18845,6 +18916,7 @@ function generateHindsightComposeSnippet(llm, mirrorDir, litellm, gpu, perf, cpA
18845
18916
  const environment = [
18846
18917
  ` - HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
18847
18918
  ` - HINDSIGHT_API_LLM_MODEL=${llmModel}`,
18919
+ ...resolveHindsightGlobalLlmExtras(llm).map(([k, v]) => ` - ${k}=${v}`),
18848
18920
  ...perOpLlm.map(([k, v]) => ` - ${k}=${v}`),
18849
18921
  ...llmProvider === "claude-code" ? [` - ANTHROPIC_MODEL=${llmModel}`] : [],
18850
18922
  ...hostNetwork ? hindsightHostNetworkBindEnvPairs(apiPort).map(([k, v]) => ` - ${k}=${v}`) : [` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`],
@@ -28256,7 +28328,9 @@ function declaredRoutingMode(resolvedModel) {
28256
28328
  return isClaudeModel(resolvedModel) ? "passthrough" : "router-root";
28257
28329
  }
28258
28330
  function normalizeModelAlias(model) {
28259
- return model === "claude-fable-5" ? "fable" : model;
28331
+ if (model === "claude-fable-5")
28332
+ return "fable";
28333
+ return expandModelAlias(model);
28260
28334
  }
28261
28335
  function dedupe2(items) {
28262
28336
  const seen = new Set;
@@ -28959,8 +29033,9 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig, resolvedA
28959
29033
  if (!memory)
28960
29034
  return null;
28961
29035
  const agentMemory = switchroomConfig.agents[agentName]?.memory;
28962
- if (agentMemory?.auto_recall === false)
29036
+ if (resolveHindsightAutoRecall(switchroomConfig, agentName, resolvedAgentConfig) === false) {
28963
29037
  return null;
29038
+ }
28964
29039
  const sourcePath = resolveHindsightVendorPath();
28965
29040
  if (!existsSync17(sourcePath)) {
28966
29041
  process.stderr.write(`installHindsightPlugin: vendor source missing at ${sourcePath} ` + `\u2014 hindsight plugin NOT installed for ${agentName}. ` + `Likely a packaging regression: check the npm tarball's files array.
@@ -28969,13 +29044,14 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig, resolvedA
28969
29044
  }
28970
29045
  const destPath = join12(agentDir, ".claude", "plugins", "hindsight-memory");
28971
29046
  const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
28972
- const retainConfig = resolveHindsightRetainConfig(switchroomConfig, agentName);
29047
+ const retainConfig = resolveHindsightRetainConfig(switchroomConfig, agentName, resolvedAgentConfig);
29048
+ const observationScopeSettings = resolveHindsightObservationScopeSettings(switchroomConfig, agentName, resolvedAgentConfig);
28973
29049
  const recallTunables = resolveHindsightRecallTunables(resolveHindsightRecallConfig(switchroomConfig, agentName, resolvedAgentConfig));
28974
29050
  const contentOverrides = {};
28975
29051
  try {
28976
29052
  const vendorSettingsPath = join12(sourcePath, "settings.json");
28977
29053
  if (existsSync17(vendorSettingsPath)) {
28978
- const expectedSettings = renderHindsightSettingsOverrides(readFileSync15(vendorSettingsPath, "utf-8"), additionalBanks, retainConfig, recallTunables);
29054
+ const expectedSettings = renderHindsightSettingsOverrides(readFileSync15(vendorSettingsPath, "utf-8"), additionalBanks, retainConfig, recallTunables, observationScopeSettings);
28979
29055
  if (expectedSettings != null)
28980
29056
  contentOverrides["settings.json"] = expectedSettings;
28981
29057
  }
@@ -28993,21 +29069,34 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig, resolvedA
28993
29069
  }
28994
29070
  copyDirRecursive2(sourcePath, destPath);
28995
29071
  }
28996
- applyHindsightSettingsOverrides(destPath, additionalBanks, retainConfig, recallTunables);
29072
+ applyHindsightSettingsOverrides(destPath, additionalBanks, retainConfig, recallTunables, observationScopeSettings);
28997
29073
  applyHindsightHooksOverrides(destPath, recallTunables);
28998
29074
  const bankId = agentMemory?.collection ?? agentName;
28999
29075
  const mcpUrl = memory.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
29000
29076
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
29001
29077
  return { pluginDir: destPath, apiBaseUrl, bankId };
29002
29078
  }
29003
- function resolveHindsightRetainConfig(switchroomConfig, agentName) {
29004
- const resolved = switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {}) : undefined;
29005
- const retain = resolved?.memory?.retain;
29079
+ function resolveHindsightRetainConfig(switchroomConfig, agentName, resolvedAgentConfig) {
29080
+ const retain = resolvedAgentConfig ? resolvedAgentConfig.memory?.retain : switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {})?.memory?.retain : undefined;
29006
29081
  return {
29007
29082
  everyNTurns: retain?.every_n_turns ?? HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS,
29008
29083
  overlapTurns: retain?.overlap_turns ?? HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS
29009
29084
  };
29010
29085
  }
29086
+ function resolveHindsightAutoRecall(switchroomConfig, agentName, resolvedAgentConfig) {
29087
+ if (resolvedAgentConfig)
29088
+ return resolvedAgentConfig.memory?.auto_recall;
29089
+ if (!switchroomConfig)
29090
+ return;
29091
+ return resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {})?.memory?.auto_recall;
29092
+ }
29093
+ function resolveHindsightObservationScopeSettings(switchroomConfig, agentName, resolvedAgentConfig) {
29094
+ const memory = resolvedAgentConfig ? resolvedAgentConfig.memory : switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {})?.memory : undefined;
29095
+ return {
29096
+ observationScopes: memory?.observation_scopes,
29097
+ observationScopeStrategy: memory?.observation_scope_strategy
29098
+ };
29099
+ }
29011
29100
  function resolveHindsightRecallConfig(switchroomConfig, agentName, resolvedAgentConfig) {
29012
29101
  if (resolvedAgentConfig) {
29013
29102
  return resolvedAgentConfig.memory?.recall;
@@ -29032,7 +29121,7 @@ function applyHindsightHooksOverrides(pluginDestPath, tunables) {
29032
29121
  return;
29033
29122
  writeFileSyncIfChanged(hooksPath, next);
29034
29123
  }
29035
- function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retainConfig, recallTunables) {
29124
+ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retainConfig, recallTunables, observationScopeSettings) {
29036
29125
  const settingsPath = join12(pluginDestPath, "settings.json");
29037
29126
  if (!existsSync17(settingsPath))
29038
29127
  return;
@@ -29042,12 +29131,12 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retain
29042
29131
  } catch {
29043
29132
  return;
29044
29133
  }
29045
- const next = renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig, recallTunables);
29134
+ const next = renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig, recallTunables, observationScopeSettings);
29046
29135
  if (next == null)
29047
29136
  return;
29048
29137
  writeFileSyncIfChanged(settingsPath, next);
29049
29138
  }
29050
- function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig, recallTunables) {
29139
+ function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig, recallTunables, observationScopeSettings) {
29051
29140
  let settings;
29052
29141
  try {
29053
29142
  settings = JSON.parse(raw);
@@ -29069,6 +29158,14 @@ function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig, re
29069
29158
  if (additionalBanks.length > 0) {
29070
29159
  settings.recallAdditionalBanks = [...additionalBanks];
29071
29160
  }
29161
+ const observationScopes = observationScopeSettings.observationScopes;
29162
+ if (typeof observationScopes === "string" && observationScopes.length > 0) {
29163
+ settings.observationScopes = observationScopes;
29164
+ }
29165
+ const observationScopeStrategy = observationScopeSettings.observationScopeStrategy;
29166
+ if (typeof observationScopeStrategy === "string" && observationScopeStrategy.length > 0) {
29167
+ settings.observationScopeStrategy = observationScopeStrategy;
29168
+ }
29072
29169
  return JSON.stringify(settings, null, 2) + `
29073
29170
  `;
29074
29171
  }
@@ -29549,8 +29646,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
29549
29646
  retractStaleIntegrationKeys(integration, new Set(entries.map((e) => e.key)), settings.mcpServers, site1Protected);
29550
29647
  }
29551
29648
  installHindsightPlugin(name, agentDir, switchroomConfig, agentConfig);
29552
- const hindsightOn = isHindsightEnabled(switchroomConfig) && switchroomConfig.agents[name]?.memory?.auto_recall !== false;
29553
- if (hindsightOn) {
29649
+ if (hindsightAutoRecallEnabled) {
29554
29650
  settings.autoMemoryEnabled = false;
29555
29651
  }
29556
29652
  settings.hooks = buildSettingsHooksBlock({
@@ -29907,13 +30003,13 @@ ${body}
29907
30003
  const userBankMission = agentConfig.memory?.bank_mission;
29908
30004
  const userRetainMission = agentConfig.memory?.retain_mission;
29909
30005
  const seededRetainMission = await resolveRetainMissionPush(apiUrl, hindsightBankId, userRetainMission, formatAgentBankLabel(name, hindsightBankId));
29910
- const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
30006
+ const extras = resolveBankMissionExtras(agentConfig.memory, resolveMemoryProfile(agentConfig));
29911
30007
  const missions = { ...extras };
29912
30008
  if (seededRetainMission) {
29913
30009
  missions.retain_mission = seededRetainMission;
29914
30010
  }
29915
30011
  delete missions.observations_mission;
29916
- const seededObservationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, agentConfig.extends ?? DEFAULT_PROFILE, formatAgentBankLabel(name, hindsightBankId));
30012
+ const seededObservationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, resolveMemoryProfile(agentConfig), formatAgentBankLabel(name, hindsightBankId));
29917
30013
  if (seededObservationsMission) {
29918
30014
  missions.observations_mission = seededObservationsMission;
29919
30015
  }
@@ -30985,7 +31081,7 @@ ${body}
30985
31081
  trackBankOps(bankOpsChain.then(async (bankReady) => {
30986
31082
  if (!bankReady)
30987
31083
  return;
30988
- const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
31084
+ const extras = resolveBankMissionExtras(agentConfig.memory, resolveMemoryProfile(agentConfig));
30989
31085
  const missions = { ...extras };
30990
31086
  if (agentConfig.memory?.bank_mission) {
30991
31087
  missions.bank_mission = agentConfig.memory.bank_mission;
@@ -30994,7 +31090,7 @@ ${body}
30994
31090
  if (retainMission)
30995
31091
  missions.retain_mission = retainMission;
30996
31092
  delete missions.observations_mission;
30997
- const observationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, agentConfig.extends ?? DEFAULT_PROFILE, formatAgentBankLabel(name, hindsightBankId));
31093
+ const observationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, resolveMemoryProfile(agentConfig), formatAgentBankLabel(name, hindsightBankId));
30998
31094
  if (observationsMission)
30999
31095
  missions.observations_mission = observationsMission;
31000
31096
  if (Object.keys(missions).length > 0) {
@@ -31583,6 +31679,7 @@ never reaches them.`, TELEGRAM_ENV_PLACEHOLDER = `# Set your bot token: TELEGRAM
31583
31679
  var init_scaffold = __esm(() => {
31584
31680
  init_source();
31585
31681
  init_cron_unit_name();
31682
+ init_model_aliases();
31586
31683
  init_atomic();
31587
31684
  init_agent_owned_tree();
31588
31685
  init_generation_stamp();
@@ -39142,15 +39239,15 @@ async function getJson(url, opts) {
39142
39239
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
39143
39240
  try {
39144
39241
  const resp = await fetchImpl(url, { signal: controller.signal });
39145
- clearTimeout(timeout);
39146
39242
  if (!resp.ok)
39147
39243
  return { ok: false, reason: `HTTP ${resp.status}` };
39148
39244
  return { ok: true, data: await resp.json() };
39149
39245
  } catch (err) {
39150
- clearTimeout(timeout);
39151
39246
  if (err.name === "AbortError")
39152
39247
  return { ok: false, reason: "Timeout" };
39153
39248
  return { ok: false, reason: String(err.message ?? err) };
39249
+ } finally {
39250
+ clearTimeout(timeout);
39154
39251
  }
39155
39252
  }
39156
39253
  function summarizeBasedOn(reflect) {
@@ -39182,6 +39279,7 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
39182
39279
  totalDocuments: 0,
39183
39280
  totalFacts: 0,
39184
39281
  pendingOperations: 0,
39282
+ pendingConsolidation: 0,
39185
39283
  newestDocumentAt: null,
39186
39284
  unextractedDocuments: [],
39187
39285
  mentalModels: [],
@@ -39222,6 +39320,7 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
39222
39320
  totalDocuments: stats.data.total_documents ?? docItems.length,
39223
39321
  totalFacts: stats.data.total_nodes ?? 0,
39224
39322
  pendingOperations: stats.data.pending_operations ?? 0,
39323
+ pendingConsolidation: stats.data.pending_consolidation ?? 0,
39225
39324
  newestDocumentAt,
39226
39325
  unextractedDocuments: unextracted,
39227
39326
  activeDirectiveCount,
@@ -45067,6 +45166,84 @@ var init_manifest = __esm(() => {
45067
45166
  ]);
45068
45167
  });
45069
45168
 
45169
+ // src/config/memory-key-drift.ts
45170
+ function unwrapObjectNode(schema) {
45171
+ let s = schema;
45172
+ for (let i = 0;s && i < 10; i++) {
45173
+ const node = s;
45174
+ if (node.shape && typeof node.shape === "object") {
45175
+ return node;
45176
+ }
45177
+ if (node._def?.innerType) {
45178
+ s = node._def.innerType;
45179
+ continue;
45180
+ }
45181
+ if (node._def?.schema) {
45182
+ s = node._def.schema;
45183
+ continue;
45184
+ }
45185
+ break;
45186
+ }
45187
+ return;
45188
+ }
45189
+ function objectShapeKeys(schema) {
45190
+ const node = unwrapObjectNode(schema);
45191
+ return node?.shape ? new Set(Object.keys(node.shape)) : new Set;
45192
+ }
45193
+ function memoryChildSchema(memorySchema, child) {
45194
+ return unwrapObjectNode(memorySchema)?.shape?.[child];
45195
+ }
45196
+ function tierKnownKeys(memorySchema) {
45197
+ return {
45198
+ memory: objectShapeKeys(memorySchema),
45199
+ recall: objectShapeKeys(memoryChildSchema(memorySchema, "recall")),
45200
+ retain: objectShapeKeys(memoryChildSchema(memorySchema, "retain"))
45201
+ };
45202
+ }
45203
+ function isPlainObject2(v) {
45204
+ return typeof v === "object" && v !== null && !Array.isArray(v);
45205
+ }
45206
+ function diffScope(obj, scope, known, retired, out) {
45207
+ if (!isPlainObject2(obj))
45208
+ return;
45209
+ for (const key of Object.keys(obj)) {
45210
+ if (known.has(key))
45211
+ continue;
45212
+ out.push(key in retired ? { scope, key, replacement: retired[key] } : { scope, key });
45213
+ }
45214
+ }
45215
+ function findUnknownMemoryKeys(memory, tier = "agent") {
45216
+ if (!isPlainObject2(memory))
45217
+ return [];
45218
+ const known = KNOWN_BY_TIER[tier];
45219
+ const out = [];
45220
+ diffScope(memory, "memory", known.memory, RETIRED_MEMORY_KEYS, out);
45221
+ diffScope(memory.recall, "memory.recall", known.recall, RETIRED_MEMORY_RECALL_KEYS, out);
45222
+ diffScope(memory.retain, "memory.retain", known.retain, RETIRED_MEMORY_RETAIN_KEYS, out);
45223
+ return out.sort((a, b) => a.scope === b.scope ? a.key.localeCompare(b.key) : a.scope.localeCompare(b.scope));
45224
+ }
45225
+ var AGENT_KNOWN, MIRROR_KNOWN, KNOWN_BY_TIER, KNOWN_MEMORY_KEYS, KNOWN_MEMORY_RECALL_KEYS, KNOWN_MEMORY_RETAIN_KEYS, KNOWN_MIRROR_MEMORY_KEYS, KNOWN_MIRROR_MEMORY_RECALL_KEYS, KNOWN_MIRROR_MEMORY_RETAIN_KEYS, RETIRED_MEMORY_KEYS, RETIRED_MEMORY_RECALL_KEYS, RETIRED_MEMORY_RETAIN_KEYS;
45226
+ var init_memory_key_drift = __esm(() => {
45227
+ init_schema();
45228
+ AGENT_KNOWN = tierKnownKeys(AgentMemorySchema);
45229
+ MIRROR_KNOWN = tierKnownKeys(ProfileSchema.shape.memory);
45230
+ KNOWN_BY_TIER = {
45231
+ agent: AGENT_KNOWN,
45232
+ mirror: MIRROR_KNOWN
45233
+ };
45234
+ KNOWN_MEMORY_KEYS = AGENT_KNOWN.memory;
45235
+ KNOWN_MEMORY_RECALL_KEYS = AGENT_KNOWN.recall;
45236
+ KNOWN_MEMORY_RETAIN_KEYS = AGENT_KNOWN.retain;
45237
+ KNOWN_MIRROR_MEMORY_KEYS = MIRROR_KNOWN.memory;
45238
+ KNOWN_MIRROR_MEMORY_RECALL_KEYS = MIRROR_KNOWN.recall;
45239
+ KNOWN_MIRROR_MEMORY_RETAIN_KEYS = MIRROR_KNOWN.retain;
45240
+ RETIRED_MEMORY_KEYS = {};
45241
+ RETIRED_MEMORY_RECALL_KEYS = {
45242
+ min_overlap: "removed in #3761 \u2014 the lexical-overlap recall gate it tuned was deleted " + "outright, with no replacement floor; delete the line"
45243
+ };
45244
+ RETIRED_MEMORY_RETAIN_KEYS = {};
45245
+ });
45246
+
45070
45247
  // src/cli/doctor-memory.ts
45071
45248
  import { execFileSync as execFileSync19 } from "node:child_process";
45072
45249
  function classifyDirectiveCount(count, bankLabel) {
@@ -45105,28 +45282,75 @@ function classifyShmSize(bytes) {
45105
45282
  const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
45106
45283
  return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
45107
45284
  }
45108
- function classifyConsolidationBacklog(queueDepth, oldestAgeSecs = null) {
45285
+ function classifyAsyncOpQueue(queueDepth, oldestAgeSecs = null) {
45109
45286
  const ageStr = oldestAgeSecs !== null && oldestAgeSecs > 0 ? `, oldest ${Math.round(oldestAgeSecs / 60)}m old` : "";
45110
- const base = `${queueDepth} pending consolidation op(s)${ageStr}`;
45111
- if (queueDepth >= CONSOLIDATION_BACKLOG_FAIL) {
45287
+ const base = `${queueDepth} pending async op(s)${ageStr}`;
45288
+ if (queueDepth >= OP_QUEUE_FAIL) {
45289
+ return {
45290
+ name: "hindsight async op queue",
45291
+ status: "fail",
45292
+ detail: `${base} \u2014 the operation queue is deep enough to be wedged; ops are ` + `enqueued but not draining (a stuck reaper / claim stall)`,
45293
+ fix: "Check `docker logs switchroom-hindsight` for claim/reaper errors " + "(quota/429/shm, stuck 'processing' ops); restart with `switchroom " + "memory --restart` if the worker is stuck."
45294
+ };
45295
+ }
45296
+ if (queueDepth >= OP_QUEUE_WARN) {
45297
+ return {
45298
+ name: "hindsight async op queue",
45299
+ status: "warn",
45300
+ detail: `${base} \u2014 non-terminal ops are building faster than they drain; ` + `claims may be lagging (check the reaper before it wedges)`
45301
+ };
45302
+ }
45303
+ return {
45304
+ name: "hindsight async op queue",
45305
+ status: "ok",
45306
+ detail: queueDepth > 0 ? base : "queue drained (0 pending)"
45307
+ };
45308
+ }
45309
+ function classifyConsolidationBacklog(pendingConsolidation) {
45310
+ const base = `${pendingConsolidation} memory unit(s) pending consolidation`;
45311
+ if (pendingConsolidation >= CONSOLIDATION_BACKLOG_FAIL) {
45112
45312
  return {
45113
45313
  name: "hindsight consolidation backlog",
45114
45314
  status: "fail",
45115
- detail: `${base} \u2014 queue is deep enough to be wedged; retains are landing but ` + `not consolidating into recall (drains ~7 ops/hr under the #2894 throttle)`,
45116
- fix: "Check `docker logs switchroom-hindsight` for the queue-lag WARNING and " + "consolidation errors (quota/429/shm); restart with `switchroom memory " + "--restart` if the worker is stuck."
45315
+ detail: `${base} \u2014 consolidation has stalled far behind ingest; retained ` + `memory is landing but not reaching recall (drains ~7 ops/hr under ` + `the #2894 throttle)`,
45316
+ fix: "Check `docker logs switchroom-hindsight` for consolidation errors " + "(quota/429/shm, HNSW index faults) and the consolidation-failure-" + "streak signal; a bank legitimately mid-ingest can read deep, so " + "confirm the number is not draining before restarting."
45117
45317
  };
45118
45318
  }
45119
- if (queueDepth >= CONSOLIDATION_BACKLOG_WARN) {
45319
+ if (pendingConsolidation >= CONSOLIDATION_BACKLOG_WARN) {
45120
45320
  return {
45121
45321
  name: "hindsight consolidation backlog",
45122
45322
  status: "warn",
45123
- detail: `${base} \u2014 building faster than it drains (~7 ops/hr under the #2894 ` + `throttle); recall may lag recent turns until it catches up`
45323
+ detail: `${base} \u2014 building faster than it drains (~7 ops/hr under the #2894 ` + `throttle); recall may lag recent turns until it catches up (a bank ` + `mid-ingest can read deep \u2014 confirm it is draining)`
45124
45324
  };
45125
45325
  }
45126
45326
  return {
45127
45327
  name: "hindsight consolidation backlog",
45128
45328
  status: "ok",
45129
- detail: queueDepth > 0 ? base : "queue drained (0 pending)"
45329
+ detail: pendingConsolidation > 0 ? base : "consolidation drained (0 pending)"
45330
+ };
45331
+ }
45332
+ function classifyBankConfigReachability(result, bankId) {
45333
+ const name = BANK_CONFIG_API_CHECK_NAME;
45334
+ if (result.ok) {
45335
+ return {
45336
+ name,
45337
+ status: "ok",
45338
+ detail: `retain_mission readable on bank ${bankId} \u2014 the fail-closed seed path (#3529) can read the field`
45339
+ };
45340
+ }
45341
+ if (result.reason === "Unexpected shape") {
45342
+ return {
45343
+ name,
45344
+ status: "fail",
45345
+ detail: `GET /v1/default/banks/${bankId}/config returned no retain_mission key \u2014 ` + `the bank-config API is disabled or the field was renamed/renested. ` + `Retain-mission seeding is fail-closed (#3529): with this read failing, ` + `apply seeds retain_mission for NO agent (including new ones) and still ` + `exits 0, so the fleet silently runs with unset retain missions.`,
45346
+ fix: "Confirm `HINDSIGHT_API_ENABLE_BANK_CONFIG_API` is true on the Hindsight " + "deployment (config.py default True) and that GET .../config still nests " + "`retain_mission` under `config`. If Hindsight renamed/renested the field, " + "update `fetchBankMissionField` in src/memory/hindsight.ts to match."
45347
+ };
45348
+ }
45349
+ return {
45350
+ name,
45351
+ status: "warn",
45352
+ detail: `could not probe the bank-config API on ${bankId}: ${result.reason} \u2014 ` + `retain-mission seeding is fail-closed (#3529) and can't be confirmed. If ` + `this persists it silently skips seeding for every agent.`,
45353
+ fix: "Check the hindsight container is up and `HINDSIGHT_API_ENABLE_BANK_CONFIG_API` " + "is enabled; re-run once it is reachable."
45130
45354
  };
45131
45355
  }
45132
45356
  function classifyExtractionLogs(logs) {
@@ -45533,7 +45757,7 @@ function classifyToolContract(advertised) {
45533
45757
  }
45534
45758
  return results;
45535
45759
  }
45536
- var MIN_HINDSIGHT_SHM_BYTES, MAX_DIRECTIVES = 30, DIRECTIVE_WARN_THRESHOLD = 24, CONSOLIDATION_BACKLOG_WARN = 25, CONSOLIDATION_BACKLOG_FAIL = 200;
45760
+ var MIN_HINDSIGHT_SHM_BYTES, MAX_DIRECTIVES = 30, DIRECTIVE_WARN_THRESHOLD = 24, OP_QUEUE_WARN = 25, OP_QUEUE_FAIL = 200, CONSOLIDATION_BACKLOG_WARN = 1000, CONSOLIDATION_BACKLOG_FAIL = 1e4, BANK_CONFIG_API_CHECK_NAME = "hindsight bank-config API";
45537
45761
  var init_doctor_memory = __esm(() => {
45538
45762
  init_hindsight_tools();
45539
45763
  init_hindsight_repair();
@@ -45741,8 +45965,83 @@ var init_doctor_hnsw_index = __esm(() => {
45741
45965
  });
45742
45966
 
45743
45967
  // src/cli/doctor-observation-scopes.ts
45744
- function computeScopeSaturation(scopes, cap, warnRatio = OBSERVATION_SCOPE_WARN_RATIO) {
45745
- if (cap < 0)
45968
+ function fnmatchTranslate(pat) {
45969
+ const ANY = "[\\s\\S]";
45970
+ let res = "";
45971
+ let i = 0;
45972
+ const n = pat.length;
45973
+ while (i < n) {
45974
+ const c = pat[i];
45975
+ i += 1;
45976
+ if (c === "*") {
45977
+ res += ANY + "*";
45978
+ } else if (c === "?") {
45979
+ res += ANY;
45980
+ } else if (c === "[") {
45981
+ let j = i;
45982
+ if (j < n && pat[j] === "!")
45983
+ j += 1;
45984
+ if (j < n && pat[j] === "]")
45985
+ j += 1;
45986
+ while (j < n && pat[j] !== "]")
45987
+ j += 1;
45988
+ if (j >= n) {
45989
+ res += "\\[";
45990
+ } else {
45991
+ let stuff = pat.slice(i, j).replace(/\\/g, "\\\\");
45992
+ i = j + 1;
45993
+ if (stuff.startsWith("!"))
45994
+ stuff = "^" + stuff.slice(1);
45995
+ else if (stuff.startsWith("^") || stuff.startsWith("["))
45996
+ stuff = "\\" + stuff;
45997
+ res += "[" + stuff + "]";
45998
+ }
45999
+ } else {
46000
+ res += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46001
+ }
46002
+ }
46003
+ return new RegExp("^(?:" + res + ")$");
46004
+ }
46005
+ function fnmatchcase(name, pat) {
46006
+ return fnmatchTranslate(pat).test(name);
46007
+ }
46008
+ function parseScopeLimitRules(raw) {
46009
+ if (!Array.isArray(raw))
46010
+ return [];
46011
+ const rules = [];
46012
+ for (const entry of raw) {
46013
+ if (entry == null || typeof entry !== "object")
46014
+ continue;
46015
+ const scope = entry.scope;
46016
+ const limit = entry.limit;
46017
+ if (!Array.isArray(scope) || scope.length === 0)
46018
+ continue;
46019
+ if (!scope.every((g) => typeof g === "string" && g.length > 0))
46020
+ continue;
46021
+ if (typeof limit !== "number" || !Number.isInteger(limit))
46022
+ continue;
46023
+ rules.push({ globs: scope, limit });
46024
+ }
46025
+ return rules;
46026
+ }
46027
+ function scopeMatchesGlobs(globs, tags) {
46028
+ if (tags.length === 0)
46029
+ return false;
46030
+ if (!tags.every((t) => globs.some((g) => fnmatchcase(t, g))))
46031
+ return false;
46032
+ if (!globs.every((g) => tags.some((t) => fnmatchcase(t, g))))
46033
+ return false;
46034
+ return true;
46035
+ }
46036
+ function effectiveScopeLimit(rules, tags, bankCap) {
46037
+ for (const rule of rules) {
46038
+ if (scopeMatchesGlobs(rule.globs, tags))
46039
+ return rule.limit;
46040
+ }
46041
+ return bankCap;
46042
+ }
46043
+ function computeScopeSaturation(scopes, cap, warnRatio = OBSERVATION_SCOPE_WARN_RATIO, scopeLimitRules = []) {
46044
+ if (cap < 0 && scopeLimitRules.length === 0)
45746
46045
  return [];
45747
46046
  const sets = scopes.map((s) => ({ tags: s.tags, set: new Set(s.tags), count: s.count }));
45748
46047
  const postings = new Map;
@@ -45781,8 +46080,11 @@ function computeScopeSaturation(scopes, cap, warnRatio = OBSERVATION_SCOPE_WARN_
45781
46080
  if (contains)
45782
46081
  effective += other.count;
45783
46082
  }
45784
- const status = cap > 0 && effective >= cap ? "fail" : cap > 0 && effective >= cap * warnRatio ? "warn" : "ok";
45785
- out.push({ tags: [...entry.tags], exact: entry.count, effective, cap, status });
46083
+ const scopeCap = effectiveScopeLimit(scopeLimitRules, entry.tags, cap);
46084
+ if (scopeCap < 0)
46085
+ continue;
46086
+ const status = scopeCap > 0 && effective >= scopeCap ? "fail" : scopeCap > 0 && effective >= scopeCap * warnRatio ? "warn" : "ok";
46087
+ out.push({ tags: [...entry.tags], exact: entry.count, effective, cap: scopeCap, status });
45786
46088
  }
45787
46089
  out.sort((a, b) => b.effective - a.effective);
45788
46090
  return out;
@@ -45806,17 +46108,17 @@ function classifyObservationScopeSaturation(reports) {
45806
46108
  detail: `could not read observation scopes for ${unreadable.length} bank(s): ` + unreadable.map((r) => `${r.bankId} (${r.reason ?? "unknown"})`).join(", ")
45807
46109
  };
45808
46110
  }
45809
- const unjudged = readable.filter((r) => r.hasScopeLimitRules || !r.observationsEnabled);
45810
- const judged = readable.filter((r) => !r.hasScopeLimitRules && r.observationsEnabled);
46111
+ const unjudged = readable.filter((r) => !r.observationsEnabled);
46112
+ const judged = readable.filter((r) => r.observationsEnabled);
45811
46113
  const notes = [];
45812
46114
  if (unreadable.length > 0) {
45813
46115
  notes.push(`${unreadable.length} bank(s) unreadable: ` + unreadable.map((r) => `${r.bankId} (${r.reason ?? "unknown"})`).join(", "));
45814
46116
  }
45815
- const withRules = unjudged.filter((r) => r.hasScopeLimitRules);
46117
+ const withRules = judged.filter((r) => r.hasScopeLimitRules);
45816
46118
  if (withRules.length > 0) {
45817
- notes.push(`${withRules.length} bank(s) not judged \u2014 observation_scope_limits sets a ` + `per-scope cap this check does not resolve: ${withRules.map((r) => r.bankId).join(", ")}`);
46119
+ notes.push(`${withRules.length} bank(s) judged with per-scope observation_scope_limits ` + `glob caps: ${withRules.map((r) => r.bankId).join(", ")}`);
45818
46120
  }
45819
- const disabled = unjudged.filter((r) => !r.hasScopeLimitRules && !r.observationsEnabled);
46121
+ const disabled = unjudged.filter((r) => !r.observationsEnabled);
45820
46122
  if (disabled.length > 0) {
45821
46123
  notes.push(`${disabled.length} bank(s) have observations disabled: ` + disabled.map((r) => r.bankId).join(", "));
45822
46124
  }
@@ -45831,8 +46133,8 @@ function classifyObservationScopeSaturation(reports) {
45831
46133
  const closed = judged.filter((r) => r.cap === 0);
45832
46134
  const unlimited = judged.filter((r) => r.cap < 0);
45833
46135
  const capped = judged.filter((r) => r.cap > 0);
45834
- const failing = capped.flatMap((r) => r.saturated.filter((s) => s.status === "fail").map((s) => ({ report: r, scope: s })));
45835
- const warning = capped.flatMap((r) => r.saturated.filter((s) => s.status === "warn").map((s) => ({ report: r, scope: s })));
46136
+ const failing = judged.flatMap((r) => r.saturated.filter((s) => s.status === "fail").map((s) => ({ report: r, scope: s })));
46137
+ const warning = judged.flatMap((r) => r.saturated.filter((s) => s.status === "warn").map((s) => ({ report: r, scope: s })));
45836
46138
  failing.sort((a, b) => b.scope.effective - a.scope.effective);
45837
46139
  warning.sort((a, b) => b.scope.effective - a.scope.effective);
45838
46140
  const backlog = (rows) => {
@@ -45866,7 +46168,7 @@ function classifyObservationScopeSaturation(reports) {
45866
46168
  const detail = warning.length > 0 ? scopeClause + growthSuffix + suffix : `${growthNote}${suffix}`;
45867
46169
  return { name, status: "warn", detail, ...warning.length > 0 ? { fix } : {} };
45868
46170
  }
45869
- const totalScopes = capped.reduce((n, r) => n + r.scopeCount, 0);
46171
+ const totalScopes = capped.reduce((n, r) => n + r.judgedScopeCount, 0);
45870
46172
  const detailBase = capped.length === 0 ? `no bank has a positive observation cap` : `${totalScopes} scope(s) across ${capped.length} bank(s) below ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap`;
45871
46173
  const extra = [];
45872
46174
  if (unlimited.length > 0) {
@@ -45881,24 +46183,6 @@ function classifyObservationScopeSaturation(reports) {
45881
46183
  detail: detailBase + (extra.length > 0 ? ` \u00b7 ${extra.join(" \u00b7 ")}` : "") + suffix
45882
46184
  };
45883
46185
  }
45884
- async function getJson3(url, opts) {
45885
- const fetchImpl = opts?.fetchImpl ?? fetch;
45886
- const timeoutMs = opts?.timeoutMs ?? 15000;
45887
- const controller = new AbortController;
45888
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
45889
- try {
45890
- const resp = await fetchImpl(url, { signal: controller.signal });
45891
- clearTimeout(timeout);
45892
- if (!resp.ok)
45893
- return { ok: false, reason: `HTTP ${resp.status}` };
45894
- return { ok: true, data: await resp.json() };
45895
- } catch (err) {
45896
- clearTimeout(timeout);
45897
- if (err.name === "AbortError")
45898
- return { ok: false, reason: "Timeout" };
45899
- return { ok: false, reason: String(err.message ?? err) };
45900
- }
45901
- }
45902
46186
  async function inspectBankScopes(apiUrl, bankId, opts) {
45903
46187
  const base = hindsightRestBase(apiUrl);
45904
46188
  const bank = encodeURIComponent(bankId);
@@ -45910,11 +46194,12 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45910
46194
  observationsEnabled: false,
45911
46195
  hasScopeLimitRules: false,
45912
46196
  scopeCount: 0,
46197
+ judgedScopeCount: 0,
45913
46198
  untaggedCount: 0,
45914
46199
  saturated: [],
45915
46200
  pendingConsolidation: null
45916
46201
  });
45917
- const cfg = await getJson3(`${base}/v1/default/banks/${bank}/config`, opts);
46202
+ const cfg = await getJson(`${base}/v1/default/banks/${bank}/config`, opts);
45918
46203
  if (!cfg.ok)
45919
46204
  return fail4(cfg.reason);
45920
46205
  const config = cfg.data?.config;
@@ -45924,10 +46209,10 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45924
46209
  if (typeof rawCap !== "number" || !Number.isFinite(rawCap)) {
45925
46210
  return fail4("Unexpected shape");
45926
46211
  }
45927
- const rules = config["observation_scope_limits"];
45928
- const hasScopeLimitRules = Array.isArray(rules) && rules.length > 0;
46212
+ const scopeLimitRules = parseScopeLimitRules(config["observation_scope_limits"]);
46213
+ const hasScopeLimitRules = scopeLimitRules.length > 0;
45929
46214
  const observationsEnabled = config["enable_observations"] !== false;
45930
- const scopesResp = await getJson3(`${base}/v1/default/banks/${bank}/observations/scopes`, opts);
46215
+ const scopesResp = await getJson(`${base}/v1/default/banks/${bank}/observations/scopes`, opts);
45931
46216
  if (!scopesResp.ok)
45932
46217
  return fail4(scopesResp.reason);
45933
46218
  const rawScopes = scopesResp.data?.scopes;
@@ -45943,11 +46228,12 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45943
46228
  continue;
45944
46229
  scopes.push({ tags, count });
45945
46230
  }
45946
- const saturated = computeScopeSaturation(scopes, rawCap).filter((s) => s.status !== "ok");
46231
+ const allJudged = computeScopeSaturation(scopes, rawCap, OBSERVATION_SCOPE_WARN_RATIO, scopeLimitRules);
46232
+ const saturated = allJudged.filter((s) => s.status !== "ok");
45947
46233
  const untaggedCount = scopes.filter((s) => s.tags.length === 0).reduce((n, s) => n + s.count, 0);
45948
46234
  let pendingConsolidation = null;
45949
- if (saturated.length > 0 && !hasScopeLimitRules && observationsEnabled) {
45950
- const stats = await getJson3(`${base}/v1/default/banks/${bank}/stats`, opts);
46235
+ if (saturated.length > 0 && observationsEnabled) {
46236
+ const stats = await getJson(`${base}/v1/default/banks/${bank}/stats`, opts);
45951
46237
  if (stats.ok && typeof stats.data?.pending_consolidation === "number") {
45952
46238
  pendingConsolidation = stats.data.pending_consolidation;
45953
46239
  }
@@ -45959,6 +46245,7 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45959
46245
  observationsEnabled,
45960
46246
  hasScopeLimitRules,
45961
46247
  scopeCount: scopes.length,
46248
+ judgedScopeCount: allJudged.length,
45962
46249
  untaggedCount,
45963
46250
  saturated,
45964
46251
  pendingConsolidation
@@ -45982,7 +46269,8 @@ var init_doctor_observation_scopes = __esm(() => {
45982
46269
  });
45983
46270
 
45984
46271
  // src/hindsight-watch/install-cron.ts
45985
- import { existsSync as existsSync63, mkdirSync as mkdirSync35, readFileSync as readFileSync60, renameSync as renameSync15, writeFileSync as writeFileSync22 } from "node:fs";
46272
+ import { execFileSync as execFileSync20 } from "node:child_process";
46273
+ import { chownSync as chownSync6, existsSync as existsSync63, mkdirSync as mkdirSync35, readFileSync as readFileSync60, renameSync as renameSync15, statSync as statSync38, writeFileSync as writeFileSync22 } from "node:fs";
45986
46274
  import { dirname as dirname22 } from "node:path";
45987
46275
  function renderCron(opts) {
45988
46276
  return `# switchroom hindsight-watch \u2014 model-free memory watchdog.
@@ -46009,6 +46297,58 @@ function installCron(opts) {
46009
46297
  renameSync15(tmp, path6);
46010
46298
  return { status: "installed", path: path6, content };
46011
46299
  }
46300
+ function ensureLogFile(opts) {
46301
+ const path6 = opts.path ?? CRON_LOG_PATH;
46302
+ if (existsSync63(path6))
46303
+ return { status: "unchanged", path: path6 };
46304
+ const { uid, gid } = (opts.resolveIds ?? defaultIdResolver)(opts.user);
46305
+ mkdirSync35(dirname22(path6), { recursive: true });
46306
+ writeFileSync22(path6, "", { mode: 420, flag: "a" });
46307
+ const isRoot = process.getuid?.() === 0;
46308
+ let alreadyOwned = false;
46309
+ try {
46310
+ const st = statSync38(path6);
46311
+ alreadyOwned = st.uid === uid && st.gid === gid;
46312
+ } catch {}
46313
+ if (isRoot && !alreadyOwned) {
46314
+ try {
46315
+ chownSync6(path6, uid, gid);
46316
+ } catch (e) {
46317
+ if (e.code !== "EPERM")
46318
+ throw e;
46319
+ }
46320
+ }
46321
+ return { status: "created", path: path6 };
46322
+ }
46323
+ function renderLogrotate(opts) {
46324
+ return `${opts.logPath} {
46325
+ ` + ` weekly
46326
+ ` + ` rotate 8
46327
+ ` + ` compress
46328
+ ` + ` delaycompress
46329
+ ` + ` missingok
46330
+ ` + ` notifempty
46331
+ ` + ` copytruncate
46332
+ ` + ` su ${opts.user} ${opts.user}
46333
+ ` + `}
46334
+ `;
46335
+ }
46336
+ function installLogrotate(opts) {
46337
+ const path6 = opts.path ?? LOGROTATE_PATH;
46338
+ const content = renderLogrotate({ logPath: opts.logPath ?? CRON_LOG_PATH, user: opts.user });
46339
+ if (existsSync63(path6)) {
46340
+ try {
46341
+ if (readFileSync60(path6, "utf8") === content) {
46342
+ return { status: "unchanged", path: path6, content };
46343
+ }
46344
+ } catch {}
46345
+ }
46346
+ mkdirSync35(dirname22(path6), { recursive: true });
46347
+ const tmp = `${path6}.${process.pid}.tmp`;
46348
+ writeFileSync22(tmp, content, { mode: 420 });
46349
+ renameSync15(tmp, path6);
46350
+ return { status: "installed", path: path6, content };
46351
+ }
46012
46352
  function parseCronUser(content) {
46013
46353
  for (const line of content.split(`
46014
46354
  `)) {
@@ -46041,7 +46381,14 @@ function reconcileCron(opts) {
46041
46381
  content: r.content
46042
46382
  };
46043
46383
  }
46044
- var CRON_PATH = "/etc/cron.d/hindsight-watch", CRON_SCHEDULE = "*/15 * * * *", CRON_LOG_PATH = "/var/log/hindsight-watch.log", CRON_LOCK_PATH = "/run/lock/hindsight-watch.lock";
46384
+ var CRON_PATH = "/etc/cron.d/hindsight-watch", CRON_SCHEDULE = "*/15 * * * *", CRON_LOG_PATH = "/var/log/hindsight-watch.log", LOGROTATE_PATH = "/etc/logrotate.d/hindsight-watch", CRON_LOCK_PATH = "/run/lock/hindsight-watch.lock", defaultIdResolver = (user) => {
46385
+ const uid = Number(execFileSync20("id", ["-u", user], { encoding: "utf8" }).trim());
46386
+ const gid = Number(execFileSync20("id", ["-g", user], { encoding: "utf8" }).trim());
46387
+ if (!Number.isInteger(uid) || !Number.isInteger(gid)) {
46388
+ throw new Error(`could not resolve uid/gid for user ${JSON.stringify(user)}`);
46389
+ }
46390
+ return { uid, gid };
46391
+ };
46045
46392
  var init_install_cron = () => {};
46046
46393
 
46047
46394
  // src/hindsight-watch/thresholds.ts
@@ -46190,7 +46537,7 @@ var init_state2 = __esm(() => {
46190
46537
  });
46191
46538
 
46192
46539
  // src/cli/doctor-hindsight-watch.ts
46193
- import { existsSync as existsSync64, statSync as statSync38 } from "node:fs";
46540
+ import { existsSync as existsSync64, statSync as statSync39 } from "node:fs";
46194
46541
  function classifyWatchArmed(cronPresent, stateMtimeMs, now) {
46195
46542
  const name = "hindsight-watch armed";
46196
46543
  if (stateMtimeMs === null) {
@@ -46228,7 +46575,7 @@ function classifyWatchArmed(cronPresent, stateMtimeMs, now) {
46228
46575
  function checkHindsightWatchArmed(now = Date.now(), cronPath = CRON_PATH, statePath = defaultStatePath2()) {
46229
46576
  let mtime = null;
46230
46577
  try {
46231
- mtime = statSync38(statePath).mtimeMs;
46578
+ mtime = statSync39(statePath).mtimeMs;
46232
46579
  } catch {}
46233
46580
  return classifyWatchArmed(existsSync64(cronPath), mtime, now);
46234
46581
  }
@@ -46789,10 +47136,11 @@ function requiredKeyModels(config, proxyModels) {
46789
47136
  apiKeyRef: ref && ref.length > 0 ? ref : null
46790
47137
  });
46791
47138
  };
46792
- consider(llm.model, llm.provider, undefined, "hindsight.llm.model (global)");
46793
- consider(llm.retain?.model, llm.retain?.provider, llm.retain?.api_key, "hindsight.llm.retain");
46794
- consider(llm.reflect?.model, llm.reflect?.provider, llm.reflect?.api_key, "hindsight.llm.reflect");
46795
- consider(llm.consolidation?.model, llm.consolidation?.provider, llm.consolidation?.api_key, "hindsight.llm.consolidation");
47139
+ const globalKey = llm.api_key;
47140
+ consider(llm.model, llm.provider, globalKey, "hindsight.llm.model (global)");
47141
+ consider(llm.retain?.model, llm.retain?.provider, llm.retain?.api_key ?? globalKey, "hindsight.llm.retain");
47142
+ consider(llm.reflect?.model, llm.reflect?.provider, llm.reflect?.api_key ?? globalKey, "hindsight.llm.reflect");
47143
+ consider(llm.consolidation?.model, llm.consolidation?.provider, llm.consolidation?.api_key ?? globalKey, "hindsight.llm.consolidation");
46796
47144
  return out;
46797
47145
  }
46798
47146
  function classifyAllowlist(models) {
@@ -47467,7 +47815,7 @@ import {
47467
47815
  constants as fsConstants4,
47468
47816
  existsSync as existsSync66,
47469
47817
  realpathSync as realpathSync5,
47470
- statSync as statSync39
47818
+ statSync as statSync40
47471
47819
  } from "node:fs";
47472
47820
  import { userInfo, homedir as homedir33 } from "node:os";
47473
47821
  import { join as join61 } from "node:path";
@@ -47485,7 +47833,7 @@ function defaultStatVault(path6) {
47485
47833
  let uid = -1;
47486
47834
  let mode = 0;
47487
47835
  try {
47488
- const s = statSync39(real);
47836
+ const s = statSync40(real);
47489
47837
  uid = s.uid;
47490
47838
  mode = s.mode & 511;
47491
47839
  } catch {
@@ -49648,7 +49996,7 @@ __export(exports_voice_sidecar_token, {
49648
49996
  VOICE_SIDECAR_TOKEN_ENV: () => VOICE_SIDECAR_TOKEN_ENV
49649
49997
  });
49650
49998
  import { randomBytes as randomBytes12 } from "node:crypto";
49651
- import { chmodSync as chmodSync13, chownSync as chownSync6, existsSync as existsSync69, mkdirSync as mkdirSync40, readFileSync as readFileSync67, rmSync as rmSync12, writeFileSync as writeFileSync27 } from "node:fs";
49999
+ import { chmodSync as chmodSync13, chownSync as chownSync7, existsSync as existsSync69, mkdirSync as mkdirSync40, readFileSync as readFileSync67, rmSync as rmSync12, writeFileSync as writeFileSync27 } from "node:fs";
49652
50000
  import { dirname as dirname27 } from "node:path";
49653
50001
  async function defaultResolveOrSeedToken(home2, writeErr) {
49654
50002
  const [{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 }, { resolveOperatorVaultPassphrase }] = await Promise.all([
@@ -49722,7 +50070,7 @@ async function provisionVoiceSidecarToken(composePath, home2, ctx) {
49722
50070
  chmodSync13(envPath, 384);
49723
50071
  if (ctx.operatorUid !== undefined && process.geteuid?.() === 0) {
49724
50072
  try {
49725
- chownSync6(envPath, ctx.operatorUid, ctx.operatorUid);
50073
+ chownSync7(envPath, ctx.operatorUid, ctx.operatorUid);
49726
50074
  } catch {}
49727
50075
  }
49728
50076
  ctx.writeOut(source_default.gray(` Wrote voice-sidecar token to ${envPath} (0600)
@@ -49769,13 +50117,13 @@ __export(exports_apply, {
49769
50117
  DEFAULT_COMPOSE_PATH: () => DEFAULT_COMPOSE_PATH,
49770
50118
  COMPOSE_PROJECT: () => COMPOSE_PROJECT2
49771
50119
  });
49772
- import { accessSync as accessSync2, chmodSync as chmodSync14, chownSync as chownSync7, constants as fsConstants5, copyFileSync as copyFileSync10, existsSync as existsSync70, mkdirSync as mkdirSync41, readFileSync as readFileSync68, readdirSync as readdirSync24, renameSync as renameSync17, statSync as statSync40, writeFileSync as writeFileSync28 } from "node:fs";
50120
+ import { accessSync as accessSync2, chmodSync as chmodSync14, chownSync as chownSync8, constants as fsConstants5, copyFileSync as copyFileSync10, existsSync as existsSync70, mkdirSync as mkdirSync41, readFileSync as readFileSync68, readdirSync as readdirSync24, renameSync as renameSync17, statSync as statSync41, writeFileSync as writeFileSync28 } from "node:fs";
49773
50121
  import { mkdir as mkdir2 } from "node:fs/promises";
49774
50122
  import { spawnSync as childSpawnSync } from "node:child_process";
49775
50123
  import readline from "node:readline";
49776
50124
  import { dirname as dirname28, join as join69, resolve as resolve40 } from "node:path";
49777
50125
  import { homedir as homedir37 } from "node:os";
49778
- import { execFileSync as execFileSync20 } from "node:child_process";
50126
+ import { execFileSync as execFileSync21 } from "node:child_process";
49779
50127
  function effectiveLiteLLMEnabled(config, agentResolvedLitellm) {
49780
50128
  return agentResolvedLitellm?.enabled ?? config.litellm?.enabled ?? false;
49781
50129
  }
@@ -50074,7 +50422,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
50074
50422
  try {
50075
50423
  let cfgMode = 420;
50076
50424
  try {
50077
- cfgMode = statSync40(switchroomConfigPath).mode & 511;
50425
+ cfgMode = statSync41(switchroomConfigPath).mode & 511;
50078
50426
  } catch {}
50079
50427
  writeConfigFileSync(switchroomConfigPath, configText, cfgMode);
50080
50428
  writeOut(source_default.gray(` ~ litellm: granted read-ACL on per-agent keys (updated ${switchroomConfigPath})
@@ -50296,7 +50644,7 @@ async function ensureHostMountSources(config) {
50296
50644
  }
50297
50645
  try {
50298
50646
  const uid = allocateAgentUid(name);
50299
- chownSync7(tokenPath, uid, uid);
50647
+ chownSync8(tokenPath, uid, uid);
50300
50648
  } catch {}
50301
50649
  }
50302
50650
  const fleetDir = join69(home2, ".switchroom", "fleet");
@@ -50316,7 +50664,7 @@ async function ensureHostMountSources(config) {
50316
50664
  }
50317
50665
  function detectComposeV2() {
50318
50666
  try {
50319
- const out = execFileSync20("docker", ["compose", "version"], {
50667
+ const out = execFileSync21("docker", ["compose", "version"], {
50320
50668
  stdio: ["ignore", "pipe", "pipe"],
50321
50669
  encoding: "utf8"
50322
50670
  });
@@ -51104,7 +51452,7 @@ var init_apply = __esm(() => {
51104
51452
  });
51105
51453
 
51106
51454
  // src/agents/drift.ts
51107
- import { execFileSync as execFileSync21 } from "node:child_process";
51455
+ import { execFileSync as execFileSync22 } from "node:child_process";
51108
51456
  import { createHash as createHash15 } from "node:crypto";
51109
51457
  import {
51110
51458
  existsSync as existsSync71,
@@ -51439,7 +51787,7 @@ function detectAgentDrift(name, agentConfigRaw, agentsDir, config, configPath, o
51439
51787
  }
51440
51788
  return findings;
51441
51789
  }
51442
- var BUNDLED_POOL_SEGMENT = "/.switchroom/skills/_bundled/", defaultExec = (cmd, args) => execFileSync21(cmd, args, {
51790
+ var BUNDLED_POOL_SEGMENT = "/.switchroom/skills/_bundled/", defaultExec = (cmd, args) => execFileSync22(cmd, args, {
51443
51791
  encoding: "utf-8",
51444
51792
  stdio: ["ignore", "pipe", "ignore"],
51445
51793
  timeout: 1e4
@@ -52421,7 +52769,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
52421
52769
  var init_doctor_credentials_migration = () => {};
52422
52770
 
52423
52771
  // src/cli/doctor-agent-dotfile-ownership.ts
52424
- import { existsSync as existsSync73, lstatSync as lstatSync10, statSync as statSync41 } from "node:fs";
52772
+ import { existsSync as existsSync73, lstatSync as lstatSync10, statSync as statSync42 } from "node:fs";
52425
52773
  import { join as join75 } from "node:path";
52426
52774
  function agentDotfileCandidates(agentDir) {
52427
52775
  return [
@@ -52438,7 +52786,7 @@ function runAgentDotfileOwnershipChecks(config, deps = {}) {
52438
52786
  if (agents.length === 0)
52439
52787
  return results;
52440
52788
  const exists = deps.existsSync ?? existsSync73;
52441
- const scanFs = deps.scanFs ?? { lstatSync: lstatSync10, statSync: statSync41 };
52789
+ const scanFs = deps.scanFs ?? { lstatSync: lstatSync10, statSync: statSync42 };
52442
52790
  let agentsDir = deps.agentsDir;
52443
52791
  if (agentsDir === undefined) {
52444
52792
  try {
@@ -52583,7 +52931,7 @@ var init_doctor_inlined_secrets = __esm(() => {
52583
52931
  });
52584
52932
 
52585
52933
  // src/cli/doctor-approval-attribution.ts
52586
- import { execFileSync as execFileSync22 } from "node:child_process";
52934
+ import { execFileSync as execFileSync23 } from "node:child_process";
52587
52935
  import { readFileSync as fsReadFileSync2 } from "node:fs";
52588
52936
  import { join as join76 } from "node:path";
52589
52937
  function parseApproverSet(canonical) {
@@ -52685,7 +53033,7 @@ async function defaultLoadDecisions() {
52685
53033
  } catch {}
52686
53034
  try {
52687
53035
  const SEP2 = String.fromCharCode(31);
52688
- const stdout = execFileSync22("docker", [
53036
+ const stdout = execFileSync23("docker", [
52689
53037
  "exec",
52690
53038
  "switchroom-vault-broker",
52691
53039
  "sqlite3",
@@ -52957,8 +53305,8 @@ var init_doctor_agent_smoke = __esm(() => {
52957
53305
  });
52958
53306
 
52959
53307
  // src/cli/doctor-vault-broker-durability.ts
52960
- import { execFileSync as execFileSync23 } from "node:child_process";
52961
- import { existsSync as existsSync75, statSync as statSync42, readFileSync as readFileSync70 } from "node:fs";
53308
+ import { execFileSync as execFileSync24 } from "node:child_process";
53309
+ import { existsSync as existsSync75, statSync as statSync43, readFileSync as readFileSync70 } from "node:fs";
52962
53310
  import { homedir as homedir43 } from "node:os";
52963
53311
  import { join as join79 } from "node:path";
52964
53312
  import { createRequire as createRequire2 } from "node:module";
@@ -52986,7 +53334,7 @@ function defaultStatHost(p) {
52986
53334
  if (!existsSync75(p))
52987
53335
  return null;
52988
53336
  try {
52989
- const s = statSync42(p, { bigint: true });
53337
+ const s = statSync43(p, { bigint: true });
52990
53338
  return { ino: s.ino, size: Number(s.size) };
52991
53339
  } catch {
52992
53340
  return null;
@@ -53020,7 +53368,7 @@ function spawnDockerStat(p) {
53020
53368
  }
53021
53369
  function spawnDockerStatForContainer(containerName2, p) {
53022
53370
  try {
53023
- const stdout = execFileSync23("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53371
+ const stdout = execFileSync24("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53024
53372
  return { status: 0, stdout, stderr: "", error: null };
53025
53373
  } catch (err) {
53026
53374
  const e = err;
@@ -53094,7 +53442,7 @@ function probeBrokerUnlocked(opts) {
53094
53442
  }
53095
53443
  function defaultBrokerStatusProbe() {
53096
53444
  try {
53097
- const out = execFileSync23("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53445
+ const out = execFileSync24("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53098
53446
  const parsed = JSON.parse(out.trim());
53099
53447
  if (!parsed.running)
53100
53448
  return null;
@@ -53195,7 +53543,7 @@ function defaultKernelStatBroker(p) {
53195
53543
  }
53196
53544
  function defaultWalStatBroker(p) {
53197
53545
  try {
53198
- const stdout = execFileSync23("docker", ["exec", "switchroom-vault-broker", "stat", "-c", "%Y %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53546
+ const stdout = execFileSync24("docker", ["exec", "switchroom-vault-broker", "stat", "-c", "%Y %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
53199
53547
  const [mStr, sStr] = stdout.trim().split(/\s+/);
53200
53548
  const mtime = Number(mStr);
53201
53549
  const size = Number(sStr);
@@ -53324,7 +53672,7 @@ function probeOrphanVaultTokens(config, opts) {
53324
53672
  }
53325
53673
  function defaultTokenMtimeMs(agent, home2) {
53326
53674
  try {
53327
- return statSync42(join79(home2, ".switchroom", "agents", agent, ".vault-token")).mtimeMs;
53675
+ return statSync43(join79(home2, ".switchroom", "agents", agent, ".vault-token")).mtimeMs;
53328
53676
  } catch {
53329
53677
  return null;
53330
53678
  }
@@ -53372,7 +53720,7 @@ function probeAutoUnlockBlob(home2) {
53372
53720
  fix: "Run `switchroom vault broker enable-auto-unlock` to seal the blob with the current passphrase + machine-id"
53373
53721
  };
53374
53722
  }
53375
- const sz = statSync42(blobPath).size;
53723
+ const sz = statSync43(blobPath).size;
53376
53724
  if (sz === 0) {
53377
53725
  return {
53378
53726
  name: "vault-broker: auto-unlock blob",
@@ -53728,6 +54076,7 @@ __export(exports_doctor, {
53728
54076
  checkMffAuthFlow: () => checkMffAuthFlow,
53729
54077
  checkMffApiReachable: () => checkMffApiReachable,
53730
54078
  checkMff: () => checkMff,
54079
+ checkMemoryKeysAreKnown: () => checkMemoryKeysAreKnown,
53731
54080
  checkManifestDrift: () => checkManifestDrift,
53732
54081
  checkLegacyState: () => checkLegacyState,
53733
54082
  checkLeakedHomeSwitchroom: () => checkLeakedHomeSwitchroom,
@@ -53739,6 +54088,7 @@ __export(exports_doctor, {
53739
54088
  checkConfig: () => checkConfig,
53740
54089
  checkBankObservationsMissions: () => checkBankObservationsMissions,
53741
54090
  checkBankIngestHealth: () => checkBankIngestHealth,
54091
+ checkBankConfigApiReachable: () => checkBankConfigApiReachable,
53742
54092
  checkAgents: () => checkAgents,
53743
54093
  buildPendingRetainsProbeScript: () => buildPendingRetainsProbeScript,
53744
54094
  PENDING_RETAINS_EVICTION_WINDOW_DAYS: () => PENDING_RETAINS_EVICTION_WINDOW_DAYS,
@@ -53754,7 +54104,7 @@ import {
53754
54104
  mkdirSync as mkdirSync42,
53755
54105
  readFileSync as readFileSync72,
53756
54106
  readdirSync as readdirSync26,
53757
- statSync as statSync43
54107
+ statSync as statSync44
53758
54108
  } from "node:fs";
53759
54109
  import { dirname as dirname31, join as join82, resolve as resolve44 } from "node:path";
53760
54110
  import { createPublicKey, createPrivateKey } from "node:crypto";
@@ -53781,7 +54131,7 @@ function findInNvm(bin) {
53781
54131
  for (const v of versions) {
53782
54132
  const candidate = join82(nvmRoot, v, "bin", bin);
53783
54133
  try {
53784
- const s = statSync43(candidate);
54134
+ const s = statSync44(candidate);
53785
54135
  if (s.isFile() || s.isSymbolicLink()) {
53786
54136
  return candidate;
53787
54137
  }
@@ -54074,8 +54424,68 @@ function checkConfig(config, configPath) {
54074
54424
  detail: effortRisks.length > 0 ? `${effortRisks.length} agent(s) on pinned Opus 4.x with thinking_effort > low: ${effortRisks.join(", ")}` : "no risky model/effort combos",
54075
54425
  fix: effortRisks.length > 0 ? "Pin `thinking_effort: low` for pinned Opus 4.x agents, or move them to a current Opus model. medium+ could 400 on 'thinking blocks cannot be modified' with concurrent sub-agents (issue #1978); the upstream fix shipped in claude-code " + `${CLAUDE_CLI_THINKING_MERGE_FIX_VERSION} but the Opus 4.x reproduction was never re-tested. Removing the field is NOT a fix (Opus defaults effort=high when unset).` : undefined
54076
54426
  });
54427
+ results.push(checkMemoryKeysAreKnown(configPath));
54077
54428
  return results;
54078
54429
  }
54430
+ function checkMemoryKeysAreKnown(configPath) {
54431
+ const name = "memory config keys";
54432
+ let rawText;
54433
+ try {
54434
+ rawText = readFileSync72(configPath, "utf-8");
54435
+ } catch {
54436
+ return { name, status: "ok", detail: "config unreadable \u2014 skipped (see load row)" };
54437
+ }
54438
+ let parsed;
54439
+ try {
54440
+ parsed = import_yaml19.parse(rawText);
54441
+ } catch {
54442
+ return { name, status: "ok", detail: "config unparseable \u2014 skipped (see load row)" };
54443
+ }
54444
+ if (typeof parsed !== "object" || parsed === null) {
54445
+ return { name, status: "ok", detail: "no memory config to check" };
54446
+ }
54447
+ const root = parsed;
54448
+ const sources = [];
54449
+ const asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
54450
+ const defaults = asObj(root.defaults);
54451
+ if (defaults?.memory !== undefined)
54452
+ sources.push({ label: "defaults", memory: defaults.memory, tier: "mirror" });
54453
+ const profiles = asObj(root.profiles);
54454
+ for (const [pname, pval] of Object.entries(profiles ?? {})) {
54455
+ const p = asObj(pval);
54456
+ if (p?.memory !== undefined)
54457
+ sources.push({ label: `profile ${pname}`, memory: p.memory, tier: "mirror" });
54458
+ }
54459
+ const agents = asObj(root.agents);
54460
+ for (const [aname, aval] of Object.entries(agents ?? {})) {
54461
+ const a = asObj(aval);
54462
+ if (a?.memory !== undefined)
54463
+ sources.push({ label: aname, memory: a.memory, tier: "agent" });
54464
+ }
54465
+ const lines = [];
54466
+ let retiredCount = 0;
54467
+ for (const { label, memory, tier } of sources) {
54468
+ for (const finding of findUnknownMemoryKeys(memory, tier)) {
54469
+ if (finding.replacement) {
54470
+ retiredCount++;
54471
+ lines.push(`${label}: \`${finding.scope}.${finding.key}\` \u2014 ${finding.replacement}`);
54472
+ } else {
54473
+ lines.push(`${label}: \`${finding.scope}.${finding.key}\` is not a recognized key \u2014 silently ignored`);
54474
+ }
54475
+ }
54476
+ }
54477
+ if (lines.length === 0) {
54478
+ return { name, status: "ok", detail: "all `memory` keys are recognized" };
54479
+ }
54480
+ return {
54481
+ name,
54482
+ status: "fail",
54483
+ detail: `${lines.length} unrecognized \`memory\` key(s)` + (retiredCount > 0 ? ` (${retiredCount} removed knob(s))` : "") + ` \u2014 silently stripped at parse time, so the line reads as set but does nothing:
54484
+ ` + lines.join(`
54485
+ `),
54486
+ fix: "Remove or fix the key(s). A removed knob is gone for good \u2014 delete the " + "line; a typo must match a declared key. The recognized keys live on " + "`AgentMemorySchema` (and its `recall`/`retain`) in src/config/schema.ts."
54487
+ };
54488
+ }
54079
54489
  function checkUserDeclaredMcps(name, agentConfig, config, renderedMcpServers) {
54080
54490
  const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
54081
54491
  const declaredMcp = resolved.mcp_servers ?? {};
@@ -54377,7 +54787,7 @@ function checkHostCapabilitiesReadable(config, read = readHostCapabilities()) {
54377
54787
  const proven = voice?.gpuPresent === true && voice?.containerToolkit === true;
54378
54788
  let mode;
54379
54789
  try {
54380
- mode = statSync43(read.path).mode & 511;
54790
+ mode = statSync44(read.path).mode & 511;
54381
54791
  } catch {
54382
54792
  mode = undefined;
54383
54793
  }
@@ -54467,7 +54877,8 @@ async function checkBankObservationsMissions(config, url, opts) {
54467
54877
  detail: `could not read observations_mission: ${read.reason}`
54468
54878
  };
54469
54879
  }
54470
- const profileDefault = PROFILE_MEMORY_DEFAULTS[entry.config.extends ?? DEFAULT_PROFILE]?.observations_mission;
54880
+ const memoryProfile = resolveMemoryProfile(entry.config);
54881
+ const profileDefault = PROFILE_MEMORY_DEFAULTS[memoryProfile]?.observations_mission;
54471
54882
  const decision = decideObservationsMissionUpgrade(entry.config.memory?.observations_mission, profileDefault, read.mission);
54472
54883
  if (decision.action === "upgrade") {
54473
54884
  return {
@@ -54489,10 +54900,22 @@ async function checkBankObservationsMissions(config, url, opts) {
54489
54900
  return {
54490
54901
  name: label,
54491
54902
  status: "ok",
54492
- detail: isSwitchroomDefault ? profileDefault != null ? `switchroom ${entry.config.extends ?? DEFAULT_PROFILE}-profile default` : "switchroom fleet default" : "operator-authored (left untouched by the never-clobber rule)"
54903
+ detail: isSwitchroomDefault ? profileDefault != null ? `switchroom ${memoryProfile}-profile default` : "switchroom fleet default" : "operator-authored (left untouched by the never-clobber rule)"
54493
54904
  };
54494
54905
  });
54495
54906
  }
54907
+ async function checkBankConfigApiReachable(config, url, opts) {
54908
+ let bankId;
54909
+ for (const [agentName, agentConfig] of Object.entries(config.agents)) {
54910
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
54911
+ bankId = resolved.memory?.collection ?? agentName;
54912
+ break;
54913
+ }
54914
+ if (bankId === undefined)
54915
+ return [];
54916
+ const read = await fetchBankRetainMission(url, bankId, { fetchImpl: opts?.fetchImpl });
54917
+ return [classifyBankConfigReachability(read, bankId)];
54918
+ }
54496
54919
  async function checkBankIngestHealth(config, url, opts) {
54497
54920
  const results = [];
54498
54921
  const now = opts?.now ?? new Date;
@@ -54507,18 +54930,22 @@ async function checkBankIngestHealth(config, url, opts) {
54507
54930
  banks.set(bank, []);
54508
54931
  }
54509
54932
  const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
54510
- let backlogRow;
54933
+ const backlogRows = [];
54511
54934
  if (opts?.includeConsolidationBacklog) {
54512
- let totalPending = 0;
54935
+ let totalPendingOps = 0;
54936
+ let totalPendingConsolidation = 0;
54513
54937
  let anyBankReachable = false;
54514
54938
  for (const [, , h] of inspected) {
54515
54939
  if (h.ok) {
54516
54940
  anyBankReachable = true;
54517
- totalPending += h.pendingOperations;
54941
+ totalPendingOps += h.pendingOperations;
54942
+ totalPendingConsolidation += h.pendingConsolidation;
54518
54943
  }
54519
54944
  }
54520
- if (anyBankReachable)
54521
- backlogRow = classifyConsolidationBacklog(totalPending);
54945
+ if (anyBankReachable) {
54946
+ backlogRows.push(classifyAsyncOpQueue(totalPendingOps));
54947
+ backlogRows.push(classifyConsolidationBacklog(totalPendingConsolidation));
54948
+ }
54522
54949
  }
54523
54950
  for (const [bankId, agents, h] of inspected) {
54524
54951
  const label = `bank ${bankId}` + (profileBanks.has(bankId) ? " (profile)" : agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
@@ -54584,8 +55011,7 @@ async function checkBankIngestHealth(config, url, opts) {
54584
55011
  }
54585
55012
  results.push({ name: label, status: "ok", detail: summary });
54586
55013
  }
54587
- if (backlogRow)
54588
- results.push(backlogRow);
55014
+ results.push(...backlogRows);
54589
55015
  return results;
54590
55016
  }
54591
55017
  function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
@@ -54653,6 +55079,7 @@ async function checkHindsight(config) {
54653
55079
  results.push(await checkHindsightHealthEndpoint(url));
54654
55080
  results.push(...await checkBankIngestHealth(config, url, { includeConsolidationBacklog: true }));
54655
55081
  results.push(...await checkBankObservationsMissions(config, url));
55082
+ results.push(...await checkBankConfigApiReachable(config, url));
54656
55083
  results.push(await checkObservationScopeSaturation(config, url));
54657
55084
  const memoryAgentsDir = resolveAgentsDir(config);
54658
55085
  for (const agentName of Object.keys(config.agents)) {
@@ -54690,7 +55117,7 @@ function probePendingRetainsQueue(agentName) {
54690
55117
  function buildPendingRetainsProbeScript(base, now = Date.now()) {
54691
55118
  const dir = `${base}/pending-retains`;
54692
55119
  const cutoff = new Date(now - PENDING_RETAINS_EVICTION_WINDOW_DAYS * 86400000).toISOString().replace(/\.\d+Z$/, "Z");
54693
- return `P=0; D=0; X=0; E=0; C=\${HINDSIGHT_PENDING_MAX_ENTRIES:-${PENDING_RETAINS_DEFAULT_MAX_ENTRIES}}; ` + `if [ -d '${dir}' ]; then ` + `P=$(ls -1 '${dir}' 2>/dev/null | grep -c '\\.json$' || true); ` + `D=$(ls -1 '${dir}' 2>/dev/null | grep -c '\\.json\\.dead$' || true); ` + `fi; ` + `if [ -d '${base}/pending-dead' ]; then ` + `D=$((D + $(ls -1 '${base}/pending-dead' 2>/dev/null | grep -c '\\.dead$' || true))); ` + `fi; ` + `for L in '${base}/pending-drops.json' '${dir}/.drops.json'; do ` + `if [ "$X" = 0 ] && [ -f "$L" ]; then ` + `X=$(grep -o '"count"[^0-9]*[0-9][0-9]*' "$L" 2>/dev/null ` + `| grep -o '[0-9][0-9]*$' | head -n1); ` + `[ -n "$X" ] || X=0; ` + `fi; done; ` + `if [ -f '${base}/pending-evictions.log' ]; then ` + `E=$(awk -v c='${cutoff}' '$1 >= c && /evicted=/ {n++} END {print n+0}' ` + `'${base}/pending-evictions.log' 2>/dev/null || echo 0); ` + `[ -n "$E" ] || E=0; ` + `fi; ` + `echo "P=$P D=$D X=$X E=$E C=$C"`;
55120
+ return `P=0; D=0; X=0; E=0; Q=0; C=\${HINDSIGHT_PENDING_MAX_ENTRIES:-${PENDING_RETAINS_DEFAULT_MAX_ENTRIES}}; ` + `if [ -d '${dir}' ]; then ` + `P=$(ls -1 '${dir}' 2>/dev/null | grep -c '\\.json$' || true); ` + `D=$(ls -1 '${dir}' 2>/dev/null | grep -c '\\.json\\.dead$' || true); ` + `fi; ` + `if [ -d '${base}/pending-dead' ]; then ` + `D=$((D + $(ls -1 '${base}/pending-dead' 2>/dev/null | grep -c '\\.dead$' || true))); ` + `fi; ` + `for L in '${base}/pending-drops.json' '${dir}/.drops.json'; do ` + `if [ "$X" = 0 ] && [ -f "$L" ]; then ` + `X=$(grep -o '"count"[^0-9]*[0-9][0-9]*' "$L" 2>/dev/null ` + `| grep -o '[0-9][0-9]*$' | head -n1); ` + `[ -n "$X" ] || X=0; ` + `fi; done; ` + `if [ -f '${base}/pending-evictions.log' ]; then ` + `E=$(awk -v c='${cutoff}' '$1 >= c && /evicted=/ {n++} END {print n+0}' ` + `'${base}/pending-evictions.log' 2>/dev/null || echo 0); ` + `[ -n "$E" ] || E=0; ` + `fi; ` + `if [ -d '${base}/pending-duplicate' ]; then ` + `Q=$(ls -1 '${base}/pending-duplicate' 2>/dev/null | grep -c '\\.json$' || true); ` + `fi; ` + `echo "P=$P D=$D X=$X E=$E Q=$Q C=$C"`;
54694
55121
  }
54695
55122
  function parsePendingRetainsProbeOutput(r) {
54696
55123
  const unreachable = {
@@ -54699,6 +55126,7 @@ function parsePendingRetainsProbeOutput(r) {
54699
55126
  dead: 0,
54700
55127
  dropped: 0,
54701
55128
  evicted: 0,
55129
+ duplicates: 0,
54702
55130
  cap: PENDING_RETAINS_DEFAULT_MAX_ENTRIES
54703
55131
  };
54704
55132
  if (r.error || r.status !== 0)
@@ -54712,11 +55140,13 @@ function parsePendingRetainsProbeOutput(r) {
54712
55140
  const dead = parseInt(dm[1], 10);
54713
55141
  const xm = out.match(/X=(\d+)/);
54714
55142
  const em = out.match(/E=(\d+)/);
55143
+ const qm = out.match(/Q=(\d+)/);
54715
55144
  const cm = out.match(/C=(\d+)/);
54716
55145
  const dropped = xm ? parseInt(xm[1], 10) : 0;
54717
55146
  const evicted = em ? parseInt(em[1], 10) : 0;
55147
+ const duplicates = qm ? parseInt(qm[1], 10) : 0;
54718
55148
  const cap = cm && parseInt(cm[1], 10) > 0 ? parseInt(cm[1], 10) : PENDING_RETAINS_DEFAULT_MAX_ENTRIES;
54719
- const common = { pending, dead, dropped, evicted, cap };
55149
+ const common = { pending, dead, dropped, evicted, duplicates, cap };
54720
55150
  if (dead > 0)
54721
55151
  return { state: "dead", ...common };
54722
55152
  if (pending > 0)
@@ -54748,12 +55178,15 @@ function checkPendingRetainsQueues(config, opts) {
54748
55178
  const unreachable = [];
54749
55179
  const dropped = [];
54750
55180
  const evicted = [];
55181
+ const duplicated = [];
54751
55182
  let okCount = 0;
54752
55183
  for (const [a, r] of results) {
54753
55184
  if (r.dropped > 0)
54754
55185
  dropped.push(`${a} (${r.dropped})`);
54755
55186
  if (r.evicted > 0)
54756
55187
  evicted.push(`${a} (${r.evicted})`);
55188
+ if (r.duplicates > 0)
55189
+ duplicated.push(`${a} (${r.duplicates})`);
54757
55190
  if (r.state === "dead") {
54758
55191
  dead.push(`${a} (${r.dead} dead${r.pending > 0 ? `, ${r.pending} queued` : ""})`);
54759
55192
  } else if (r.state === "backlog") {
@@ -54768,6 +55201,7 @@ function checkPendingRetainsQueues(config, opts) {
54768
55201
  }
54769
55202
  }
54770
55203
  const skippedNote = unreachable.length > 0 ? `; skipped (unreachable): ${unreachable.join(", ")}` : "";
55204
+ const dupNote = duplicated.length > 0 ? `; collapsed duplicates (not loss): ${duplicated.join(", ")}` : "";
54771
55205
  const backlogFix = `Clear a backlog explicitly (the SessionStart drain cannot \u2014 its per-entry ` + `timeout is clamped to the hook budget, far below a real retain): ` + `\`docker exec switchroom-<agent> python3 ` + `/state/agent/.claude/plugins/hindsight-memory/scripts/drain_pending.py --backlog\`. ` + `Run \`--phase reconcile\` first \u2014 it costs nothing and typically clears most of ` + `the queue by confirming those documents already exist. PACE THE REST: the retain ` + `model pool is small and shared with live traffic, so drain ONE agent at a time at ` + `the default HINDSIGHT_DRAIN_CONCURRENCY=1, and keep HINDSIGHT_DRAIN_SLEEP_S set. ` + `Point HINDSIGHT_DRAIN_P95_CMD at the same figure the latency watchdog alarms on so ` + `the replay backs off instead of causing the alarm \u2014 on a LiteLLM deployment that is ` + `\`docker exec -i <postgres> psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c ` + `"select coalesce((select round(percentile_cont(0.95) within group (order by ` + `request_duration_ms)) from \\"LiteLLM_SpendLogs\\" where \\"startTime\\" > now() ` + `- interval '10 minutes' and metadata->>'user_api_key_alias' like 'hindsight-%' and ` + `status='success' and request_duration_ms is not null), -1)"\` (must print a ` + `millisecond integer on stdout; unset = no backoff). ` + `SAFE ALONGSIDE THE \`hindsight-drain\` SIDECAR and a session booting underneath ` + `you: drain_pending.py takes an exclusive lock on ` + `\`$HOME/.hindsight/drain-pending.lock\` for the whole run, so whichever drain ` + `starts second prints "another drain holds ..." and exits without touching the ` + `queue. Do NOT wrap this in \`flock\` on that path yourself \u2014 you would hold the ` + `lock the script then asks for, and it would skip every time. Entries past the ` + `attempt ceiling (HINDSIGHT_DRAIN_ATTEMPT_CEILING, default 20) are PARKED and ` + `reported rather than retried forever; add \`--force\` once the upstream is fixed ` + `to replay them. ` + `Fix the upstream first (bank ` + `health rows above), or every retry just ages entries toward .dead. Entries the ` + `drain retires are MOVED to \`.hindsight/pending-reconciled/\` (bounded, so they ` + `expire), never deleted \u2014 every retire rests on an HTTP 200, which is an ack and ` + `not proof, so the payload stays recoverable. If that archive cannot be written ` + `(disk full, permissions) the entry STAYS QUEUED and the drain says so on stderr; ` + `a failure to archive is never a reason to delete. FIX THE DISK ANYWAY \u2014 "stays ` + `queued" is bounded: entries pile up, the queue hits ` + `HINDSIGHT_PENDING_MAX_ENTRIES/MAX_BYTES, and enqueue then sheds the OLDEST ` + `entries to keep accepting the newest. While the disk is full their archive move ` + `fails too, so those oldest turns are removed outright (the ledger line reads ` + `\`reason=...+archive-failed\`). Under sustained ENOSPC "keep it queued" degrades ` + `to "keep the newest, drop the oldest".`;
54772
55206
  if (dead.length > 0 || dropped.length > 0 || evicted.length > 0) {
54773
55207
  const parts = [];
@@ -54798,7 +55232,7 @@ function checkPendingRetainsQueues(config, opts) {
54798
55232
  return {
54799
55233
  name,
54800
55234
  status: "fail",
54801
- detail: `${parts.join("; ")}${skippedNote}`,
55235
+ detail: `${parts.join("; ")}${skippedNote}${dupNote}`,
54802
55236
  fix: fixParts.join(" ")
54803
55237
  };
54804
55238
  }
@@ -54812,14 +55246,14 @@ function checkPendingRetainsQueues(config, opts) {
54812
55246
  return {
54813
55247
  name,
54814
55248
  status: "warn",
54815
- detail: `${parts.join("; ")}${skippedNote}`,
55249
+ detail: `${parts.join("; ")}${skippedNote}${dupNote}`,
54816
55250
  fix: backlogFix
54817
55251
  };
54818
55252
  }
54819
55253
  return {
54820
55254
  name,
54821
55255
  status: "ok",
54822
- detail: `${okCount}/${agentNames.length} agents: empty (no failed retains)${skippedNote}`
55256
+ detail: `${okCount}/${agentNames.length} agents: empty (no failed retains)${skippedNote}${dupNote}`
54823
55257
  };
54824
55258
  }
54825
55259
  function classifyReadError(err) {
@@ -55949,8 +56383,8 @@ function registerDoctorCommand(program3) {
55949
56383
  results: await runContextChecks(config, {
55950
56384
  readSnapshot: async (agent) => {
55951
56385
  try {
55952
- const { execFileSync: execFileSync24 } = await import("node:child_process");
55953
- const out = execFileSync24("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/context-occupancy.json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
56386
+ const { execFileSync: execFileSync25 } = await import("node:child_process");
56387
+ const out = execFileSync25("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/context-occupancy.json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
55954
56388
  return { kind: "ok", snapshot: JSON.parse(out) };
55955
56389
  } catch (err) {
55956
56390
  const msg = err.message ?? String(err);
@@ -56051,7 +56485,7 @@ function registerDoctorCommand(program3) {
56051
56485
  }
56052
56486
  }));
56053
56487
  }
56054
- var DEFAULT_DEPLOY_MOUNTS_DEPS, PENDING_RETAINS_DEFAULT_MAX_ENTRIES = 2000, PENDING_RETAINS_EVICTION_WINDOW_DAYS = 7, MFF_VAULT_KEY = "mff/agent-private-key", MANIFEST_WARN_ONLY;
56488
+ var import_yaml19, DEFAULT_DEPLOY_MOUNTS_DEPS, PENDING_RETAINS_DEFAULT_MAX_ENTRIES = 2000, PENDING_RETAINS_EVICTION_WINDOW_DAYS = 7, MFF_VAULT_KEY = "mff/agent-private-key", MANIFEST_WARN_ONLY;
56055
56489
  var init_doctor = __esm(() => {
56056
56490
  init_source();
56057
56491
  init_doctor_status();
@@ -56072,6 +56506,7 @@ var init_doctor = __esm(() => {
56072
56506
  init_hindsight();
56073
56507
  init_host_capabilities();
56074
56508
  init_hindsight_perf_defaults();
56509
+ init_memory_key_drift();
56075
56510
  init_doctor_memory();
56076
56511
  init_doctor_recall_health();
56077
56512
  init_doctor_hnsw_index();
@@ -56103,6 +56538,7 @@ var init_doctor = __esm(() => {
56103
56538
  init_doctor_timezone();
56104
56539
  init_doctor_fix_session_model();
56105
56540
  init_doctor_claude_cli();
56541
+ import_yaml19 = __toESM(require_dist(), 1);
56106
56542
  DEFAULT_DEPLOY_MOUNTS_DEPS = { pathKind: defaultPathKind };
56107
56543
  MANIFEST_WARN_ONLY = new Set([
56108
56544
  "@playwright/mcp",
@@ -56197,7 +56633,7 @@ __export(exports_util, {
56197
56633
  jsonStringifyReplacer: () => jsonStringifyReplacer,
56198
56634
  joinValues: () => joinValues,
56199
56635
  issue: () => issue,
56200
- isPlainObject: () => isPlainObject2,
56636
+ isPlainObject: () => isPlainObject3,
56201
56637
  isObject: () => isObject2,
56202
56638
  getSizableOrigin: () => getSizableOrigin,
56203
56639
  getParsedType: () => getParsedType2,
@@ -56338,7 +56774,7 @@ function esc(str) {
56338
56774
  function isObject2(data) {
56339
56775
  return typeof data === "object" && data !== null && !Array.isArray(data);
56340
56776
  }
56341
- function isPlainObject2(o) {
56777
+ function isPlainObject3(o) {
56342
56778
  if (isObject2(o) === false)
56343
56779
  return false;
56344
56780
  const ctor = o.constructor;
@@ -56466,7 +56902,7 @@ function omit(schema, mask) {
56466
56902
  });
56467
56903
  }
56468
56904
  function extend(schema, shape) {
56469
- if (!isPlainObject2(shape)) {
56905
+ if (!isPlainObject3(shape)) {
56470
56906
  throw new Error("Invalid input to extend: expected a plain object");
56471
56907
  }
56472
56908
  const def = {
@@ -57408,7 +57844,7 @@ function mergeValues2(a, b) {
57408
57844
  if (a instanceof Date && b instanceof Date && +a === +b) {
57409
57845
  return { valid: true, data: a };
57410
57846
  }
57411
- if (isPlainObject2(a) && isPlainObject2(b)) {
57847
+ if (isPlainObject3(a) && isPlainObject3(b)) {
57412
57848
  const bKeys = Object.keys(b);
57413
57849
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
57414
57850
  const newObj = { ...a, ...b };
@@ -58253,7 +58689,7 @@ var init_schemas = __esm(() => {
58253
58689
  $ZodType.init(inst, def);
58254
58690
  inst._zod.parse = (payload, ctx) => {
58255
58691
  const input = payload.value;
58256
- if (!isPlainObject2(input)) {
58692
+ if (!isPlainObject3(input)) {
58257
58693
  payload.issues.push({
58258
58694
  expected: "record",
58259
58695
  code: "invalid_type",
@@ -61936,7 +62372,7 @@ class Protocol {
61936
62372
  };
61937
62373
  }
61938
62374
  }
61939
- function isPlainObject3(value) {
62375
+ function isPlainObject4(value) {
61940
62376
  return value !== null && typeof value === "object" && !Array.isArray(value);
61941
62377
  }
61942
62378
  function mergeCapabilities(base, additional) {
@@ -61947,7 +62383,7 @@ function mergeCapabilities(base, additional) {
61947
62383
  if (addValue === undefined)
61948
62384
  continue;
61949
62385
  const baseValue = result[k];
61950
- if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
62386
+ if (isPlainObject4(baseValue) && isPlainObject4(addValue)) {
61951
62387
  result[k] = { ...baseValue, ...addValue };
61952
62388
  } else {
61953
62389
  result[k] = addValue;
@@ -70661,14 +71097,14 @@ function detectHeaderMisconfig(parsed) {
70661
71097
  }
70662
71098
  function parseLitellmConfig(text) {
70663
71099
  try {
70664
- return import_yaml26.parse(text);
71100
+ return import_yaml27.parse(text);
70665
71101
  } catch {
70666
71102
  return null;
70667
71103
  }
70668
71104
  }
70669
- var import_yaml26, FORWARD_HEADERS_FLAG = "forward_client_headers_to_llm_api", COOLIFY_SERVICES_DIR = "/data/coolify/services", LIVE_LITELLM_CONFIG_BASENAME = "litellm-config.yaml", BARE_ANTHROPIC_FAMILY_ALIASES;
71105
+ var import_yaml27, FORWARD_HEADERS_FLAG = "forward_client_headers_to_llm_api", COOLIFY_SERVICES_DIR = "/data/coolify/services", LIVE_LITELLM_CONFIG_BASENAME = "litellm-config.yaml", BARE_ANTHROPIC_FAMILY_ALIASES;
70670
71106
  var init_header_passthrough_guard = __esm(() => {
70671
- import_yaml26 = __toESM(require_dist(), 1);
71107
+ import_yaml27 = __toESM(require_dist(), 1);
70672
71108
  BARE_ANTHROPIC_FAMILY_ALIASES = new Set([
70673
71109
  "opus",
70674
71110
  "sonnet",
@@ -73897,6 +74333,9 @@ function summarizeReconcileBatch(totalCount, failures) {
73897
74333
  lines: failures.map((f) => `${f.name}: ${f.error}`)
73898
74334
  };
73899
74335
  }
74336
+ function shouldAutoRestartAfterReconcile(opts, restartRequiredCount) {
74337
+ return opts.restart !== false && restartRequiredCount > 0;
74338
+ }
73900
74339
  function resolveLogsTail(lines) {
73901
74340
  const DEFAULT_TAIL = 50;
73902
74341
  const MAX_TAIL = 1000;
@@ -74745,7 +75184,7 @@ Scaffolding agent: ${name}
74745
75184
  }
74746
75185
  }
74747
75186
  const needsRestart = restartRequired.length > 0 || staleTillRestart.length > 0;
74748
- const autoRestart = restartRequired.length > 0 && !opts.noRestart;
75187
+ const autoRestart = shouldAutoRestartAfterReconcile(opts, restartRequired.length);
74749
75188
  if (!needsRestart && hot.length > 0) {
74750
75189
  console.log(source_default.green(`
74751
75190
  (no restart needed, changes active next turn)`));
@@ -74765,7 +75204,7 @@ Restart: switchroom agent restart ${n}`));
74765
75204
  }
74766
75205
  }
74767
75206
  const changesBySemantics = result.changesBySemantics;
74768
- const autoRestartNeeded = !opts.noRestart && changesBySemantics !== undefined && changesBySemantics.restartRequired.length > 0;
75207
+ const autoRestartNeeded = changesBySemantics !== undefined && shouldAutoRestartAfterReconcile(opts, changesBySemantics.restartRequired.length);
74769
75208
  const shouldRestart = (opts.restart || opts.gracefulRestart || autoRestartNeeded) && allChanges.length > 0;
74770
75209
  if (shouldRestart) {
74771
75210
  try {
@@ -85897,14 +86336,21 @@ ${name}:`)];
85897
86336
  const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
85898
86337
  const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
85899
86338
  const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
85900
- lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "")));
86339
+ const collapseTurns = entries.filter((e) => typeof e.injected_own_bank_count === "number" && e.injected_own_bank_count === 0 && typeof e.injected_additional_bank_count === "number" && e.injected_additional_bank_count > 0).length;
86340
+ lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "") + (collapseTurns > 0 ? source_default.red(` own_bank_collapse_turns=${collapseTurns}`) : "")));
85901
86341
  for (const e of entries) {
85902
86342
  const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
85903
86343
  const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
85904
86344
  const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
85905
86345
  const ids = e.memory_ids && e.memory_ids.length > 0 ? source_default.dim(` ids=${e.memory_ids.slice(0, 3).join(",")}${e.memory_ids.length > 3 ? `\u2026+${e.memory_ids.length - 3}` : ""}`) : "";
86346
+ const own = e.injected_own_bank_count;
86347
+ const additional = e.injected_additional_bank_count;
86348
+ const composition = typeof own === "number" || typeof additional === "number" ? (() => {
86349
+ const text = ` own/add=${own ?? "\u2014"}/${additional ?? "\u2014"}`;
86350
+ return own === 0 && typeof additional === "number" && additional > 0 ? source_default.red(text) : source_default.dim(text);
86351
+ })() : "";
85906
86352
  const score = typeof e.injected_score_min === "number" && typeof e.injected_score_median === "number" && typeof e.injected_score_max === "number" ? source_default.dim(` score=${e.injected_score_min.toFixed(2)}/` + `${e.injected_score_median.toFixed(2)}/` + `${e.injected_score_max.toFixed(2)}`) : "";
85907
- lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${ids}`);
86353
+ lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${composition}${ids}`);
85908
86354
  }
85909
86355
  return lines;
85910
86356
  }
@@ -92414,24 +92860,24 @@ init_source();
92414
92860
  init_loader();
92415
92861
  init_lifecycle();
92416
92862
  init_compose_env();
92417
- import { existsSync as existsSync81, mkdirSync as mkdirSync46, readFileSync as readFileSync75, realpathSync as realpathSync6, statSync as statSync45, chownSync as chownSync8 } from "node:fs";
92863
+ import { existsSync as existsSync81, mkdirSync as mkdirSync46, readFileSync as readFileSync75, realpathSync as realpathSync6, statSync as statSync46, chownSync as chownSync9 } from "node:fs";
92418
92864
  import { spawnSync as spawnSync17 } from "node:child_process";
92419
92865
  import { join as join84, dirname as dirname34, resolve as resolve45 } from "node:path";
92420
92866
  import { homedir as homedir44 } from "node:os";
92421
92867
 
92422
92868
  // src/cli/release-yaml.ts
92423
- var import_yaml19 = __toESM(require_dist(), 1);
92869
+ var import_yaml20 = __toESM(require_dist(), 1);
92424
92870
  function getReleasePinFromConfig(yamlText) {
92425
92871
  let v;
92426
92872
  try {
92427
- v = import_yaml19.parseDocument(yamlText).getIn(["release", "pin"]);
92873
+ v = import_yaml20.parseDocument(yamlText).getIn(["release", "pin"]);
92428
92874
  } catch {
92429
92875
  return;
92430
92876
  }
92431
92877
  return typeof v === "string" ? v : undefined;
92432
92878
  }
92433
92879
  function setReleasePinInConfig(yamlText, pin, now = new Date().toISOString().slice(0, 10)) {
92434
- const doc = import_yaml19.parseDocument(yamlText);
92880
+ const doc = import_yaml20.parseDocument(yamlText);
92435
92881
  const currentPin = doc.getIn(["release", "pin"]);
92436
92882
  const hasChannel = doc.hasIn(["release", "channel"]);
92437
92883
  if (currentPin === pin && !hasChannel)
@@ -92440,31 +92886,31 @@ function setReleasePinInConfig(yamlText, pin, now = new Date().toISOString().sli
92440
92886
  doc.deleteIn(["release", "channel"]);
92441
92887
  doc.setIn(["release", "pin"], pin);
92442
92888
  const node = doc.getIn(["release", "pin"], true);
92443
- if (import_yaml19.isScalar(node)) {
92889
+ if (import_yaml20.isScalar(node)) {
92444
92890
  node.comment = ` ${now}: rolled by switchroom rollout`;
92445
92891
  }
92446
92892
  return String(doc);
92447
92893
  }
92448
92894
  function deleteReleasePinInConfig(yamlText) {
92449
- const doc = import_yaml19.parseDocument(yamlText);
92895
+ const doc = import_yaml20.parseDocument(yamlText);
92450
92896
  if (!doc.hasIn(["release", "pin"]))
92451
92897
  return yamlText;
92452
92898
  doc.deleteIn(["release", "pin"]);
92453
92899
  const rel = doc.getIn(["release"]);
92454
- if (import_yaml19.isMap(rel) && rel.items.length === 0 && !releaseBlockIsCommented(doc)) {
92900
+ if (import_yaml20.isMap(rel) && rel.items.length === 0 && !releaseBlockIsCommented(doc)) {
92455
92901
  doc.delete("release");
92456
92902
  }
92457
92903
  return String(doc);
92458
92904
  }
92459
92905
  function releaseBlockIsCommented(doc) {
92460
92906
  const contents = doc.contents;
92461
- if (!import_yaml19.isMap(contents))
92907
+ if (!import_yaml20.isMap(contents))
92462
92908
  return false;
92463
92909
  for (const item of contents.items) {
92464
- if (!import_yaml19.isPair(item))
92910
+ if (!import_yaml20.isPair(item))
92465
92911
  continue;
92466
92912
  const key = item.key;
92467
- if (!import_yaml19.isScalar(key) || key.value !== "release")
92913
+ if (!import_yaml20.isScalar(key) || key.value !== "release")
92468
92914
  continue;
92469
92915
  const nodes = [key, item.value];
92470
92916
  return nodes.some((n) => !!n && typeof n === "object" && (("commentBefore" in n) && !!n.commentBefore || ("comment" in n) && !!n.comment));
@@ -92477,7 +92923,7 @@ init_operator_uid();
92477
92923
  init_atomic();
92478
92924
 
92479
92925
  // src/cli/preflight-mounts.ts
92480
- import { statSync as statSync44 } from "node:fs";
92926
+ import { statSync as statSync45 } from "node:fs";
92481
92927
  function parseHostBindSources(composeText) {
92482
92928
  const out = [];
92483
92929
  const FILE_HINT = /\.(ya?ml|db|log|toml|json|token|id)$|\/\.vault-token$|machine-id$|localtime$|vault-auto-unlock$|\/webkite$/;
@@ -92504,7 +92950,7 @@ function parseHostBindSources(composeText) {
92504
92950
  return out;
92505
92951
  }
92506
92952
  function validateBindSources(composeText, deps = {}) {
92507
- const stat = deps.stat ?? ((p) => statSync44(p));
92953
+ const stat = deps.stat ?? ((p) => statSync45(p));
92508
92954
  const sources = parseHostBindSources(composeText);
92509
92955
  const issues = [];
92510
92956
  const seen = new Set;
@@ -93093,12 +93539,12 @@ function defaultPersistPin(configPath) {
93093
93539
  const after = setReleasePinInConfig(before, pin);
93094
93540
  if (after === before)
93095
93541
  return;
93096
- writeConfigFileSync(path7, after, statSync45(path7).mode & 511);
93542
+ writeConfigFileSync(path7, after, statSync46(path7).mode & 511);
93097
93543
  try {
93098
93544
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
93099
93545
  const uid = resolveOperatorUid();
93100
93546
  if (uid !== undefined)
93101
- chownSync8(path7, uid, uid);
93547
+ chownSync9(path7, uid, uid);
93102
93548
  }
93103
93549
  } catch {}
93104
93550
  };
@@ -93577,7 +94023,7 @@ function defaultStatusProbe(composePath) {
93577
94023
  } catch {}
93578
94024
  if (scriptPath) {
93579
94025
  try {
93580
- cliBuiltAt = new Date(statSync45(scriptPath).mtimeMs).toISOString();
94026
+ cliBuiltAt = new Date(statSync46(scriptPath).mtimeMs).toISOString();
93581
94027
  } catch {}
93582
94028
  let dir = dirname34(scriptPath);
93583
94029
  for (let i = 0;i < 8; i++) {
@@ -93853,7 +94299,7 @@ function registerUpdateCommand(program3) {
93853
94299
  // src/cli/rollout.ts
93854
94300
  init_helpers();
93855
94301
  import { spawnSync as spawnSync19 } from "node:child_process";
93856
- import { readFileSync as readFileSync77, chownSync as chownSync9, statSync as statSync47 } from "node:fs";
94302
+ import { readFileSync as readFileSync77, chownSync as chownSync10, statSync as statSync48 } from "node:fs";
93857
94303
  import { homedir as homedir46 } from "node:os";
93858
94304
 
93859
94305
  // src/cli/rollout-pin-journal.ts
@@ -93863,7 +94309,7 @@ import {
93863
94309
  writeFileSync as writeFileSync32,
93864
94310
  renameSync as renameSync21,
93865
94311
  unlinkSync as unlinkSync15,
93866
- statSync as statSync46,
94312
+ statSync as statSync47,
93867
94313
  mkdirSync as mkdirSync47
93868
94314
  } from "node:fs";
93869
94315
  import { homedir as homedir45 } from "node:os";
@@ -94020,7 +94466,7 @@ function rollbackPinPersist(configPath, opts = {}) {
94020
94466
  const next = journal.priorPin ? setReleasePinInConfig(current, journal.priorPin) : deleteReleasePinInConfig(current);
94021
94467
  let mode = 384;
94022
94468
  try {
94023
- mode = statSync46(configPath).mode & 511;
94469
+ mode = statSync47(configPath).mode & 511;
94024
94470
  } catch {}
94025
94471
  if (opts.writeConfig) {
94026
94472
  opts.writeConfig(configPath, next, mode);
@@ -94599,7 +95045,7 @@ function createRolloutDeps(params) {
94599
95045
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
94600
95046
  const uid = resolveOperatorUid();
94601
95047
  if (uid !== undefined) {
94602
- chownSync9(path7, uid, uid);
95048
+ chownSync10(path7, uid, uid);
94603
95049
  } else if (hostdCtx) {
94604
95050
  warn("\u26a0\ufe0f rollout (hostd path): could not resolve operator uid; " + "skipping config chown-back to avoid leaving switchroom.yaml " + "root-owned. Verify ownership host-side if other yaml verbs " + `EACCES.
94605
95051
  `);
@@ -94659,7 +95105,7 @@ function createRolloutDeps(params) {
94659
95105
  if (after === before)
94660
95106
  return false;
94661
95107
  beginPinPersist(configPath, pin);
94662
- writeConfigPreservingOwnership(configPath, after, statSync47(configPath).mode & 511);
95108
+ writeConfigPreservingOwnership(configPath, after, statSync48(configPath).mode & 511);
94663
95109
  return true;
94664
95110
  },
94665
95111
  commitPin: () => {
@@ -95105,7 +95551,7 @@ init_merge();
95105
95551
  import {
95106
95552
  existsSync as existsSync84,
95107
95553
  readdirSync as readdirSync28,
95108
- statSync as statSync48,
95554
+ statSync as statSync49,
95109
95555
  unlinkSync as unlinkSync16
95110
95556
  } from "node:fs";
95111
95557
  import { join as join87 } from "node:path";
@@ -95128,7 +95574,7 @@ function collectSessionJsonl(claudeConfigDir) {
95128
95574
  const full = join87(dir, name);
95129
95575
  let st;
95130
95576
  try {
95131
- st = statSync48(full);
95577
+ st = statSync49(full);
95132
95578
  } catch {
95133
95579
  continue;
95134
95580
  }
@@ -95256,7 +95702,7 @@ import {
95256
95702
  readdirSync as readdirSync29,
95257
95703
  readFileSync as readFileSync79,
95258
95704
  renameSync as renameSync22,
95259
- statSync as statSync49,
95705
+ statSync as statSync50,
95260
95706
  unlinkSync as unlinkSync17,
95261
95707
  writeFileSync as writeFileSync33,
95262
95708
  writeSync as writeSync9
@@ -95459,7 +95905,7 @@ function sweepOrphanTmpFiles(stateDir) {
95459
95905
  continue;
95460
95906
  const tmpPath = join88(stateDir, entry);
95461
95907
  try {
95462
- const stat = statSync49(tmpPath);
95908
+ const stat = statSync50(tmpPath);
95463
95909
  if (stat.mtimeMs < cutoff) {
95464
95910
  unlinkSync17(tmpPath);
95465
95911
  }
@@ -95767,7 +96213,7 @@ import {
95767
96213
  } from "node:fs";
95768
96214
  import { dirname as dirname37, join as join89 } from "node:path";
95769
96215
  import { homedir as homedir47 } from "node:os";
95770
- import { execFileSync as execFileSync24 } from "node:child_process";
96216
+ import { execFileSync as execFileSync25 } from "node:child_process";
95771
96217
 
95772
96218
  class PythonEnvError extends Error {
95773
96219
  stderr;
@@ -95814,7 +96260,7 @@ function ensurePythonEnv(opts) {
95814
96260
  }
95815
96261
  mkdirSync49(dirname37(venvDir), { recursive: true });
95816
96262
  try {
95817
- execFileSync24(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
96263
+ execFileSync25(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
95818
96264
  } catch (err) {
95819
96265
  const e = err;
95820
96266
  throw new PythonEnvError(`Failed to create venv for skill "${skillName}" with ${hostPython}: ${e.message}`, e.stderr?.toString());
@@ -95826,7 +96272,7 @@ function ensurePythonEnv(opts) {
95826
96272
  delete childEnv.PIP_TARGET;
95827
96273
  delete childEnv.PIP_PREFIX;
95828
96274
  delete childEnv.PYTHONUSERBASE;
95829
- execFileSync24(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
96275
+ execFileSync25(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
95830
96276
  } catch (err) {
95831
96277
  const e = err;
95832
96278
  throw new PythonEnvError(`Failed to install requirements for skill "${skillName}": ${e.message}`, e.stderr?.toString());
@@ -95855,7 +96301,7 @@ import {
95855
96301
  } from "node:fs";
95856
96302
  import { dirname as dirname38, join as join90 } from "node:path";
95857
96303
  import { homedir as homedir48 } from "node:os";
95858
- import { execFileSync as execFileSync25 } from "node:child_process";
96304
+ import { execFileSync as execFileSync26 } from "node:child_process";
95859
96305
 
95860
96306
  class NodeEnvError extends Error {
95861
96307
  stderr;
@@ -95939,10 +96385,10 @@ function ensureNodeEnv(opts) {
95939
96385
  try {
95940
96386
  if (installer === "bun") {
95941
96387
  const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
95942
- execFileSync25("bun", args, { cwd: envDir, stdio: "pipe" });
96388
+ execFileSync26("bun", args, { cwd: envDir, stdio: "pipe" });
95943
96389
  } else {
95944
96390
  const args = copiedLockfile ? ["ci"] : ["install"];
95945
- execFileSync25("npm", args, { cwd: envDir, stdio: "pipe" });
96391
+ execFileSync26("npm", args, { cwd: envDir, stdio: "pipe" });
95946
96392
  }
95947
96393
  } catch (err) {
95948
96394
  const e = err;
@@ -97024,7 +97470,7 @@ function registerSoulCommand(program3) {
97024
97470
  // src/cli/debug.ts
97025
97471
  init_helpers();
97026
97472
  init_loader();
97027
- import { existsSync as existsSync91, readFileSync as readFileSync83, readdirSync as readdirSync30, statSync as statSync50 } from "node:fs";
97473
+ import { existsSync as existsSync91, readFileSync as readFileSync83, readdirSync as readdirSync30, statSync as statSync51 } from "node:fs";
97028
97474
  import { resolve as resolve53, join as join93 } from "node:path";
97029
97475
  import { createHash as createHash20 } from "node:crypto";
97030
97476
  init_merge();
@@ -97063,7 +97509,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
97063
97509
  const transcriptPath = join93(projectPath, "transcript.jsonl");
97064
97510
  if (!existsSync91(transcriptPath))
97065
97511
  continue;
97066
- const stat3 = statSync50(transcriptPath);
97512
+ const stat3 = statSync51(transcriptPath);
97067
97513
  if (!latest || stat3.mtimeMs > latest.mtime) {
97068
97514
  latest = { path: transcriptPath, mtime: stat3.mtimeMs };
97069
97515
  }
@@ -97298,7 +97744,7 @@ function registerDebugCommand(program3) {
97298
97744
  init_source();
97299
97745
 
97300
97746
  // src/worktree/claim.ts
97301
- import { execFileSync as execFileSync26 } from "node:child_process";
97747
+ import { execFileSync as execFileSync27 } from "node:child_process";
97302
97748
  import { closeSync as closeSync17, mkdirSync as mkdirSync52, openSync as openSync17, existsSync as existsSync93, unlinkSync as unlinkSync19 } from "node:fs";
97303
97749
  import { join as join95, resolve as resolve55 } from "node:path";
97304
97750
  import { homedir as homedir51 } from "node:os";
@@ -97467,7 +97913,7 @@ async function claimWorktree(input, codeRepos) {
97467
97913
  releaseLock();
97468
97914
  }
97469
97915
  try {
97470
- execFileSync26("git", ["worktree", "add", "-b", branch, worktreePath], {
97916
+ execFileSync27("git", ["worktree", "add", "-b", branch, worktreePath], {
97471
97917
  cwd: repoPath,
97472
97918
  stdio: "pipe"
97473
97919
  });
@@ -97480,7 +97926,7 @@ async function claimWorktree(input, codeRepos) {
97480
97926
  }
97481
97927
 
97482
97928
  // src/worktree/release.ts
97483
- import { execFileSync as execFileSync27 } from "node:child_process";
97929
+ import { execFileSync as execFileSync28 } from "node:child_process";
97484
97930
  import { existsSync as existsSync94 } from "node:fs";
97485
97931
  function releaseWorktree(input) {
97486
97932
  const { id } = input;
@@ -97491,7 +97937,7 @@ function releaseWorktree(input) {
97491
97937
  let gitSuccess = true;
97492
97938
  if (existsSync94(record2.path)) {
97493
97939
  try {
97494
- execFileSync27("git", ["worktree", "remove", "--force", record2.path], {
97940
+ execFileSync28("git", ["worktree", "remove", "--force", record2.path], {
97495
97941
  cwd: record2.repo,
97496
97942
  stdio: "pipe"
97497
97943
  });
@@ -97527,7 +97973,7 @@ function listWorktrees() {
97527
97973
  }
97528
97974
 
97529
97975
  // src/worktree/reaper.ts
97530
- import { execFileSync as execFileSync28 } from "node:child_process";
97976
+ import { execFileSync as execFileSync29 } from "node:child_process";
97531
97977
  import { existsSync as existsSync95 } from "node:fs";
97532
97978
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
97533
97979
  function reapSkipReasonText(action) {
@@ -97545,7 +97991,7 @@ function reapSkipReasonText(action) {
97545
97991
  function probePathInUse(path10) {
97546
97992
  let probeRan = false;
97547
97993
  try {
97548
- execFileSync28("fuser", [path10], { stdio: "pipe" });
97994
+ execFileSync29("fuser", [path10], { stdio: "pipe" });
97549
97995
  return "in-use";
97550
97996
  } catch (err) {
97551
97997
  if (err.code !== "ENOENT") {
@@ -97553,7 +97999,7 @@ function probePathInUse(path10) {
97553
97999
  }
97554
98000
  }
97555
98001
  try {
97556
- const out = execFileSync28("lsof", ["-t", path10], {
98002
+ const out = execFileSync29("lsof", ["-t", path10], {
97557
98003
  stdio: ["ignore", "pipe", "ignore"]
97558
98004
  }).toString().trim();
97559
98005
  probeRan = true;
@@ -97568,7 +98014,7 @@ function probePathInUse(path10) {
97568
98014
  }
97569
98015
  function hasUncommittedChanges(_repoPath, worktreePath) {
97570
98016
  try {
97571
- const out = execFileSync28("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
98017
+ const out = execFileSync29("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
97572
98018
  return out.trim().length > 0;
97573
98019
  } catch {
97574
98020
  return true;
@@ -97576,7 +98022,7 @@ function hasUncommittedChanges(_repoPath, worktreePath) {
97576
98022
  }
97577
98023
  function defaultRemoveWorktree(repoPath, worktreePath) {
97578
98024
  try {
97579
- execFileSync28("git", ["worktree", "remove", "--force", worktreePath], {
98025
+ execFileSync29("git", ["worktree", "remove", "--force", worktreePath], {
97580
98026
  cwd: repoPath,
97581
98027
  stdio: "pipe"
97582
98028
  });
@@ -97668,12 +98114,12 @@ function runReaper(nowMs, deps = {}) {
97668
98114
  }
97669
98115
 
97670
98116
  // src/worktree/gc.ts
97671
- import { execFileSync as execFileSync29 } from "node:child_process";
98117
+ import { execFileSync as execFileSync30 } from "node:child_process";
97672
98118
  import {
97673
98119
  existsSync as existsSync96,
97674
98120
  readFileSync as readFileSync85,
97675
98121
  readdirSync as readdirSync32,
97676
- statSync as statSync51,
98122
+ statSync as statSync52,
97677
98123
  renameSync as renameSync24,
97678
98124
  mkdirSync as mkdirSync53,
97679
98125
  rmSync as rmSync17
@@ -97761,7 +98207,7 @@ function isEphemeralPath(path10) {
97761
98207
  }
97762
98208
  return false;
97763
98209
  }
97764
- var defaultExec3 = (file, args, cwd) => execFileSync29(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
98210
+ var defaultExec3 = (file, args, cwd) => execFileSync30(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
97765
98211
  function defaultPrSignal(repo, branch, exec) {
97766
98212
  try {
97767
98213
  const out = exec("gh", [
@@ -97800,7 +98246,7 @@ function planGc(roots, deps = {}) {
97800
98246
  const exists = deps.existsSync ?? existsSync96;
97801
98247
  const readDir = deps.readDir ?? ((p) => readdirSync32(p));
97802
98248
  const readFile4 = deps.readFile ?? ((p) => readFileSync85(p, "utf8"));
97803
- const stat3 = deps.stat ?? ((p) => statSync51(p));
98249
+ const stat3 = deps.stat ?? ((p) => statSync52(p));
97804
98250
  const exec = deps.exec ?? defaultExec3;
97805
98251
  const prSignal = deps.prSignal ?? ((repo, branch) => defaultPrSignal(repo, branch, exec));
97806
98252
  const stamp = deps.dateStamp ?? "undated";
@@ -97994,7 +98440,7 @@ function listTrashEntries(nowMs, deps = {}) {
97994
98440
  const p = join96(stampDir, name);
97995
98441
  let mtimeMs = nowMs;
97996
98442
  try {
97997
- mtimeMs = statSync51(p).mtimeMs;
98443
+ mtimeMs = statSync52(p).mtimeMs;
97998
98444
  } catch {}
97999
98445
  out.push({ path: p, ageDays: (nowMs - mtimeMs) / 86400000 });
98000
98446
  }
@@ -99269,6 +99715,54 @@ function redactToolCallParams(params) {
99269
99715
  }
99270
99716
  return { ...called, arguments: redactToolArguments(called.arguments) };
99271
99717
  }
99718
+ var DEFAULT_RECALL_MAX_TOKENS = 1024;
99719
+ var DEFAULT_REFLECT_MAX_TOKENS = 1024;
99720
+ function resolveClampMaxTokens(envVal, fallback) {
99721
+ if (envVal === undefined || envVal === "")
99722
+ return fallback;
99723
+ const n = Number.parseInt(envVal, 10);
99724
+ return Number.isFinite(n) && n > 0 ? n : fallback;
99725
+ }
99726
+ function acceptedArgs(name) {
99727
+ const spec = FALLBACK_TOOL_TABLE[name];
99728
+ if (!spec)
99729
+ return;
99730
+ return new Set([...spec[0], ...spec[1]]);
99731
+ }
99732
+ function unknownArgMessage(name, unknownKeys, allowed) {
99733
+ const keys = unknownKeys.map((k) => `'${k}'`).join(", ");
99734
+ const plural = unknownKeys.length > 1 ? "parameters" : "parameter";
99735
+ const base = `${name} has no ${keys} ${plural}. ` + `Accepted arguments: ${[...allowed].sort().join(", ")}.`;
99736
+ if (name === "recall" || name === "reflect") {
99737
+ return `${base} There is no 'limit'/'top_k' \u2014 cap results with max_tokens ` + `(a token budget over each fact's text, in relevance order) or ` + `budget: low|mid|high.`;
99738
+ }
99739
+ return base;
99740
+ }
99741
+ function guardAndClampToolCall(params, env2) {
99742
+ const called = params;
99743
+ const name = called?.name;
99744
+ if (typeof name !== "string")
99745
+ return { ok: true, params };
99746
+ const allowed = acceptedArgs(name);
99747
+ if (!allowed)
99748
+ return { ok: true, params };
99749
+ const rawArgs = called?.arguments;
99750
+ const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {};
99751
+ const unknownKeys = Object.keys(args).filter((k) => !allowed.has(k));
99752
+ if (unknownKeys.length > 0) {
99753
+ return { ok: false, text: unknownArgMessage(name, unknownKeys, allowed) };
99754
+ }
99755
+ if (name === "recall" || name === "reflect") {
99756
+ const injected = { ...args };
99757
+ if (injected.max_tokens === undefined) {
99758
+ injected.max_tokens = name === "recall" ? resolveClampMaxTokens(env2.HINDSIGHT_SHIM_RECALL_MAX_TOKENS, DEFAULT_RECALL_MAX_TOKENS) : resolveClampMaxTokens(env2.HINDSIGHT_SHIM_REFLECT_MAX_TOKENS, DEFAULT_REFLECT_MAX_TOKENS);
99759
+ }
99760
+ if (injected.budget === undefined)
99761
+ injected.budget = "low";
99762
+ return { ok: true, params: { ...called, arguments: injected } };
99763
+ }
99764
+ return { ok: true, params };
99765
+ }
99272
99766
  function buildFallbackToolsList() {
99273
99767
  const tools = Object.entries(FALLBACK_TOOL_TABLE).map(([name, [required, props]]) => ({
99274
99768
  name,
@@ -99595,7 +100089,11 @@ class HindsightShim {
99595
100089
  if (called?.name && SYNTHESIZED_TOOL_NAMES.includes(called.name)) {
99596
100090
  return this.synthesizedCall(called.name, called.arguments);
99597
100091
  }
99598
- const forwarded = redactToolCallParams(params);
100092
+ const guarded = guardAndClampToolCall(params, process.env);
100093
+ if (!guarded.ok) {
100094
+ return { content: [{ type: "text", text: guarded.text }], isError: true };
100095
+ }
100096
+ const forwarded = redactToolCallParams(guarded.params);
99599
100097
  try {
99600
100098
  const res = await this.upstream.request("tools/call", forwarded, this.opts.toolsCallTimeoutMs ?? TOOLS_CALL_TIMEOUT_MS);
99601
100099
  this.notifyIfRecovered();
@@ -99698,7 +100196,7 @@ function registerHindsightMcpShimCommand(program3) {
99698
100196
 
99699
100197
  // src/cli/deliver-file.ts
99700
100198
  init_client2();
99701
- import { readFileSync as readFileSync87, statSync as statSync52 } from "node:fs";
100199
+ import { readFileSync as readFileSync87, statSync as statSync53 } from "node:fs";
99702
100200
  import { basename as basename13 } from "node:path";
99703
100201
 
99704
100202
  // src/delivery/onedrive.ts
@@ -100037,7 +100535,7 @@ async function defaultResolveProvider() {
100037
100535
  }
100038
100536
  async function runDeliverFile(localPath, deps = {}) {
100039
100537
  const agentName = safeAgentName(deps.agentName ?? process.env.SWITCHROOM_AGENT_NAME);
100040
- const sizeOf = deps.fileSize ?? ((p) => statSync52(p).size);
100538
+ const sizeOf = deps.fileSize ?? ((p) => statSync53(p).size);
100041
100539
  const read = deps.readFile ?? ((p) => new Uint8Array(readFileSync87(p)));
100042
100540
  const resolveProvider = deps.resolveProvider ?? defaultResolveProvider;
100043
100541
  let size;
@@ -100733,7 +101231,7 @@ init_agent_config();
100733
101231
 
100734
101232
  // src/cli/agent-config-write.ts
100735
101233
  init_agent_config();
100736
- var import_yaml21 = __toESM(require_dist(), 1);
101234
+ var import_yaml22 = __toESM(require_dist(), 1);
100737
101235
 
100738
101236
  // src/config/overlay-writer.ts
100739
101237
  init_paths();
@@ -100746,7 +101244,7 @@ import {
100746
101244
  readdirSync as readdirSync35,
100747
101245
  readFileSync as readFileSync89,
100748
101246
  renameSync as renameSync26,
100749
- statSync as statSync53,
101247
+ statSync as statSync54,
100750
101248
  unlinkSync as unlinkSync20,
100751
101249
  writeSync as writeSync10
100752
101250
  } from "node:fs";
@@ -100790,7 +101288,7 @@ function withAgentLock(paths, fn) {
100790
101288
  if (e.code !== "EEXIST")
100791
101289
  throw err;
100792
101290
  try {
100793
- const age = Date.now() - statSync53(paths.lockPath).mtimeMs;
101291
+ const age = Date.now() - statSync54(paths.lockPath).mtimeMs;
100794
101292
  if (age > 30000) {
100795
101293
  unlinkSync20(paths.lockPath);
100796
101294
  continue;
@@ -100934,7 +101432,7 @@ init_overlay_schema();
100934
101432
  init_schema();
100935
101433
  init_cron_unit_name();
100936
101434
  init_lifecycle();
100937
- var import_yaml20 = __toESM(require_dist(), 1);
101435
+ var import_yaml21 = __toESM(require_dist(), 1);
100938
101436
 
100939
101437
  // src/scheduler/cron-gap.ts
100940
101438
  function expandMinuteTerm(term) {
@@ -101024,7 +101522,7 @@ function violatesMinInterval(cron) {
101024
101522
  function dryRunReconcile(input) {
101025
101523
  let parsed;
101026
101524
  try {
101027
- parsed = import_yaml20.parse(input.yamlText);
101525
+ parsed = import_yaml21.parse(input.yamlText);
101028
101526
  } catch (err) {
101029
101527
  return {
101030
101528
  ok: false,
@@ -101223,7 +101721,7 @@ function denyPendingScheduleEntry(opts) {
101223
101721
 
101224
101722
  // src/cli/agent-config-write.ts
101225
101723
  import { existsSync as existsSync101, readFileSync as readFileSync91 } from "node:fs";
101226
- import { execFileSync as execFileSync30 } from "node:child_process";
101724
+ import { execFileSync as execFileSync31 } from "node:child_process";
101227
101725
 
101228
101726
  // src/scheduler/schedule-report.ts
101229
101727
  var TIER_WEIGHT = { poll: 0, action: 0, cheap: 1, main: 5 };
@@ -101372,7 +101870,7 @@ function parseOverlayCronAndName(e) {
101372
101870
  if (headerMatch)
101373
101871
  name = headerMatch[1];
101374
101872
  try {
101375
- const doc = import_yaml21.parse(e.raw);
101873
+ const doc = import_yaml22.parse(e.raw);
101376
101874
  const first = doc?.schedule?.[0];
101377
101875
  if (first) {
101378
101876
  if (typeof first.cron === "string")
@@ -101408,7 +101906,7 @@ function scheduleAdd(opts) {
101408
101906
  ]
101409
101907
  };
101410
101908
  const yamlText = (() => {
101411
- const body = import_yaml21.stringify(doc);
101909
+ const body = import_yaml22.stringify(doc);
101412
101910
  const header = opts.name ? `# name: ${opts.name}
101413
101911
  ` : "";
101414
101912
  return header + body;
@@ -101544,7 +102042,7 @@ function scheduleAddOrStage(opts) {
101544
102042
  ]
101545
102043
  };
101546
102044
  const yamlText = (opts.name ? `# name: ${opts.name}
101547
- ` : "") + import_yaml21.stringify(doc);
102045
+ ` : "") + import_yaml22.stringify(doc);
101548
102046
  const summary = (() => {
101549
102047
  const parts = [`cron=${opts.cronExpr}`];
101550
102048
  if (opts.secrets?.length)
@@ -101594,7 +102092,7 @@ function scheduleRemove(opts) {
101594
102092
  break;
101595
102093
  }
101596
102094
  try {
101597
- const parsed = import_yaml21.parse(e.raw);
102095
+ const parsed = import_yaml22.parse(e.raw);
101598
102096
  if (parsed && parsed.name === opts.name) {
101599
102097
  match = e;
101600
102098
  break;
@@ -101819,7 +102317,7 @@ function registerAgentConfigWriteCommands(program3) {
101819
102317
  blob = existsSync101(opts.jsonl) ? readFileSync91(opts.jsonl, "utf-8") : "";
101820
102318
  } else {
101821
102319
  try {
101822
- blob = execFileSync30("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
102320
+ blob = execFileSync31("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
101823
102321
  encoding: "utf-8",
101824
102322
  stdio: ["ignore", "pipe", "ignore"]
101825
102323
  });
@@ -101847,11 +102345,11 @@ function registerAgentConfigWriteCommands(program3) {
101847
102345
  }
101848
102346
 
101849
102347
  // src/cli/agent-config-skill-write.ts
101850
- var import_yaml22 = __toESM(require_dist(), 1);
102348
+ var import_yaml23 = __toESM(require_dist(), 1);
101851
102349
  import { existsSync as existsSync102 } from "node:fs";
101852
102350
  init_reconcile_default_skills();
101853
102351
  init_agent_config();
101854
- var import_yaml23 = __toESM(require_dist(), 1);
102352
+ var import_yaml24 = __toESM(require_dist(), 1);
101855
102353
  import { join as join103 } from "node:path";
101856
102354
  var MAX_SKILLS_PER_AGENT = 20;
101857
102355
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
@@ -101901,7 +102399,7 @@ function countCurrentSkills(agent, opts) {
101901
102399
  let total = 0;
101902
102400
  for (const e of entries) {
101903
102401
  try {
101904
- const doc = import_yaml23.parse(e.raw);
102402
+ const doc = import_yaml24.parse(e.raw);
101905
102403
  total += (doc?.skills ?? []).length;
101906
102404
  } catch {}
101907
102405
  }
@@ -101931,7 +102429,7 @@ function skillInstall(opts) {
101931
102429
  if (!existsSync102(skillPath)) {
101932
102430
  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.`);
101933
102431
  }
101934
- const yamlText = import_yaml22.stringify({ skills: [skillName] });
102432
+ const yamlText = import_yaml23.stringify({ skills: [skillName] });
101935
102433
  let path10;
101936
102434
  try {
101937
102435
  path10 = writeSkillsOverlayEntry(agent, slug, yamlText, { root: opts.root });
@@ -102091,6 +102589,7 @@ function registerAgentConfigSkillWriteCommands(program3) {
102091
102589
 
102092
102590
  // src/cli/skill.ts
102093
102591
  import {
102592
+ chmodSync as chmodSync17,
102094
102593
  closeSync as closeSync20,
102095
102594
  existsSync as existsSync103,
102096
102595
  lstatSync as lstatSync12,
@@ -102102,7 +102601,7 @@ import {
102102
102601
  realpathSync as realpathSync7,
102103
102602
  renameSync as renameSync28,
102104
102603
  rmSync as rmSync19,
102105
- statSync as statSync54,
102604
+ statSync as statSync55,
102106
102605
  writeFileSync as writeFileSync43
102107
102606
  } from "node:fs";
102108
102607
  import { tmpdir as tmpdir6, homedir as homedir54 } from "node:os";
@@ -102110,7 +102609,7 @@ import { dirname as dirname41, join as join104, relative as relative4, resolve a
102110
102609
  import { spawnSync as spawnSync21 } from "node:child_process";
102111
102610
 
102112
102611
  // src/cli/skill-common.ts
102113
- var import_yaml24 = __toESM(require_dist(), 1);
102612
+ var import_yaml25 = __toESM(require_dist(), 1);
102114
102613
  var MAX_SKILLS_PER_AGENT2 = 20;
102115
102614
  var MAX_FILE_BYTES = 256 * 1024;
102116
102615
  var MAX_SKILL_BYTES = 2 * 1024 * 1024;
@@ -102215,7 +102714,7 @@ function validateSkillMd(content, expectedName) {
102215
102714
  }
102216
102715
  let parsed;
102217
102716
  try {
102218
- parsed = import_yaml24.parse(fmText);
102717
+ parsed = import_yaml25.parse(fmText);
102219
102718
  } catch (e) {
102220
102719
  return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter is not valid YAML: ${e.message}`);
102221
102720
  }
@@ -102373,7 +102872,7 @@ function isTarballPath(p) {
102373
102872
  }
102374
102873
  function loadFromDir(dir) {
102375
102874
  const abs = realpathSync7(dir);
102376
- if (!statSync54(abs).isDirectory()) {
102875
+ if (!statSync55(abs).isDirectory()) {
102377
102876
  fail4(`--from path is not a directory: ${dir}`);
102378
102877
  }
102379
102878
  const files = {};
@@ -102573,6 +103072,7 @@ function writePayload(poolDir, name, files) {
102573
103072
  fail4(`refusing to overwrite symlink at ${target}; investigate manually`);
102574
103073
  }
102575
103074
  const staging = mkdtempSync5(join104(poolDir, `.skill-apply-stage-${name}-`));
103075
+ chmodSync17(staging, 493);
102576
103076
  let oldRename = null;
102577
103077
  try {
102578
103078
  for (const [path10, content] of Object.entries(files)) {
@@ -102633,7 +103133,7 @@ function registerSkillCommand(program3) {
102633
103133
  if (!existsSync103(fromPath)) {
102634
103134
  fail4(`--from path does not exist: ${opts.from}`);
102635
103135
  }
102636
- const st = statSync54(fromPath);
103136
+ const st = statSync55(fromPath);
102637
103137
  if (st.isDirectory()) {
102638
103138
  files = loadFromDir(fromPath);
102639
103139
  } else if (isTarballPath(fromPath)) {
@@ -102685,6 +103185,7 @@ function sumBytes(files) {
102685
103185
  // src/cli/skill-personal.ts
102686
103186
  init_esm();
102687
103187
  import {
103188
+ chmodSync as chmodSync18,
102688
103189
  closeSync as closeSync21,
102689
103190
  existsSync as existsSync104,
102690
103191
  lstatSync as lstatSync13,
@@ -102695,7 +103196,7 @@ import {
102695
103196
  readdirSync as readdirSync38,
102696
103197
  renameSync as renameSync29,
102697
103198
  rmSync as rmSync20,
102698
- statSync as statSync55,
103199
+ statSync as statSync56,
102699
103200
  utimesSync,
102700
103201
  writeFileSync as writeFileSync44
102701
103202
  } from "node:fs";
@@ -102779,6 +103280,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
102779
103280
  }
102780
103281
  };
102781
103282
  walk2(liveSkillDir, staging);
103283
+ chmodSync18(staging, 493);
102782
103284
  if (existsSync104(dest)) {
102783
103285
  const prior = join105(configSkillsRoot, `.${name}-prior-${Date.now()}`);
102784
103286
  renameSync29(dest, prior);
@@ -102851,7 +103353,7 @@ function readStdinSync2() {
102851
103353
  }
102852
103354
  function loadFromDir2(dir) {
102853
103355
  const abs = resolve59(dir);
102854
- if (!statSync55(abs).isDirectory()) {
103356
+ if (!statSync56(abs).isDirectory()) {
102855
103357
  fail5(`--from path is not a directory: ${dir}`);
102856
103358
  }
102857
103359
  const files = {};
@@ -102937,7 +103439,7 @@ function sweepTrash(agentsRoot, agent) {
102937
103439
  continue;
102938
103440
  const entPath = join105(trash, ent.name);
102939
103441
  try {
102940
- const st = statSync55(entPath);
103442
+ const st = statSync56(entPath);
102941
103443
  if (now - st.mtimeMs > TRASH_TTL_MS) {
102942
103444
  rmSync20(entPath, { recursive: true, force: true });
102943
103445
  }
@@ -102957,6 +103459,7 @@ function writePersonalSkill(targetDir, files) {
102957
103459
  }
102958
103460
  mkdirSync61(dirname42(targetDir), { recursive: true, mode: 493 });
102959
103461
  const staging = mkdtempSync6(join105(dirname42(targetDir), `.skill-personal-stage-`));
103462
+ chmodSync18(staging, 493);
102960
103463
  let oldRename = null;
102961
103464
  try {
102962
103465
  for (const [path10, content] of Object.entries(files)) {
@@ -103062,7 +103565,7 @@ function loadFiles(opts) {
103062
103565
  if (!existsSync104(p)) {
103063
103566
  fail5(`--from path does not exist: ${opts.from}`);
103064
103567
  }
103065
- const st = statSync55(p);
103568
+ const st = statSync56(p);
103066
103569
  if (st.isDirectory()) {
103067
103570
  return loadFromDir2(p);
103068
103571
  }
@@ -103283,7 +103786,7 @@ function listPersonalAction(opts) {
103283
103786
  if (e.isFile()) {
103284
103787
  fileCount += 1;
103285
103788
  try {
103286
- totalBytes += statSync55(join105(sub, e.name)).size;
103789
+ totalBytes += statSync56(join105(sub, e.name)).size;
103287
103790
  } catch {}
103288
103791
  } else if (e.isDirectory()) {
103289
103792
  walk2(join105(sub, e.name));
@@ -103400,8 +103903,8 @@ function registerSelfImproveProposeSkillCommand(program3) {
103400
103903
  // src/cli/skill-search.ts
103401
103904
  init_esm();
103402
103905
  init_helpers();
103403
- var import_yaml25 = __toESM(require_dist(), 1);
103404
- import { existsSync as existsSync105, readdirSync as readdirSync39, readFileSync as readFileSync95, statSync as statSync56 } from "node:fs";
103906
+ var import_yaml26 = __toESM(require_dist(), 1);
103907
+ import { existsSync as existsSync105, readdirSync as readdirSync39, readFileSync as readFileSync95, statSync as statSync57 } from "node:fs";
103405
103908
  import { homedir as homedir57 } from "node:os";
103406
103909
  import { join as join107, resolve as resolve60 } from "node:path";
103407
103910
  var PERSONAL_PREFIX2 = "personal-";
@@ -103440,7 +103943,7 @@ function readSkillFrontmatter(skillDir) {
103440
103943
  const fmText = rest.slice(0, endIdx);
103441
103944
  let parsed;
103442
103945
  try {
103443
- parsed = import_yaml25.parse(fmText);
103946
+ parsed = import_yaml26.parse(fmText);
103444
103947
  } catch (e) {
103445
103948
  return { error: `yaml parse: ${e.message}` };
103446
103949
  }
@@ -103452,7 +103955,7 @@ function readSkillFrontmatter(skillDir) {
103452
103955
  function statSkillMd(skillDir) {
103453
103956
  const mdPath = join107(skillDir, "SKILL.md");
103454
103957
  try {
103455
- const st = statSync56(mdPath);
103958
+ const st = statSync57(mdPath);
103456
103959
  return { size: st.size, mtime: st.mtime.toISOString() };
103457
103960
  } catch {
103458
103961
  return null;
@@ -103476,7 +103979,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
103476
103979
  continue;
103477
103980
  const dirPath = join107(skillsDir, ent);
103478
103981
  try {
103479
- if (!statSync56(dirPath).isDirectory())
103982
+ if (!statSync57(dirPath).isDirectory())
103480
103983
  continue;
103481
103984
  } catch {
103482
103985
  continue;
@@ -103516,7 +104019,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
103516
104019
  continue;
103517
104020
  const dirPath = join107(sharedRoot, ent);
103518
104021
  try {
103519
- if (!statSync56(dirPath).isDirectory())
104022
+ if (!statSync57(dirPath).isDirectory())
103520
104023
  continue;
103521
104024
  } catch {
103522
104025
  continue;
@@ -103552,7 +104055,7 @@ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
103552
104055
  continue;
103553
104056
  const dirPath = join107(bundledRoot, ent);
103554
104057
  try {
103555
- if (!statSync56(dirPath).isDirectory())
104058
+ if (!statSync57(dirPath).isDirectory())
103556
104059
  continue;
103557
104060
  } catch {
103558
104061
  continue;
@@ -103700,7 +104203,7 @@ import {
103700
104203
  mkdirSync as mkdirSync62,
103701
104204
  readdirSync as readdirSync40,
103702
104205
  writeFileSync as writeFileSync45,
103703
- statSync as statSync57,
104206
+ statSync as statSync58,
103704
104207
  lstatSync as lstatSync14,
103705
104208
  realpathSync as realpathSync8,
103706
104209
  copyFileSync as copyFileSync14
@@ -103812,6 +104315,10 @@ services:
103812
104315
  # HAProxy writes its runtime state/pidfile under /run; read_only rootfs
103813
104316
  # otherwise makes it crash-loop on boot.
103814
104317
  - /run
104318
+ # The tecnativa/docker-socket-proxy entrypoint renders /tmp/haproxy.cfg
104319
+ # on boot; with a read_only rootfs and no /tmp tmpfs it can't write the
104320
+ # generated config and crash-loops before HAProxy ever starts.
104321
+ - /tmp
103815
104322
  cap_drop:
103816
104323
  - ALL
103817
104324
  cap_add:
@@ -104213,7 +104720,7 @@ function doStatus() {
104213
104720
  continue;
104214
104721
  const sockPath = join108(dir, name, "sock");
104215
104722
  if (existsSync107(sockPath)) {
104216
- const st = statSync57(sockPath);
104723
+ const st = statSync58(sockPath);
104217
104724
  if ((st.mode & 61440) === 49152) {
104218
104725
  entries.push(`${name} \u2192 ${sockPath}`);
104219
104726
  }
@@ -104304,7 +104811,7 @@ init_source();
104304
104811
  init_helpers();
104305
104812
  init_operator_uid();
104306
104813
  init_compose();
104307
- import { chownSync as chownSync10, existsSync as existsSync108, mkdirSync as mkdirSync63, writeFileSync as writeFileSync46, copyFileSync as copyFileSync15 } from "node:fs";
104814
+ import { chownSync as chownSync11, existsSync as existsSync108, mkdirSync as mkdirSync63, writeFileSync as writeFileSync46, copyFileSync as copyFileSync15 } from "node:fs";
104308
104815
  import { homedir as homedir59 } from "node:os";
104309
104816
  import { join as join109 } from "node:path";
104310
104817
  import { spawnSync as spawnSync26 } from "node:child_process";
@@ -104484,10 +104991,10 @@ async function doInstall2(opts, program3) {
104484
104991
  writeFileSync46(composePath, yaml, "utf8");
104485
104992
  try {
104486
104993
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
104487
- chownSync10(dir, operatorUid, operatorUid);
104488
- chownSync10(composePath, operatorUid, operatorUid);
104994
+ chownSync11(dir, operatorUid, operatorUid);
104995
+ chownSync11(composePath, operatorUid, operatorUid);
104489
104996
  if (bak)
104490
- chownSync10(bak, operatorUid, operatorUid);
104997
+ chownSync11(bak, operatorUid, operatorUid);
104491
104998
  }
104492
104999
  } catch {}
104493
105000
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
@@ -105104,7 +105611,7 @@ init_install_cron();
105104
105611
 
105105
105612
  // src/hindsight-watch/probe.ts
105106
105613
  import { spawnSync as spawnSync28 } from "node:child_process";
105107
- import { readFileSync as readFileSync100, readdirSync as readdirSync44, statSync as statSync59 } from "node:fs";
105614
+ import { readFileSync as readFileSync100, readdirSync as readdirSync44, statSync as statSync60 } from "node:fs";
105108
105615
  import { homedir as homedir62 } from "node:os";
105109
105616
  import { resolve as resolve62 } from "node:path";
105110
105617
 
@@ -105232,7 +105739,7 @@ function readLlmSignals(series) {
105232
105739
  }
105233
105740
 
105234
105741
  // src/hindsight-watch/recall-log.ts
105235
- import { closeSync as closeSync22, existsSync as existsSync112, openSync as openSync22, readdirSync as readdirSync43, readSync as readSync6, statSync as statSync58 } from "node:fs";
105742
+ import { closeSync as closeSync22, existsSync as existsSync112, openSync as openSync22, readdirSync as readdirSync43, readSync as readSync6, statSync as statSync59 } from "node:fs";
105236
105743
  import { join as join111 } from "node:path";
105237
105744
  var RECALL_WINDOW_ROWS = 200;
105238
105745
  var RECALL_MAX_TAIL_BYTES = 1024 * 1024;
@@ -105247,7 +105754,7 @@ function readRecallLogTail2(path10, windowRows = RECALL_WINDOW_ROWS) {
105247
105754
  let truncatedHead = false;
105248
105755
  let fd;
105249
105756
  try {
105250
- const size = statSync58(path10).size;
105757
+ const size = statSync59(path10).size;
105251
105758
  const start = Math.max(0, size - RECALL_MAX_TAIL_BYTES);
105252
105759
  truncatedHead = start > 0;
105253
105760
  const length = size - start;
@@ -105479,7 +105986,7 @@ function readDropCount(hindsightDir) {
105479
105986
  const path10 = resolve62(hindsightDir, "pending-drops.json");
105480
105987
  let parsed;
105481
105988
  try {
105482
- if (statSync59(path10).size > DROPS_LEDGER_MAX_BYTES)
105989
+ if (statSync60(path10).size > DROPS_LEDGER_MAX_BYTES)
105483
105990
  return 0;
105484
105991
  parsed = JSON.parse(readFileSync100(path10, "utf8"));
105485
105992
  } catch {
@@ -105504,6 +106011,7 @@ function probeSpool(agentsDir = resolveAgentsDir2()) {
105504
106011
  let dead = 0;
105505
106012
  let evicted = 0;
105506
106013
  let drops = 0;
106014
+ let duplicates = 0;
105507
106015
  let agents = 0;
105508
106016
  for (const name of names) {
105509
106017
  const hindsightDir = resolve62(agentsDir, name, "home", ".hindsight");
@@ -105532,9 +106040,15 @@ function probeSpool(agentsDir = resolveAgentsDir2()) {
105532
106040
  dead++;
105533
106041
  }
105534
106042
  } catch {}
106043
+ try {
106044
+ for (const f of readdirSync44(resolve62(hindsightDir, "pending-duplicate"))) {
106045
+ if (f.endsWith(".json"))
106046
+ duplicates++;
106047
+ }
106048
+ } catch {}
105535
106049
  drops += readDropCount(hindsightDir);
105536
106050
  }
105537
- return { pending, dead, evicted, drops, agents };
106051
+ return { pending, dead, evicted, drops, duplicates, agents };
105538
106052
  }
105539
106053
  var PSQL_BOOTSTRAP_SH = `
105540
106054
  set -e
@@ -105711,6 +106225,7 @@ async function probeOnce(opts = {}) {
105711
106225
  dead: spool.dead,
105712
106226
  evicted: spool.evicted,
105713
106227
  drops: spool.drops,
106228
+ duplicates: spool.duplicates,
105714
106229
  restartCount: container.restartCount,
105715
106230
  startedAt: container.startedAt,
105716
106231
  health: container.health,
@@ -105817,7 +106332,9 @@ function evaluateRetainLoss(ring) {
105817
106332
  ["dropped retain", last.drops - prev.drops]
105818
106333
  ];
105819
106334
  const risen = channels.filter(([, added]) => added > 0);
105820
- const totals = `totals: ${last.dead} dead, ${last.evicted} evicted, ${last.drops} dropped`;
106335
+ const dup = last.duplicates;
106336
+ const dupNote = typeof dup === "number" && dup > 0 ? `, ${dup} collapsed-duplicate (not loss)` : "";
106337
+ const totals = `totals: ${last.dead} dead, ${last.evicted} evicted, ${last.drops} dropped${dupNote}`;
105821
106338
  if (risen.length > 0) {
105822
106339
  return {
105823
106340
  signal,
@@ -105829,7 +106346,8 @@ function evaluateRetainLoss(ring) {
105829
106346
  dropsAdded: last.drops - prev.drops,
105830
106347
  dead: last.dead,
105831
106348
  evicted: last.evicted,
105832
- drops: last.drops
106349
+ drops: last.drops,
106350
+ duplicates: dup ?? 0
105833
106351
  }
105834
106352
  };
105835
106353
  }
@@ -105843,7 +106361,8 @@ function evaluateRetainLoss(ring) {
105843
106361
  dropsAdded: 0,
105844
106362
  dead: last.dead,
105845
106363
  evicted: last.evicted,
105846
- drops: last.drops
106364
+ drops: last.drops,
106365
+ duplicates: dup ?? 0
105847
106366
  }
105848
106367
  };
105849
106368
  }
@@ -106033,7 +106552,7 @@ function evaluateConsolidationQueueAge(ring) {
106033
106552
  return {
106034
106553
  signal,
106035
106554
  state: "ok",
106036
- detail: "consolidation queue empty",
106555
+ detail: "no pending/processing operations (failures are not counted here \u2014 " + "see consolidation-failure-streak)",
106037
106556
  measured: { pending: 0, oldestAgeS: 0 }
106038
106557
  };
106039
106558
  }
@@ -106513,8 +107032,27 @@ function runInstallCron(cronUser) {
106513
107032
  `) + " This path needs root; re-run under sudo with `--cron-user <operator>`.\n");
106514
107033
  return 1;
106515
107034
  }
107035
+ let logStatus;
107036
+ try {
107037
+ const logRes = ensureLogFile({ user });
107038
+ logStatus = logRes.status === "created" ? `created ${logRes.path} (owned by ${user})` : `${logRes.path} already present`;
107039
+ } catch (e) {
107040
+ process.stderr.write(source_default.red(`hindsight-watch: could not provision the log file: ${e.message}
107041
+ `) + ` The cron redirects into ${CRON_LOG_PATH} and would die at ` + `the redirection every tick.
107042
+ This path needs root; re-run under sudo with ` + "`--cron-user <operator>`.\n");
107043
+ return 1;
107044
+ }
107045
+ let rotateStatus;
107046
+ try {
107047
+ const rotateRes = installLogrotate({ user });
107048
+ rotateStatus = rotateRes.status === "installed" ? `installed ${rotateRes.path}` : `${rotateRes.path} already up to date`;
107049
+ } catch (e) {
107050
+ rotateStatus = source_default.yellow(`logrotate drop-in NOT written (${e.message}) \u2014 log growth is unbounded`);
107051
+ }
106516
107052
  const verb = res.status === "installed" ? "installed" : "already up to date";
106517
107053
  process.stdout.write(`${source_default.green("\u2713")} hindsight-watch cron ${verb} at ${res.path} ` + `(${CRON_SCHEDULE}, as ${user})
107054
+ ` + ` log: ${logStatus}
107055
+ ` + ` logrotate: ${rotateStatus}
106518
107056
  ` + ` Verify with: switchroom doctor | switchroom hindsight-watch --dry-run
106519
107057
  `);
106520
107058
  return 0;