switchroom 0.19.40 → 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 (37) 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 +810 -241
  5. package/dist/host-control/main.js +94 -19
  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/bridge/bridge.ts +2 -2
  10. package/telegram-plugin/dist/bridge/bridge.js +2 -2
  11. package/telegram-plugin/dist/gateway/gateway.js +332 -124
  12. package/telegram-plugin/dist/server.js +2 -2
  13. package/telegram-plugin/gateway/approval-callback-consume-record.test.ts +132 -0
  14. package/telegram-plugin/gateway/checklist-fallback.ts +370 -0
  15. package/telegram-plugin/gateway/gateway.ts +76 -76
  16. package/telegram-plugin/gateway/liveness-wiring.ts +12 -0
  17. package/telegram-plugin/gateway/model-command.ts +21 -109
  18. package/telegram-plugin/gateway/stream-render.ts +57 -0
  19. package/telegram-plugin/gateway/turn-record-status.ts +80 -0
  20. package/telegram-plugin/tests/checklist-fallback.test.ts +317 -0
  21. package/telegram-plugin/tests/framework-fallback-drains-parked.test.ts +134 -0
  22. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -6
  23. package/telegram-plugin/tests/helpers/liveness-wiring-fixture.ts +3 -0
  24. package/telegram-plugin/tests/turn-record-status.test.ts +62 -0
  25. package/vendor/hindsight-memory/CHANGELOG.md +28 -0
  26. package/vendor/hindsight-memory/docs/measurements/subagent-volume-gate-3994.md +84 -0
  27. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +254 -40
  28. package/vendor/hindsight-memory/scripts/lib/content.py +11 -2
  29. package/vendor/hindsight-memory/scripts/recall.py +18 -3
  30. package/vendor/hindsight-memory/scripts/subagent_retain.py +59 -38
  31. package/vendor/hindsight-memory/scripts/tests/data/replay_volume_gate_3994.py +295 -0
  32. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +109 -0
  33. package/vendor/hindsight-memory/scripts/tests/test_overlap_tokens.py +135 -0
  34. package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +20 -23
  35. package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +48 -0
  36. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +94 -1
  37. 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.40", COMMIT_SHA = "89286b22";
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;
@@ -70125,6 +70561,9 @@ var init_server4 = __esm(() => {
70125
70561
  ];
70126
70562
  });
70127
70563
 
70564
+ // telegram-plugin/gateway/turn-record-status.ts
70565
+ var ROUTE_FIELD_SHIP_TS = 1785456000;
70566
+
70128
70567
  // src/fleet-health/detect.ts
70129
70568
  function parseTurns(text) {
70130
70569
  const out = [];
@@ -70160,12 +70599,14 @@ function isoFromTs(ts) {
70160
70599
  function detectTurnFindings(agent, turns, opts = {}) {
70161
70600
  const findings = [];
70162
70601
  const silentNoopFloorTs = opts.silentNoopFloorTs ?? SILENT_NOOP_FLOOR_TS;
70602
+ const routeFieldShipTs = opts.routeFieldShipTs ?? ROUTE_FIELD_SHIP_TS;
70163
70603
  for (const t of ownedTurns(agent, turns)) {
70164
70604
  const tid = typeof t.turn_id === "string" ? t.turn_id : "?";
70165
70605
  const st = t.status;
70166
70606
  const tl = typeof t.tools === "number" ? t.tools : 0;
70167
70607
  const dur = typeof t.duration_ms === "number" ? t.duration_ms : 0;
70168
70608
  const synthetic = tid.includes("synthetic-");
70609
+ const route = typeof t.route === "string" ? t.route : undefined;
70169
70610
  const ts = isoFromTs(t.ts);
70170
70611
  if (st === "send_failed") {
70171
70612
  findings.push({
@@ -70194,13 +70635,33 @@ function detectTurnFindings(agent, turns, opts = {}) {
70194
70635
  });
70195
70636
  }
70196
70637
  if (st === "complete" && tl === 0 && !synthetic && t.ts != null && t.ts >= silentNoopFloorTs) {
70197
- findings.push({
70198
- signal: "silent-no-op-candidate",
70199
- agent,
70200
- turn_id: tid,
70201
- log_pointer: `turns.jsonl:${tid} tools=0`,
70202
- ts
70203
- });
70638
+ if (route === "flush") {
70639
+ findings.push({
70640
+ signal: "flush-recovered-turn",
70641
+ agent,
70642
+ turn_id: tid,
70643
+ log_pointer: `turns.jsonl:${tid} tools=0 route=flush`,
70644
+ ts
70645
+ });
70646
+ } else if (route === "reply" || route === "stream") {} else if (route === "none") {
70647
+ findings.push({
70648
+ signal: "silent-no-op-candidate",
70649
+ agent,
70650
+ turn_id: tid,
70651
+ log_pointer: `turns.jsonl:${tid} tools=0 route=none`,
70652
+ ts
70653
+ });
70654
+ } else {
70655
+ if (t.ts >= routeFieldShipTs) {
70656
+ findings.push({
70657
+ signal: "silent-no-op-candidate",
70658
+ agent,
70659
+ turn_id: tid,
70660
+ log_pointer: `turns.jsonl:${tid} tools=0`,
70661
+ ts
70662
+ });
70663
+ }
70664
+ }
70204
70665
  }
70205
70666
  }
70206
70667
  return findings;
@@ -70314,6 +70775,12 @@ var init_mapping = __esm(() => {
70314
70775
  job_spec: "know-what-my-agent-is-doing",
70315
70776
  signature: "silent-no-op:completed-zero-tools"
70316
70777
  },
70778
+ "flush-recovered-turn": {
70779
+ failure_mode: "drift",
70780
+ severity: 1,
70781
+ job_spec: "know-what-my-agent-is-doing",
70782
+ signature: "flush-recovered:completed-zero-tools-route-flush"
70783
+ },
70317
70784
  "duplicate-delivery-represent": {
70318
70785
  failure_mode: "duplicate",
70319
70786
  severity: 2,
@@ -70630,14 +71097,14 @@ function detectHeaderMisconfig(parsed) {
70630
71097
  }
70631
71098
  function parseLitellmConfig(text) {
70632
71099
  try {
70633
- return import_yaml26.parse(text);
71100
+ return import_yaml27.parse(text);
70634
71101
  } catch {
70635
71102
  return null;
70636
71103
  }
70637
71104
  }
70638
- 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;
70639
71106
  var init_header_passthrough_guard = __esm(() => {
70640
- import_yaml26 = __toESM(require_dist(), 1);
71107
+ import_yaml27 = __toESM(require_dist(), 1);
70641
71108
  BARE_ANTHROPIC_FAMILY_ALIASES = new Set([
70642
71109
  "opus",
70643
71110
  "sonnet",
@@ -73866,6 +74333,9 @@ function summarizeReconcileBatch(totalCount, failures) {
73866
74333
  lines: failures.map((f) => `${f.name}: ${f.error}`)
73867
74334
  };
73868
74335
  }
74336
+ function shouldAutoRestartAfterReconcile(opts, restartRequiredCount) {
74337
+ return opts.restart !== false && restartRequiredCount > 0;
74338
+ }
73869
74339
  function resolveLogsTail(lines) {
73870
74340
  const DEFAULT_TAIL = 50;
73871
74341
  const MAX_TAIL = 1000;
@@ -74714,7 +75184,7 @@ Scaffolding agent: ${name}
74714
75184
  }
74715
75185
  }
74716
75186
  const needsRestart = restartRequired.length > 0 || staleTillRestart.length > 0;
74717
- const autoRestart = restartRequired.length > 0 && !opts.noRestart;
75187
+ const autoRestart = shouldAutoRestartAfterReconcile(opts, restartRequired.length);
74718
75188
  if (!needsRestart && hot.length > 0) {
74719
75189
  console.log(source_default.green(`
74720
75190
  (no restart needed, changes active next turn)`));
@@ -74734,7 +75204,7 @@ Restart: switchroom agent restart ${n}`));
74734
75204
  }
74735
75205
  }
74736
75206
  const changesBySemantics = result.changesBySemantics;
74737
- const autoRestartNeeded = !opts.noRestart && changesBySemantics !== undefined && changesBySemantics.restartRequired.length > 0;
75207
+ const autoRestartNeeded = changesBySemantics !== undefined && shouldAutoRestartAfterReconcile(opts, changesBySemantics.restartRequired.length);
74738
75208
  const shouldRestart = (opts.restart || opts.gracefulRestart || autoRestartNeeded) && allChanges.length > 0;
74739
75209
  if (shouldRestart) {
74740
75210
  try {
@@ -85866,14 +86336,21 @@ ${name}:`)];
85866
86336
  const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
85867
86337
  const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
85868
86338
  const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
85869
- 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}`) : "")));
85870
86341
  for (const e of entries) {
85871
86342
  const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
85872
86343
  const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
85873
86344
  const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
85874
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
+ })() : "";
85875
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)}`) : "";
85876
- 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}`);
85877
86354
  }
85878
86355
  return lines;
85879
86356
  }
@@ -92383,24 +92860,24 @@ init_source();
92383
92860
  init_loader();
92384
92861
  init_lifecycle();
92385
92862
  init_compose_env();
92386
- 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";
92387
92864
  import { spawnSync as spawnSync17 } from "node:child_process";
92388
92865
  import { join as join84, dirname as dirname34, resolve as resolve45 } from "node:path";
92389
92866
  import { homedir as homedir44 } from "node:os";
92390
92867
 
92391
92868
  // src/cli/release-yaml.ts
92392
- var import_yaml19 = __toESM(require_dist(), 1);
92869
+ var import_yaml20 = __toESM(require_dist(), 1);
92393
92870
  function getReleasePinFromConfig(yamlText) {
92394
92871
  let v;
92395
92872
  try {
92396
- v = import_yaml19.parseDocument(yamlText).getIn(["release", "pin"]);
92873
+ v = import_yaml20.parseDocument(yamlText).getIn(["release", "pin"]);
92397
92874
  } catch {
92398
92875
  return;
92399
92876
  }
92400
92877
  return typeof v === "string" ? v : undefined;
92401
92878
  }
92402
92879
  function setReleasePinInConfig(yamlText, pin, now = new Date().toISOString().slice(0, 10)) {
92403
- const doc = import_yaml19.parseDocument(yamlText);
92880
+ const doc = import_yaml20.parseDocument(yamlText);
92404
92881
  const currentPin = doc.getIn(["release", "pin"]);
92405
92882
  const hasChannel = doc.hasIn(["release", "channel"]);
92406
92883
  if (currentPin === pin && !hasChannel)
@@ -92409,31 +92886,31 @@ function setReleasePinInConfig(yamlText, pin, now = new Date().toISOString().sli
92409
92886
  doc.deleteIn(["release", "channel"]);
92410
92887
  doc.setIn(["release", "pin"], pin);
92411
92888
  const node = doc.getIn(["release", "pin"], true);
92412
- if (import_yaml19.isScalar(node)) {
92889
+ if (import_yaml20.isScalar(node)) {
92413
92890
  node.comment = ` ${now}: rolled by switchroom rollout`;
92414
92891
  }
92415
92892
  return String(doc);
92416
92893
  }
92417
92894
  function deleteReleasePinInConfig(yamlText) {
92418
- const doc = import_yaml19.parseDocument(yamlText);
92895
+ const doc = import_yaml20.parseDocument(yamlText);
92419
92896
  if (!doc.hasIn(["release", "pin"]))
92420
92897
  return yamlText;
92421
92898
  doc.deleteIn(["release", "pin"]);
92422
92899
  const rel = doc.getIn(["release"]);
92423
- if (import_yaml19.isMap(rel) && rel.items.length === 0 && !releaseBlockIsCommented(doc)) {
92900
+ if (import_yaml20.isMap(rel) && rel.items.length === 0 && !releaseBlockIsCommented(doc)) {
92424
92901
  doc.delete("release");
92425
92902
  }
92426
92903
  return String(doc);
92427
92904
  }
92428
92905
  function releaseBlockIsCommented(doc) {
92429
92906
  const contents = doc.contents;
92430
- if (!import_yaml19.isMap(contents))
92907
+ if (!import_yaml20.isMap(contents))
92431
92908
  return false;
92432
92909
  for (const item of contents.items) {
92433
- if (!import_yaml19.isPair(item))
92910
+ if (!import_yaml20.isPair(item))
92434
92911
  continue;
92435
92912
  const key = item.key;
92436
- if (!import_yaml19.isScalar(key) || key.value !== "release")
92913
+ if (!import_yaml20.isScalar(key) || key.value !== "release")
92437
92914
  continue;
92438
92915
  const nodes = [key, item.value];
92439
92916
  return nodes.some((n) => !!n && typeof n === "object" && (("commentBefore" in n) && !!n.commentBefore || ("comment" in n) && !!n.comment));
@@ -92446,7 +92923,7 @@ init_operator_uid();
92446
92923
  init_atomic();
92447
92924
 
92448
92925
  // src/cli/preflight-mounts.ts
92449
- import { statSync as statSync44 } from "node:fs";
92926
+ import { statSync as statSync45 } from "node:fs";
92450
92927
  function parseHostBindSources(composeText) {
92451
92928
  const out = [];
92452
92929
  const FILE_HINT = /\.(ya?ml|db|log|toml|json|token|id)$|\/\.vault-token$|machine-id$|localtime$|vault-auto-unlock$|\/webkite$/;
@@ -92473,7 +92950,7 @@ function parseHostBindSources(composeText) {
92473
92950
  return out;
92474
92951
  }
92475
92952
  function validateBindSources(composeText, deps = {}) {
92476
- const stat = deps.stat ?? ((p) => statSync44(p));
92953
+ const stat = deps.stat ?? ((p) => statSync45(p));
92477
92954
  const sources = parseHostBindSources(composeText);
92478
92955
  const issues = [];
92479
92956
  const seen = new Set;
@@ -93062,12 +93539,12 @@ function defaultPersistPin(configPath) {
93062
93539
  const after = setReleasePinInConfig(before, pin);
93063
93540
  if (after === before)
93064
93541
  return;
93065
- writeConfigFileSync(path7, after, statSync45(path7).mode & 511);
93542
+ writeConfigFileSync(path7, after, statSync46(path7).mode & 511);
93066
93543
  try {
93067
93544
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
93068
93545
  const uid = resolveOperatorUid();
93069
93546
  if (uid !== undefined)
93070
- chownSync8(path7, uid, uid);
93547
+ chownSync9(path7, uid, uid);
93071
93548
  }
93072
93549
  } catch {}
93073
93550
  };
@@ -93546,7 +94023,7 @@ function defaultStatusProbe(composePath) {
93546
94023
  } catch {}
93547
94024
  if (scriptPath) {
93548
94025
  try {
93549
- cliBuiltAt = new Date(statSync45(scriptPath).mtimeMs).toISOString();
94026
+ cliBuiltAt = new Date(statSync46(scriptPath).mtimeMs).toISOString();
93550
94027
  } catch {}
93551
94028
  let dir = dirname34(scriptPath);
93552
94029
  for (let i = 0;i < 8; i++) {
@@ -93822,7 +94299,7 @@ function registerUpdateCommand(program3) {
93822
94299
  // src/cli/rollout.ts
93823
94300
  init_helpers();
93824
94301
  import { spawnSync as spawnSync19 } from "node:child_process";
93825
- 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";
93826
94303
  import { homedir as homedir46 } from "node:os";
93827
94304
 
93828
94305
  // src/cli/rollout-pin-journal.ts
@@ -93832,7 +94309,7 @@ import {
93832
94309
  writeFileSync as writeFileSync32,
93833
94310
  renameSync as renameSync21,
93834
94311
  unlinkSync as unlinkSync15,
93835
- statSync as statSync46,
94312
+ statSync as statSync47,
93836
94313
  mkdirSync as mkdirSync47
93837
94314
  } from "node:fs";
93838
94315
  import { homedir as homedir45 } from "node:os";
@@ -93989,7 +94466,7 @@ function rollbackPinPersist(configPath, opts = {}) {
93989
94466
  const next = journal.priorPin ? setReleasePinInConfig(current, journal.priorPin) : deleteReleasePinInConfig(current);
93990
94467
  let mode = 384;
93991
94468
  try {
93992
- mode = statSync46(configPath).mode & 511;
94469
+ mode = statSync47(configPath).mode & 511;
93993
94470
  } catch {}
93994
94471
  if (opts.writeConfig) {
93995
94472
  opts.writeConfig(configPath, next, mode);
@@ -94568,7 +95045,7 @@ function createRolloutDeps(params) {
94568
95045
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
94569
95046
  const uid = resolveOperatorUid();
94570
95047
  if (uid !== undefined) {
94571
- chownSync9(path7, uid, uid);
95048
+ chownSync10(path7, uid, uid);
94572
95049
  } else if (hostdCtx) {
94573
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.
94574
95051
  `);
@@ -94628,7 +95105,7 @@ function createRolloutDeps(params) {
94628
95105
  if (after === before)
94629
95106
  return false;
94630
95107
  beginPinPersist(configPath, pin);
94631
- writeConfigPreservingOwnership(configPath, after, statSync47(configPath).mode & 511);
95108
+ writeConfigPreservingOwnership(configPath, after, statSync48(configPath).mode & 511);
94632
95109
  return true;
94633
95110
  },
94634
95111
  commitPin: () => {
@@ -95074,7 +95551,7 @@ init_merge();
95074
95551
  import {
95075
95552
  existsSync as existsSync84,
95076
95553
  readdirSync as readdirSync28,
95077
- statSync as statSync48,
95554
+ statSync as statSync49,
95078
95555
  unlinkSync as unlinkSync16
95079
95556
  } from "node:fs";
95080
95557
  import { join as join87 } from "node:path";
@@ -95097,7 +95574,7 @@ function collectSessionJsonl(claudeConfigDir) {
95097
95574
  const full = join87(dir, name);
95098
95575
  let st;
95099
95576
  try {
95100
- st = statSync48(full);
95577
+ st = statSync49(full);
95101
95578
  } catch {
95102
95579
  continue;
95103
95580
  }
@@ -95225,7 +95702,7 @@ import {
95225
95702
  readdirSync as readdirSync29,
95226
95703
  readFileSync as readFileSync79,
95227
95704
  renameSync as renameSync22,
95228
- statSync as statSync49,
95705
+ statSync as statSync50,
95229
95706
  unlinkSync as unlinkSync17,
95230
95707
  writeFileSync as writeFileSync33,
95231
95708
  writeSync as writeSync9
@@ -95428,7 +95905,7 @@ function sweepOrphanTmpFiles(stateDir) {
95428
95905
  continue;
95429
95906
  const tmpPath = join88(stateDir, entry);
95430
95907
  try {
95431
- const stat = statSync49(tmpPath);
95908
+ const stat = statSync50(tmpPath);
95432
95909
  if (stat.mtimeMs < cutoff) {
95433
95910
  unlinkSync17(tmpPath);
95434
95911
  }
@@ -95736,7 +96213,7 @@ import {
95736
96213
  } from "node:fs";
95737
96214
  import { dirname as dirname37, join as join89 } from "node:path";
95738
96215
  import { homedir as homedir47 } from "node:os";
95739
- import { execFileSync as execFileSync24 } from "node:child_process";
96216
+ import { execFileSync as execFileSync25 } from "node:child_process";
95740
96217
 
95741
96218
  class PythonEnvError extends Error {
95742
96219
  stderr;
@@ -95783,7 +96260,7 @@ function ensurePythonEnv(opts) {
95783
96260
  }
95784
96261
  mkdirSync49(dirname37(venvDir), { recursive: true });
95785
96262
  try {
95786
- execFileSync24(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
96263
+ execFileSync25(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
95787
96264
  } catch (err) {
95788
96265
  const e = err;
95789
96266
  throw new PythonEnvError(`Failed to create venv for skill "${skillName}" with ${hostPython}: ${e.message}`, e.stderr?.toString());
@@ -95795,7 +96272,7 @@ function ensurePythonEnv(opts) {
95795
96272
  delete childEnv.PIP_TARGET;
95796
96273
  delete childEnv.PIP_PREFIX;
95797
96274
  delete childEnv.PYTHONUSERBASE;
95798
- 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 });
95799
96276
  } catch (err) {
95800
96277
  const e = err;
95801
96278
  throw new PythonEnvError(`Failed to install requirements for skill "${skillName}": ${e.message}`, e.stderr?.toString());
@@ -95824,7 +96301,7 @@ import {
95824
96301
  } from "node:fs";
95825
96302
  import { dirname as dirname38, join as join90 } from "node:path";
95826
96303
  import { homedir as homedir48 } from "node:os";
95827
- import { execFileSync as execFileSync25 } from "node:child_process";
96304
+ import { execFileSync as execFileSync26 } from "node:child_process";
95828
96305
 
95829
96306
  class NodeEnvError extends Error {
95830
96307
  stderr;
@@ -95908,10 +96385,10 @@ function ensureNodeEnv(opts) {
95908
96385
  try {
95909
96386
  if (installer === "bun") {
95910
96387
  const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
95911
- execFileSync25("bun", args, { cwd: envDir, stdio: "pipe" });
96388
+ execFileSync26("bun", args, { cwd: envDir, stdio: "pipe" });
95912
96389
  } else {
95913
96390
  const args = copiedLockfile ? ["ci"] : ["install"];
95914
- execFileSync25("npm", args, { cwd: envDir, stdio: "pipe" });
96391
+ execFileSync26("npm", args, { cwd: envDir, stdio: "pipe" });
95915
96392
  }
95916
96393
  } catch (err) {
95917
96394
  const e = err;
@@ -96993,7 +97470,7 @@ function registerSoulCommand(program3) {
96993
97470
  // src/cli/debug.ts
96994
97471
  init_helpers();
96995
97472
  init_loader();
96996
- 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";
96997
97474
  import { resolve as resolve53, join as join93 } from "node:path";
96998
97475
  import { createHash as createHash20 } from "node:crypto";
96999
97476
  init_merge();
@@ -97032,7 +97509,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
97032
97509
  const transcriptPath = join93(projectPath, "transcript.jsonl");
97033
97510
  if (!existsSync91(transcriptPath))
97034
97511
  continue;
97035
- const stat3 = statSync50(transcriptPath);
97512
+ const stat3 = statSync51(transcriptPath);
97036
97513
  if (!latest || stat3.mtimeMs > latest.mtime) {
97037
97514
  latest = { path: transcriptPath, mtime: stat3.mtimeMs };
97038
97515
  }
@@ -97267,7 +97744,7 @@ function registerDebugCommand(program3) {
97267
97744
  init_source();
97268
97745
 
97269
97746
  // src/worktree/claim.ts
97270
- import { execFileSync as execFileSync26 } from "node:child_process";
97747
+ import { execFileSync as execFileSync27 } from "node:child_process";
97271
97748
  import { closeSync as closeSync17, mkdirSync as mkdirSync52, openSync as openSync17, existsSync as existsSync93, unlinkSync as unlinkSync19 } from "node:fs";
97272
97749
  import { join as join95, resolve as resolve55 } from "node:path";
97273
97750
  import { homedir as homedir51 } from "node:os";
@@ -97436,7 +97913,7 @@ async function claimWorktree(input, codeRepos) {
97436
97913
  releaseLock();
97437
97914
  }
97438
97915
  try {
97439
- execFileSync26("git", ["worktree", "add", "-b", branch, worktreePath], {
97916
+ execFileSync27("git", ["worktree", "add", "-b", branch, worktreePath], {
97440
97917
  cwd: repoPath,
97441
97918
  stdio: "pipe"
97442
97919
  });
@@ -97449,7 +97926,7 @@ async function claimWorktree(input, codeRepos) {
97449
97926
  }
97450
97927
 
97451
97928
  // src/worktree/release.ts
97452
- import { execFileSync as execFileSync27 } from "node:child_process";
97929
+ import { execFileSync as execFileSync28 } from "node:child_process";
97453
97930
  import { existsSync as existsSync94 } from "node:fs";
97454
97931
  function releaseWorktree(input) {
97455
97932
  const { id } = input;
@@ -97460,7 +97937,7 @@ function releaseWorktree(input) {
97460
97937
  let gitSuccess = true;
97461
97938
  if (existsSync94(record2.path)) {
97462
97939
  try {
97463
- execFileSync27("git", ["worktree", "remove", "--force", record2.path], {
97940
+ execFileSync28("git", ["worktree", "remove", "--force", record2.path], {
97464
97941
  cwd: record2.repo,
97465
97942
  stdio: "pipe"
97466
97943
  });
@@ -97496,7 +97973,7 @@ function listWorktrees() {
97496
97973
  }
97497
97974
 
97498
97975
  // src/worktree/reaper.ts
97499
- import { execFileSync as execFileSync28 } from "node:child_process";
97976
+ import { execFileSync as execFileSync29 } from "node:child_process";
97500
97977
  import { existsSync as existsSync95 } from "node:fs";
97501
97978
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
97502
97979
  function reapSkipReasonText(action) {
@@ -97514,7 +97991,7 @@ function reapSkipReasonText(action) {
97514
97991
  function probePathInUse(path10) {
97515
97992
  let probeRan = false;
97516
97993
  try {
97517
- execFileSync28("fuser", [path10], { stdio: "pipe" });
97994
+ execFileSync29("fuser", [path10], { stdio: "pipe" });
97518
97995
  return "in-use";
97519
97996
  } catch (err) {
97520
97997
  if (err.code !== "ENOENT") {
@@ -97522,7 +97999,7 @@ function probePathInUse(path10) {
97522
97999
  }
97523
98000
  }
97524
98001
  try {
97525
- const out = execFileSync28("lsof", ["-t", path10], {
98002
+ const out = execFileSync29("lsof", ["-t", path10], {
97526
98003
  stdio: ["ignore", "pipe", "ignore"]
97527
98004
  }).toString().trim();
97528
98005
  probeRan = true;
@@ -97537,7 +98014,7 @@ function probePathInUse(path10) {
97537
98014
  }
97538
98015
  function hasUncommittedChanges(_repoPath, worktreePath) {
97539
98016
  try {
97540
- const out = execFileSync28("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
98017
+ const out = execFileSync29("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
97541
98018
  return out.trim().length > 0;
97542
98019
  } catch {
97543
98020
  return true;
@@ -97545,7 +98022,7 @@ function hasUncommittedChanges(_repoPath, worktreePath) {
97545
98022
  }
97546
98023
  function defaultRemoveWorktree(repoPath, worktreePath) {
97547
98024
  try {
97548
- execFileSync28("git", ["worktree", "remove", "--force", worktreePath], {
98025
+ execFileSync29("git", ["worktree", "remove", "--force", worktreePath], {
97549
98026
  cwd: repoPath,
97550
98027
  stdio: "pipe"
97551
98028
  });
@@ -97637,12 +98114,12 @@ function runReaper(nowMs, deps = {}) {
97637
98114
  }
97638
98115
 
97639
98116
  // src/worktree/gc.ts
97640
- import { execFileSync as execFileSync29 } from "node:child_process";
98117
+ import { execFileSync as execFileSync30 } from "node:child_process";
97641
98118
  import {
97642
98119
  existsSync as existsSync96,
97643
98120
  readFileSync as readFileSync85,
97644
98121
  readdirSync as readdirSync32,
97645
- statSync as statSync51,
98122
+ statSync as statSync52,
97646
98123
  renameSync as renameSync24,
97647
98124
  mkdirSync as mkdirSync53,
97648
98125
  rmSync as rmSync17
@@ -97730,7 +98207,7 @@ function isEphemeralPath(path10) {
97730
98207
  }
97731
98208
  return false;
97732
98209
  }
97733
- 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();
97734
98211
  function defaultPrSignal(repo, branch, exec) {
97735
98212
  try {
97736
98213
  const out = exec("gh", [
@@ -97769,7 +98246,7 @@ function planGc(roots, deps = {}) {
97769
98246
  const exists = deps.existsSync ?? existsSync96;
97770
98247
  const readDir = deps.readDir ?? ((p) => readdirSync32(p));
97771
98248
  const readFile4 = deps.readFile ?? ((p) => readFileSync85(p, "utf8"));
97772
- const stat3 = deps.stat ?? ((p) => statSync51(p));
98249
+ const stat3 = deps.stat ?? ((p) => statSync52(p));
97773
98250
  const exec = deps.exec ?? defaultExec3;
97774
98251
  const prSignal = deps.prSignal ?? ((repo, branch) => defaultPrSignal(repo, branch, exec));
97775
98252
  const stamp = deps.dateStamp ?? "undated";
@@ -97963,7 +98440,7 @@ function listTrashEntries(nowMs, deps = {}) {
97963
98440
  const p = join96(stampDir, name);
97964
98441
  let mtimeMs = nowMs;
97965
98442
  try {
97966
- mtimeMs = statSync51(p).mtimeMs;
98443
+ mtimeMs = statSync52(p).mtimeMs;
97967
98444
  } catch {}
97968
98445
  out.push({ path: p, ageDays: (nowMs - mtimeMs) / 86400000 });
97969
98446
  }
@@ -99238,6 +99715,54 @@ function redactToolCallParams(params) {
99238
99715
  }
99239
99716
  return { ...called, arguments: redactToolArguments(called.arguments) };
99240
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
+ }
99241
99766
  function buildFallbackToolsList() {
99242
99767
  const tools = Object.entries(FALLBACK_TOOL_TABLE).map(([name, [required, props]]) => ({
99243
99768
  name,
@@ -99564,7 +100089,11 @@ class HindsightShim {
99564
100089
  if (called?.name && SYNTHESIZED_TOOL_NAMES.includes(called.name)) {
99565
100090
  return this.synthesizedCall(called.name, called.arguments);
99566
100091
  }
99567
- 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);
99568
100097
  try {
99569
100098
  const res = await this.upstream.request("tools/call", forwarded, this.opts.toolsCallTimeoutMs ?? TOOLS_CALL_TIMEOUT_MS);
99570
100099
  this.notifyIfRecovered();
@@ -99667,7 +100196,7 @@ function registerHindsightMcpShimCommand(program3) {
99667
100196
 
99668
100197
  // src/cli/deliver-file.ts
99669
100198
  init_client2();
99670
- import { readFileSync as readFileSync87, statSync as statSync52 } from "node:fs";
100199
+ import { readFileSync as readFileSync87, statSync as statSync53 } from "node:fs";
99671
100200
  import { basename as basename13 } from "node:path";
99672
100201
 
99673
100202
  // src/delivery/onedrive.ts
@@ -100006,7 +100535,7 @@ async function defaultResolveProvider() {
100006
100535
  }
100007
100536
  async function runDeliverFile(localPath, deps = {}) {
100008
100537
  const agentName = safeAgentName(deps.agentName ?? process.env.SWITCHROOM_AGENT_NAME);
100009
- const sizeOf = deps.fileSize ?? ((p) => statSync52(p).size);
100538
+ const sizeOf = deps.fileSize ?? ((p) => statSync53(p).size);
100010
100539
  const read = deps.readFile ?? ((p) => new Uint8Array(readFileSync87(p)));
100011
100540
  const resolveProvider = deps.resolveProvider ?? defaultResolveProvider;
100012
100541
  let size;
@@ -100702,7 +101231,7 @@ init_agent_config();
100702
101231
 
100703
101232
  // src/cli/agent-config-write.ts
100704
101233
  init_agent_config();
100705
- var import_yaml21 = __toESM(require_dist(), 1);
101234
+ var import_yaml22 = __toESM(require_dist(), 1);
100706
101235
 
100707
101236
  // src/config/overlay-writer.ts
100708
101237
  init_paths();
@@ -100715,7 +101244,7 @@ import {
100715
101244
  readdirSync as readdirSync35,
100716
101245
  readFileSync as readFileSync89,
100717
101246
  renameSync as renameSync26,
100718
- statSync as statSync53,
101247
+ statSync as statSync54,
100719
101248
  unlinkSync as unlinkSync20,
100720
101249
  writeSync as writeSync10
100721
101250
  } from "node:fs";
@@ -100759,7 +101288,7 @@ function withAgentLock(paths, fn) {
100759
101288
  if (e.code !== "EEXIST")
100760
101289
  throw err;
100761
101290
  try {
100762
- const age = Date.now() - statSync53(paths.lockPath).mtimeMs;
101291
+ const age = Date.now() - statSync54(paths.lockPath).mtimeMs;
100763
101292
  if (age > 30000) {
100764
101293
  unlinkSync20(paths.lockPath);
100765
101294
  continue;
@@ -100903,7 +101432,7 @@ init_overlay_schema();
100903
101432
  init_schema();
100904
101433
  init_cron_unit_name();
100905
101434
  init_lifecycle();
100906
- var import_yaml20 = __toESM(require_dist(), 1);
101435
+ var import_yaml21 = __toESM(require_dist(), 1);
100907
101436
 
100908
101437
  // src/scheduler/cron-gap.ts
100909
101438
  function expandMinuteTerm(term) {
@@ -100993,7 +101522,7 @@ function violatesMinInterval(cron) {
100993
101522
  function dryRunReconcile(input) {
100994
101523
  let parsed;
100995
101524
  try {
100996
- parsed = import_yaml20.parse(input.yamlText);
101525
+ parsed = import_yaml21.parse(input.yamlText);
100997
101526
  } catch (err) {
100998
101527
  return {
100999
101528
  ok: false,
@@ -101192,7 +101721,7 @@ function denyPendingScheduleEntry(opts) {
101192
101721
 
101193
101722
  // src/cli/agent-config-write.ts
101194
101723
  import { existsSync as existsSync101, readFileSync as readFileSync91 } from "node:fs";
101195
- import { execFileSync as execFileSync30 } from "node:child_process";
101724
+ import { execFileSync as execFileSync31 } from "node:child_process";
101196
101725
 
101197
101726
  // src/scheduler/schedule-report.ts
101198
101727
  var TIER_WEIGHT = { poll: 0, action: 0, cheap: 1, main: 5 };
@@ -101341,7 +101870,7 @@ function parseOverlayCronAndName(e) {
101341
101870
  if (headerMatch)
101342
101871
  name = headerMatch[1];
101343
101872
  try {
101344
- const doc = import_yaml21.parse(e.raw);
101873
+ const doc = import_yaml22.parse(e.raw);
101345
101874
  const first = doc?.schedule?.[0];
101346
101875
  if (first) {
101347
101876
  if (typeof first.cron === "string")
@@ -101377,7 +101906,7 @@ function scheduleAdd(opts) {
101377
101906
  ]
101378
101907
  };
101379
101908
  const yamlText = (() => {
101380
- const body = import_yaml21.stringify(doc);
101909
+ const body = import_yaml22.stringify(doc);
101381
101910
  const header = opts.name ? `# name: ${opts.name}
101382
101911
  ` : "";
101383
101912
  return header + body;
@@ -101513,7 +102042,7 @@ function scheduleAddOrStage(opts) {
101513
102042
  ]
101514
102043
  };
101515
102044
  const yamlText = (opts.name ? `# name: ${opts.name}
101516
- ` : "") + import_yaml21.stringify(doc);
102045
+ ` : "") + import_yaml22.stringify(doc);
101517
102046
  const summary = (() => {
101518
102047
  const parts = [`cron=${opts.cronExpr}`];
101519
102048
  if (opts.secrets?.length)
@@ -101563,7 +102092,7 @@ function scheduleRemove(opts) {
101563
102092
  break;
101564
102093
  }
101565
102094
  try {
101566
- const parsed = import_yaml21.parse(e.raw);
102095
+ const parsed = import_yaml22.parse(e.raw);
101567
102096
  if (parsed && parsed.name === opts.name) {
101568
102097
  match = e;
101569
102098
  break;
@@ -101788,7 +102317,7 @@ function registerAgentConfigWriteCommands(program3) {
101788
102317
  blob = existsSync101(opts.jsonl) ? readFileSync91(opts.jsonl, "utf-8") : "";
101789
102318
  } else {
101790
102319
  try {
101791
- blob = execFileSync30("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
102320
+ blob = execFileSync31("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
101792
102321
  encoding: "utf-8",
101793
102322
  stdio: ["ignore", "pipe", "ignore"]
101794
102323
  });
@@ -101816,11 +102345,11 @@ function registerAgentConfigWriteCommands(program3) {
101816
102345
  }
101817
102346
 
101818
102347
  // src/cli/agent-config-skill-write.ts
101819
- var import_yaml22 = __toESM(require_dist(), 1);
102348
+ var import_yaml23 = __toESM(require_dist(), 1);
101820
102349
  import { existsSync as existsSync102 } from "node:fs";
101821
102350
  init_reconcile_default_skills();
101822
102351
  init_agent_config();
101823
- var import_yaml23 = __toESM(require_dist(), 1);
102352
+ var import_yaml24 = __toESM(require_dist(), 1);
101824
102353
  import { join as join103 } from "node:path";
101825
102354
  var MAX_SKILLS_PER_AGENT = 20;
101826
102355
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
@@ -101870,7 +102399,7 @@ function countCurrentSkills(agent, opts) {
101870
102399
  let total = 0;
101871
102400
  for (const e of entries) {
101872
102401
  try {
101873
- const doc = import_yaml23.parse(e.raw);
102402
+ const doc = import_yaml24.parse(e.raw);
101874
102403
  total += (doc?.skills ?? []).length;
101875
102404
  } catch {}
101876
102405
  }
@@ -101900,7 +102429,7 @@ function skillInstall(opts) {
101900
102429
  if (!existsSync102(skillPath)) {
101901
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.`);
101902
102431
  }
101903
- const yamlText = import_yaml22.stringify({ skills: [skillName] });
102432
+ const yamlText = import_yaml23.stringify({ skills: [skillName] });
101904
102433
  let path10;
101905
102434
  try {
101906
102435
  path10 = writeSkillsOverlayEntry(agent, slug, yamlText, { root: opts.root });
@@ -102060,6 +102589,7 @@ function registerAgentConfigSkillWriteCommands(program3) {
102060
102589
 
102061
102590
  // src/cli/skill.ts
102062
102591
  import {
102592
+ chmodSync as chmodSync17,
102063
102593
  closeSync as closeSync20,
102064
102594
  existsSync as existsSync103,
102065
102595
  lstatSync as lstatSync12,
@@ -102071,7 +102601,7 @@ import {
102071
102601
  realpathSync as realpathSync7,
102072
102602
  renameSync as renameSync28,
102073
102603
  rmSync as rmSync19,
102074
- statSync as statSync54,
102604
+ statSync as statSync55,
102075
102605
  writeFileSync as writeFileSync43
102076
102606
  } from "node:fs";
102077
102607
  import { tmpdir as tmpdir6, homedir as homedir54 } from "node:os";
@@ -102079,7 +102609,7 @@ import { dirname as dirname41, join as join104, relative as relative4, resolve a
102079
102609
  import { spawnSync as spawnSync21 } from "node:child_process";
102080
102610
 
102081
102611
  // src/cli/skill-common.ts
102082
- var import_yaml24 = __toESM(require_dist(), 1);
102612
+ var import_yaml25 = __toESM(require_dist(), 1);
102083
102613
  var MAX_SKILLS_PER_AGENT2 = 20;
102084
102614
  var MAX_FILE_BYTES = 256 * 1024;
102085
102615
  var MAX_SKILL_BYTES = 2 * 1024 * 1024;
@@ -102184,7 +102714,7 @@ function validateSkillMd(content, expectedName) {
102184
102714
  }
102185
102715
  let parsed;
102186
102716
  try {
102187
- parsed = import_yaml24.parse(fmText);
102717
+ parsed = import_yaml25.parse(fmText);
102188
102718
  } catch (e) {
102189
102719
  return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter is not valid YAML: ${e.message}`);
102190
102720
  }
@@ -102342,7 +102872,7 @@ function isTarballPath(p) {
102342
102872
  }
102343
102873
  function loadFromDir(dir) {
102344
102874
  const abs = realpathSync7(dir);
102345
- if (!statSync54(abs).isDirectory()) {
102875
+ if (!statSync55(abs).isDirectory()) {
102346
102876
  fail4(`--from path is not a directory: ${dir}`);
102347
102877
  }
102348
102878
  const files = {};
@@ -102542,6 +103072,7 @@ function writePayload(poolDir, name, files) {
102542
103072
  fail4(`refusing to overwrite symlink at ${target}; investigate manually`);
102543
103073
  }
102544
103074
  const staging = mkdtempSync5(join104(poolDir, `.skill-apply-stage-${name}-`));
103075
+ chmodSync17(staging, 493);
102545
103076
  let oldRename = null;
102546
103077
  try {
102547
103078
  for (const [path10, content] of Object.entries(files)) {
@@ -102602,7 +103133,7 @@ function registerSkillCommand(program3) {
102602
103133
  if (!existsSync103(fromPath)) {
102603
103134
  fail4(`--from path does not exist: ${opts.from}`);
102604
103135
  }
102605
- const st = statSync54(fromPath);
103136
+ const st = statSync55(fromPath);
102606
103137
  if (st.isDirectory()) {
102607
103138
  files = loadFromDir(fromPath);
102608
103139
  } else if (isTarballPath(fromPath)) {
@@ -102654,6 +103185,7 @@ function sumBytes(files) {
102654
103185
  // src/cli/skill-personal.ts
102655
103186
  init_esm();
102656
103187
  import {
103188
+ chmodSync as chmodSync18,
102657
103189
  closeSync as closeSync21,
102658
103190
  existsSync as existsSync104,
102659
103191
  lstatSync as lstatSync13,
@@ -102664,7 +103196,7 @@ import {
102664
103196
  readdirSync as readdirSync38,
102665
103197
  renameSync as renameSync29,
102666
103198
  rmSync as rmSync20,
102667
- statSync as statSync55,
103199
+ statSync as statSync56,
102668
103200
  utimesSync,
102669
103201
  writeFileSync as writeFileSync44
102670
103202
  } from "node:fs";
@@ -102748,6 +103280,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
102748
103280
  }
102749
103281
  };
102750
103282
  walk2(liveSkillDir, staging);
103283
+ chmodSync18(staging, 493);
102751
103284
  if (existsSync104(dest)) {
102752
103285
  const prior = join105(configSkillsRoot, `.${name}-prior-${Date.now()}`);
102753
103286
  renameSync29(dest, prior);
@@ -102820,7 +103353,7 @@ function readStdinSync2() {
102820
103353
  }
102821
103354
  function loadFromDir2(dir) {
102822
103355
  const abs = resolve59(dir);
102823
- if (!statSync55(abs).isDirectory()) {
103356
+ if (!statSync56(abs).isDirectory()) {
102824
103357
  fail5(`--from path is not a directory: ${dir}`);
102825
103358
  }
102826
103359
  const files = {};
@@ -102906,7 +103439,7 @@ function sweepTrash(agentsRoot, agent) {
102906
103439
  continue;
102907
103440
  const entPath = join105(trash, ent.name);
102908
103441
  try {
102909
- const st = statSync55(entPath);
103442
+ const st = statSync56(entPath);
102910
103443
  if (now - st.mtimeMs > TRASH_TTL_MS) {
102911
103444
  rmSync20(entPath, { recursive: true, force: true });
102912
103445
  }
@@ -102926,6 +103459,7 @@ function writePersonalSkill(targetDir, files) {
102926
103459
  }
102927
103460
  mkdirSync61(dirname42(targetDir), { recursive: true, mode: 493 });
102928
103461
  const staging = mkdtempSync6(join105(dirname42(targetDir), `.skill-personal-stage-`));
103462
+ chmodSync18(staging, 493);
102929
103463
  let oldRename = null;
102930
103464
  try {
102931
103465
  for (const [path10, content] of Object.entries(files)) {
@@ -103031,7 +103565,7 @@ function loadFiles(opts) {
103031
103565
  if (!existsSync104(p)) {
103032
103566
  fail5(`--from path does not exist: ${opts.from}`);
103033
103567
  }
103034
- const st = statSync55(p);
103568
+ const st = statSync56(p);
103035
103569
  if (st.isDirectory()) {
103036
103570
  return loadFromDir2(p);
103037
103571
  }
@@ -103252,7 +103786,7 @@ function listPersonalAction(opts) {
103252
103786
  if (e.isFile()) {
103253
103787
  fileCount += 1;
103254
103788
  try {
103255
- totalBytes += statSync55(join105(sub, e.name)).size;
103789
+ totalBytes += statSync56(join105(sub, e.name)).size;
103256
103790
  } catch {}
103257
103791
  } else if (e.isDirectory()) {
103258
103792
  walk2(join105(sub, e.name));
@@ -103369,8 +103903,8 @@ function registerSelfImproveProposeSkillCommand(program3) {
103369
103903
  // src/cli/skill-search.ts
103370
103904
  init_esm();
103371
103905
  init_helpers();
103372
- var import_yaml25 = __toESM(require_dist(), 1);
103373
- 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";
103374
103908
  import { homedir as homedir57 } from "node:os";
103375
103909
  import { join as join107, resolve as resolve60 } from "node:path";
103376
103910
  var PERSONAL_PREFIX2 = "personal-";
@@ -103409,7 +103943,7 @@ function readSkillFrontmatter(skillDir) {
103409
103943
  const fmText = rest.slice(0, endIdx);
103410
103944
  let parsed;
103411
103945
  try {
103412
- parsed = import_yaml25.parse(fmText);
103946
+ parsed = import_yaml26.parse(fmText);
103413
103947
  } catch (e) {
103414
103948
  return { error: `yaml parse: ${e.message}` };
103415
103949
  }
@@ -103421,7 +103955,7 @@ function readSkillFrontmatter(skillDir) {
103421
103955
  function statSkillMd(skillDir) {
103422
103956
  const mdPath = join107(skillDir, "SKILL.md");
103423
103957
  try {
103424
- const st = statSync56(mdPath);
103958
+ const st = statSync57(mdPath);
103425
103959
  return { size: st.size, mtime: st.mtime.toISOString() };
103426
103960
  } catch {
103427
103961
  return null;
@@ -103445,7 +103979,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
103445
103979
  continue;
103446
103980
  const dirPath = join107(skillsDir, ent);
103447
103981
  try {
103448
- if (!statSync56(dirPath).isDirectory())
103982
+ if (!statSync57(dirPath).isDirectory())
103449
103983
  continue;
103450
103984
  } catch {
103451
103985
  continue;
@@ -103485,7 +104019,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
103485
104019
  continue;
103486
104020
  const dirPath = join107(sharedRoot, ent);
103487
104021
  try {
103488
- if (!statSync56(dirPath).isDirectory())
104022
+ if (!statSync57(dirPath).isDirectory())
103489
104023
  continue;
103490
104024
  } catch {
103491
104025
  continue;
@@ -103521,7 +104055,7 @@ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
103521
104055
  continue;
103522
104056
  const dirPath = join107(bundledRoot, ent);
103523
104057
  try {
103524
- if (!statSync56(dirPath).isDirectory())
104058
+ if (!statSync57(dirPath).isDirectory())
103525
104059
  continue;
103526
104060
  } catch {
103527
104061
  continue;
@@ -103669,7 +104203,7 @@ import {
103669
104203
  mkdirSync as mkdirSync62,
103670
104204
  readdirSync as readdirSync40,
103671
104205
  writeFileSync as writeFileSync45,
103672
- statSync as statSync57,
104206
+ statSync as statSync58,
103673
104207
  lstatSync as lstatSync14,
103674
104208
  realpathSync as realpathSync8,
103675
104209
  copyFileSync as copyFileSync14
@@ -103781,6 +104315,10 @@ services:
103781
104315
  # HAProxy writes its runtime state/pidfile under /run; read_only rootfs
103782
104316
  # otherwise makes it crash-loop on boot.
103783
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
103784
104322
  cap_drop:
103785
104323
  - ALL
103786
104324
  cap_add:
@@ -104182,7 +104720,7 @@ function doStatus() {
104182
104720
  continue;
104183
104721
  const sockPath = join108(dir, name, "sock");
104184
104722
  if (existsSync107(sockPath)) {
104185
- const st = statSync57(sockPath);
104723
+ const st = statSync58(sockPath);
104186
104724
  if ((st.mode & 61440) === 49152) {
104187
104725
  entries.push(`${name} \u2192 ${sockPath}`);
104188
104726
  }
@@ -104273,7 +104811,7 @@ init_source();
104273
104811
  init_helpers();
104274
104812
  init_operator_uid();
104275
104813
  init_compose();
104276
- 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";
104277
104815
  import { homedir as homedir59 } from "node:os";
104278
104816
  import { join as join109 } from "node:path";
104279
104817
  import { spawnSync as spawnSync26 } from "node:child_process";
@@ -104453,10 +104991,10 @@ async function doInstall2(opts, program3) {
104453
104991
  writeFileSync46(composePath, yaml, "utf8");
104454
104992
  try {
104455
104993
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
104456
- chownSync10(dir, operatorUid, operatorUid);
104457
- chownSync10(composePath, operatorUid, operatorUid);
104994
+ chownSync11(dir, operatorUid, operatorUid);
104995
+ chownSync11(composePath, operatorUid, operatorUid);
104458
104996
  if (bak)
104459
- chownSync10(bak, operatorUid, operatorUid);
104997
+ chownSync11(bak, operatorUid, operatorUid);
104460
104998
  }
104461
104999
  } catch {}
104462
105000
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
@@ -105073,7 +105611,7 @@ init_install_cron();
105073
105611
 
105074
105612
  // src/hindsight-watch/probe.ts
105075
105613
  import { spawnSync as spawnSync28 } from "node:child_process";
105076
- 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";
105077
105615
  import { homedir as homedir62 } from "node:os";
105078
105616
  import { resolve as resolve62 } from "node:path";
105079
105617
 
@@ -105201,7 +105739,7 @@ function readLlmSignals(series) {
105201
105739
  }
105202
105740
 
105203
105741
  // src/hindsight-watch/recall-log.ts
105204
- 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";
105205
105743
  import { join as join111 } from "node:path";
105206
105744
  var RECALL_WINDOW_ROWS = 200;
105207
105745
  var RECALL_MAX_TAIL_BYTES = 1024 * 1024;
@@ -105216,7 +105754,7 @@ function readRecallLogTail2(path10, windowRows = RECALL_WINDOW_ROWS) {
105216
105754
  let truncatedHead = false;
105217
105755
  let fd;
105218
105756
  try {
105219
- const size = statSync58(path10).size;
105757
+ const size = statSync59(path10).size;
105220
105758
  const start = Math.max(0, size - RECALL_MAX_TAIL_BYTES);
105221
105759
  truncatedHead = start > 0;
105222
105760
  const length = size - start;
@@ -105448,7 +105986,7 @@ function readDropCount(hindsightDir) {
105448
105986
  const path10 = resolve62(hindsightDir, "pending-drops.json");
105449
105987
  let parsed;
105450
105988
  try {
105451
- if (statSync59(path10).size > DROPS_LEDGER_MAX_BYTES)
105989
+ if (statSync60(path10).size > DROPS_LEDGER_MAX_BYTES)
105452
105990
  return 0;
105453
105991
  parsed = JSON.parse(readFileSync100(path10, "utf8"));
105454
105992
  } catch {
@@ -105473,6 +106011,7 @@ function probeSpool(agentsDir = resolveAgentsDir2()) {
105473
106011
  let dead = 0;
105474
106012
  let evicted = 0;
105475
106013
  let drops = 0;
106014
+ let duplicates = 0;
105476
106015
  let agents = 0;
105477
106016
  for (const name of names) {
105478
106017
  const hindsightDir = resolve62(agentsDir, name, "home", ".hindsight");
@@ -105501,9 +106040,15 @@ function probeSpool(agentsDir = resolveAgentsDir2()) {
105501
106040
  dead++;
105502
106041
  }
105503
106042
  } catch {}
106043
+ try {
106044
+ for (const f of readdirSync44(resolve62(hindsightDir, "pending-duplicate"))) {
106045
+ if (f.endsWith(".json"))
106046
+ duplicates++;
106047
+ }
106048
+ } catch {}
105504
106049
  drops += readDropCount(hindsightDir);
105505
106050
  }
105506
- return { pending, dead, evicted, drops, agents };
106051
+ return { pending, dead, evicted, drops, duplicates, agents };
105507
106052
  }
105508
106053
  var PSQL_BOOTSTRAP_SH = `
105509
106054
  set -e
@@ -105680,6 +106225,7 @@ async function probeOnce(opts = {}) {
105680
106225
  dead: spool.dead,
105681
106226
  evicted: spool.evicted,
105682
106227
  drops: spool.drops,
106228
+ duplicates: spool.duplicates,
105683
106229
  restartCount: container.restartCount,
105684
106230
  startedAt: container.startedAt,
105685
106231
  health: container.health,
@@ -105786,7 +106332,9 @@ function evaluateRetainLoss(ring) {
105786
106332
  ["dropped retain", last.drops - prev.drops]
105787
106333
  ];
105788
106334
  const risen = channels.filter(([, added]) => added > 0);
105789
- 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}`;
105790
106338
  if (risen.length > 0) {
105791
106339
  return {
105792
106340
  signal,
@@ -105798,7 +106346,8 @@ function evaluateRetainLoss(ring) {
105798
106346
  dropsAdded: last.drops - prev.drops,
105799
106347
  dead: last.dead,
105800
106348
  evicted: last.evicted,
105801
- drops: last.drops
106349
+ drops: last.drops,
106350
+ duplicates: dup ?? 0
105802
106351
  }
105803
106352
  };
105804
106353
  }
@@ -105812,7 +106361,8 @@ function evaluateRetainLoss(ring) {
105812
106361
  dropsAdded: 0,
105813
106362
  dead: last.dead,
105814
106363
  evicted: last.evicted,
105815
- drops: last.drops
106364
+ drops: last.drops,
106365
+ duplicates: dup ?? 0
105816
106366
  }
105817
106367
  };
105818
106368
  }
@@ -106002,7 +106552,7 @@ function evaluateConsolidationQueueAge(ring) {
106002
106552
  return {
106003
106553
  signal,
106004
106554
  state: "ok",
106005
- detail: "consolidation queue empty",
106555
+ detail: "no pending/processing operations (failures are not counted here \u2014 " + "see consolidation-failure-streak)",
106006
106556
  measured: { pending: 0, oldestAgeS: 0 }
106007
106557
  };
106008
106558
  }
@@ -106482,8 +107032,27 @@ function runInstallCron(cronUser) {
106482
107032
  `) + " This path needs root; re-run under sudo with `--cron-user <operator>`.\n");
106483
107033
  return 1;
106484
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
+ }
106485
107052
  const verb = res.status === "installed" ? "installed" : "already up to date";
106486
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}
106487
107056
  ` + ` Verify with: switchroom doctor | switchroom hindsight-watch --dry-run
106488
107057
  `);
106489
107058
  return 0;