@lincoln504/pi-research 0.1.13 → 1.0.1

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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1195 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Planning Utilities
3
+ *
4
+ * Helper functions for planning service
5
+ */
6
+
7
+ import { logger } from '../logger.ts';
8
+ import { Value } from 'typebox/value';
9
+ import { extractJson } from '../utils/json-utils.ts';
10
+ import type { ResearchPlan, ResearcherConfig } from './interfaces/planning-interfaces.ts';
11
+ import { ResearchPlanSchema } from './interfaces/planning-interfaces.ts';
12
+ import {
13
+ MAX_TEAM_SIZE_LEVEL_1,
14
+ MAX_TEAM_SIZE_LEVEL_2,
15
+ MAX_TEAM_SIZE_LEVEL_3,
16
+ MAX_ROUNDS_LEVEL_1,
17
+ MAX_ROUNDS_LEVEL_2,
18
+ MAX_ROUNDS_LEVEL_3,
19
+ MAX_QUERIES_PER_RESEARCHER_LEVEL_1,
20
+ MAX_QUERIES_PER_RESEARCHER_LEVEL_2,
21
+ MAX_QUERIES_PER_RESEARCHER_LEVEL_3,
22
+ } from '../constants.ts';
23
+
24
+ /**
25
+ * Get the max team size for a complexity level
26
+ */
27
+ export function getTeamSize(complexity: 1 | 2 | 3): number {
28
+ return complexity === 1 ? MAX_TEAM_SIZE_LEVEL_1
29
+ : complexity === 2 ? MAX_TEAM_SIZE_LEVEL_2
30
+ : MAX_TEAM_SIZE_LEVEL_3;
31
+ }
32
+
33
+ /**
34
+ * Get the query budget per researcher for a complexity level
35
+ */
36
+ export function getQueryBudget(complexity: 1 | 2 | 3): number {
37
+ return complexity === 1 ? MAX_QUERIES_PER_RESEARCHER_LEVEL_1
38
+ : complexity === 2 ? MAX_QUERIES_PER_RESEARCHER_LEVEL_2
39
+ : MAX_QUERIES_PER_RESEARCHER_LEVEL_3;
40
+ }
41
+
42
+ /**
43
+ * Get the max rounds for a complexity level
44
+ */
45
+ export function getMaxRounds(complexity: 1 | 2 | 3): number {
46
+ return complexity === 1 ? MAX_ROUNDS_LEVEL_1
47
+ : complexity === 2 ? MAX_ROUNDS_LEVEL_2
48
+ : MAX_ROUNDS_LEVEL_3;
49
+ }
50
+
51
+ /**
52
+ * Get complexity-specific guidance for the coordinator
53
+ */
54
+ export function getComplexityGuidance(complexity: 1 | 2 | 3, maxTeamSize: number, queryBudget: number): string {
55
+ if (complexity === 1) {
56
+ return '**Complexity: Level 1 (Normal)**. Conduct a thorough, well-rounded investigation of the topic covering the primary angles with adequate citations. **Plan ONLY Round 1** — a single comprehensive round. **Default to a single researcher** for most Level 1 topics — one focused researcher can handle the majority of normal-complexity queries efficiently. Use 2 researchers only when the topic clearly spans two distinct, non-overlapping domains that benefit from parallel investigation. This is a ONE-SHOT effort: plan Round 1 as if it is the only round, because it should be. Only Level 2 or Level 3 research should involve multiple rounds.';
57
+ } else if (complexity === 2) {
58
+ return `**Complexity: Level 2 (Deep)**. **ONLY for HIGH/EXTREME research needs.** Level 2 should be reserved for topics that clearly require deeper investigation than a single comprehensive round can provide. **Plan ONLY Round 1** — a thorough, well-structured first round. Subsequent rounds are reactive and only delegated when Round 1 findings reveal clear gaps that warrant deeper or broader coverage. Do not pre-plan multiple rounds; delegate follow-ups only when Round 1 demonstrates genuine need.
59
+
60
+ Scale your team (1-${maxTeamSize}) based on topic scope — not every round needs the full team. A single well-targeted researcher for follow-up gaps is usually sufficient.`;
61
+ } else {
62
+ return `**Complexity: Level 3 (Ultra)**. Perform an exhaustive, deep-dive research effort, leaving no stone unturned. **Plan ONLY Round 1** — make it comprehensive, deploying up to ${maxTeamSize} researchers with full query budgets (${queryBudget} each) covering all major dimensions of the topic. Round 1 should aim to cover the full scope comprehensively.
63
+
64
+ Rounds 2-3 are purely reactive — they only happen when Round 1 findings reveal gaps that warrant deeper diving into specific dimensions or broader exploration of adjacent topics. Do NOT pre-plan multiple rounds; let the findings drive the need. Think of Round 1 as the comprehensive landscape map, with follow-up rounds as targeted expeditions into areas that need more detail.
65
+
66
+ **ULTRA-SPECIFICITY MANDATE**: Level 3 demands granular, exhaustive detail on every fact that benefits from it — exact figures, dates, names, mechanisms, edge cases, historical context, technical specifics, and primary-source precision. Plan dedicated researchers for drilling into the ultra-specific dimensions of any finding where greater detail adds value.`;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get complexity-specific guidance for the evaluator
72
+ */
73
+ export function getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string {
74
+ if (complexity === 1) {
75
+ return `**Level 1 (Normal)** - Thorough, well-rounded investigation with solid multi-source coverage.
76
+
77
+ - **SYNTHESIZE when**: The primary topic is covered from multiple angles with evidence from diverse sources and no significant gaps remain that would prevent a complete answer.
78
+ - **DELEGATE when**: Coverage is incomplete, important angles are missing, or additional sources would meaningfully strengthen the findings.
79
+
80
+ **DEFAULT PATH**: Synthesize after Round 1 if the researcher produced solid coverage of the core topic. **Level 1 should almost always be a single round** — only delegate a second round when Round 1 clearly missed important angles or lacks source diversity. A single researcher is the default for Level 1 follow-up rounds — only use 2 researchers if the remaining gaps clearly span two distinct domains. Level 2 research escalation should be reserved for truly high/extreme research needs only.`;
81
+ } else if (complexity === 2) {
82
+ return `**Level 2 (Deep)** - Thorough, multi-phase investigation with comprehensive citations.
83
+
84
+ - **SYNTHESIZE when**: You are confident the research is genuinely complete. This means multiple angles covered with substantial findings across all major topics, diverse sources cited throughout, and no significant gaps in coverage.
85
+ - **DELEGATE when**: ANY gaps remain in major topics, insufficient source diversity, missing details, or areas that need deeper exploration. Don't synthesize prematurely.
86
+
87
+ **DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 2 is designed for multi-round research. Each round adds depth and citation diversity — but do not delegate unnecessarily. Synthesize after Round 1 ONLY if the researcher produced comprehensive coverage of the topic with good source diversity. If Round 1 has any gaps, delegate Round 2. A third round is only warranted when Round 2 still leaves significant holes. Scale researcher count to match the gaps — focused gaps may only need 1 researcher, while broad gaps benefit from the full team.`;
88
+ } else {
89
+ return `**Level 3 (Ultra)** - Exhaustive, comprehensive deep-dive with extensive citations.
90
+
91
+ - **SYNTHESIZE when**: You are confident the research is genuinely and exhaustively complete. This means exhaustively covered across ALL substantial avenues with multiple diverse sources per major topic, comprehensive citations throughout, and no meaningful gaps remain.
92
+ - **DELEGATE when**: ANY meaningful gaps, nuanced angles, insufficient source diversity, inadequate citations, or areas needing deeper investigation remain.
93
+
94
+ **DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 3 has ${MAX_ROUNDS_LEVEL_3} rounds available. Be generous with follow-up delegation — lean toward using all available rounds. Each round adds breadth, depth, and citation diversity. Delegate for follow-up whenever remaining gaps or under-explored angles exist, even if progress has been good. Only synthesize when you have genuinely comprehensive coverage across all major areas with no meaningful gaps that another round would address.
95
+
96
+ **ULTRA-SPECIFICITY MANDATE**: For every fact, finding, or topic area where greater granularity adds value, delegate additional researchers to pursue it. This includes: exact figures and statistics, precise dates and timelines, technical mechanisms, named individuals and their specific contributions, primary-source verbatim data, edge cases, and any dimension where surface-level coverage would leave the reader with unanswered questions.`;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get round phase guidance based on current round and max rounds
102
+ */
103
+ export function getRoundPhaseGuidance(currentRound: number, maxRounds: number, complexity: 1 | 2 | 3, maxTeamSize: number): string {
104
+ const roundRatio = currentRound / maxRounds;
105
+
106
+ if (roundRatio <= 0.5) {
107
+ // Early rounds
108
+ if (complexity === 3) {
109
+ return `\n\n---\n\n**Round Phase: EARLY (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\n**DELEGATE. Do not synthesize.** You are in the early phase of an exhaustive research effort with ${maxRounds} rounds available.\n- Deploy up to ${maxTeamSize} researchers across completely distinct angles — leave no major dimension unmapped\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Fully saturate each researcher's query budget; partial budgets waste available depth\n- This phase exists to establish the broadest possible foundation of sources and findings\n- Do NOT synthesize under any circumstances this early`;
110
+ }
111
+ // Level 1 / Level 2 early
112
+ return `\n\n---\n\n**Round Phase: EARLY (Round ${currentRound} of ${maxRounds})**\n\nYou are in the early phase of research. Be more permissive with delegation:\n- Deploy researchers to broadly map the landscape\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Don't worry if findings are incomplete — later rounds can fill gaps\n- Focus on breadth and initial exploration\n- Use available researchers to cover distinct angles in parallel`;
113
+ } else if (roundRatio <= 0.8) {
114
+ // Middle rounds
115
+ if (complexity === 3) {
116
+ return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\n**PREFER DELEGATION.** You are in the middle phase of exhaustive research — use your remaining rounds.\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- With ${maxRounds - currentRound} round(s) remaining, delegate to cover angles not yet fully explored, drill into deeper sub-topics, or verify findings with additional sources\n- Be generous with follow-up — delegate whenever gaps or under-explored angles exist\n- Synthesize ONLY when you genuinely cannot identify gaps that another round would address\n- If you can name even one meaningful unexplored angle — DELEGATE`;
117
+ }
118
+ if (complexity === 2) {
119
+ return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds})**\n\n**Level 2 (Deep) Guidance**: You are in the middle phase of deep research. Continue delegating when there are still meaningful gaps or areas to explore.\n\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize only when findings are comprehensive and no significant gaps remain that warrant another round\n- Default to delegating when in doubt\n\n`;
120
+ }
121
+ // Level 1 middle
122
+ return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds})**\n\nYou are in the middle phase of research. Apply balanced judgment:\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize if you have substantial coverage of the key aspects\n- Delegate for significant gaps or to explore specialized sub-topics\n- Consider depth over breadth at this stage\n- Focus on rounding out incomplete areas`;
123
+ } else {
124
+ // Late rounds
125
+ if (complexity === 3) {
126
+ return `\n\n---\n\n**Round Phase: LATE (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\nYou are in the late phase of exhaustive research. Still prefer delegation — use your remaining rounds.\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Delegate if ANY meaningful dimension remains under-sourced, under-verified, or shallowly covered\n- Use remaining rounds to drill into specialist detail, verify findings, or explore nuanced angles surfaced by earlier rounds\n- Synthesize only when you have genuinely exhausted meaningful research avenues AND have comprehensive multi-source coverage across all major areas`;
127
+ }
128
+ // Level 1 / Level 2 late
129
+ return `
130
+
131
+ ---
132
+
133
+ **Round Phase: LATE (Round ${currentRound} of ${maxRounds})**
134
+
135
+ You are in the late phase of research. Set a higher threshold for delegation:
136
+ - Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize if the core question is answerable with current findings
137
+ - Delegate only for CRITICAL gaps that cannot be resolved from existing findings
138
+ - Avoid delegating for minor details or marginal improvements
139
+ - Focus on delivering a complete, coherent response`;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Parse JSON from LLM response into ResearchPlan
145
+ */
146
+ export function parseJsonPlan(text: string): ResearchPlan {
147
+ const result = extractJson<unknown>(text, 'object');
148
+ if (!result.success || !result.value) {
149
+ const preview = text.length > 100 ? text.slice(0, 100) + '...' : text;
150
+ throw new Error(`Failed to extract valid JSON plan: ${result.error}. Raw response preview: "${preview}"`);
151
+ }
152
+
153
+ // Robust validation using TypeBox
154
+ try {
155
+ // 1. Convert/Coerce values (e.g. string numbers to numbers, single values to arrays if possible)
156
+ const coerced = Value.Convert(ResearchPlanSchema, result.value);
157
+
158
+ // 2. Validate against schema
159
+ if (!Value.Check(ResearchPlanSchema, coerced)) {
160
+ const errors = [...Value.Errors(ResearchPlanSchema, coerced)];
161
+ const errorMsg = errors.map((e: any) => `${e.path}: ${e.message}`).join(', ');
162
+ // If validation fails, we throw to satisfy tests and ensure system integrity
163
+ throw new Error(`Plan validation failed: ${errorMsg}`);
164
+ }
165
+
166
+ const plan = coerced as ResearchPlan;
167
+
168
+ // Invariant: only a 'synthesize' response may omit researchers. Every other action
169
+ // (delegate, or an unset action that defaults to delegation downstream) must carry a
170
+ // researchers array to act on. The schema relaxes `researchers` to optional so a valid
171
+ // synthesis ({ action, content }) is accepted instead of being thrown into the costly
172
+ // agentic-repair path; this guard preserves the delegate-must-have-researchers rule.
173
+ if (plan.action !== 'synthesize' && plan.researchers === undefined) {
174
+ throw new Error('Plan validation failed: /researchers: a non-synthesize plan must include a researchers array');
175
+ }
176
+
177
+ // 3. Post-processing
178
+ if (plan.researchers) {
179
+ plan.researchers.forEach((r) => {
180
+ // Coerce IDs to strings for consistency
181
+ r.id = String(r.id);
182
+ });
183
+ }
184
+
185
+ return plan;
186
+ } catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ throw new Error(`Plan validation error: ${msg}`, { cause: err });
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Build a fallback coordinator plan when LLM fails
194
+ */
195
+ export function buildFallbackCoordinatorPlan(serviceName: string, _rawText: string, query: string): ResearchPlan {
196
+ const words = query.split(/\s+/).slice(0, 6).join(' ');
197
+ const queries = [query, `${words} overview`, `${words} latest`].filter(Boolean).slice(0, 3);
198
+ logger.warn(`[${serviceName}] Coordinator fallback: single researcher for "${query.slice(0, 80)}"`);
199
+ return {
200
+ action: 'delegate',
201
+ researchers: [{ id: '1', name: 'General Researcher', goal: `Research the following query comprehensively: ${query}`, queries }],
202
+ allQueries: queries,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Cap researcher queries to stay within budget
208
+ */
209
+ export function capResearcherQueries(plan: ResearchPlan, complexity: 1 | 2 | 3, serviceName: string): ResearchPlan {
210
+ const budget = getQueryBudget(complexity);
211
+
212
+ // Hard caps per round - based on actual maximum possible queries
213
+ // Level 1: 2 researchers x 10 queries = 20 maximum
214
+ // Level 2: 3 researchers x 15 queries = 45 maximum
215
+ // Level 3: 5 researchers x 20 queries = 100 maximum
216
+ const ROUND_HARD_CAP = complexity === 1 ? 20
217
+ : complexity === 2 ? 45
218
+ : 100;
219
+
220
+ if (!plan.researchers) return plan;
221
+
222
+ // 1. Normalize IDs to strings and cap individual researchers
223
+ // CRITICAL: Only keep researchers with non-empty queries to guarantee search results
224
+ const maxTeam = getTeamSize(complexity);
225
+ plan.researchers = plan.researchers
226
+ .filter(r => r && typeof r === 'object' && Array.isArray(r.queries) && r.queries.length > 0)
227
+ .slice(0, maxTeam)
228
+ .map(r => {
229
+ const normalized = { ...r, id: String(r.id) };
230
+ if (normalized.queries.length > budget) {
231
+ logger.warn(`[${serviceName}] Capping researcher ${normalized.id} queries: ${normalized.queries.length} → ${budget}`);
232
+ normalized.queries = normalized.queries.slice(0, budget);
233
+ }
234
+ return normalized;
235
+ });
236
+
237
+ // 1b. Enforce unique researcher IDs within the round. Reports are keyed `${round}.${id}`
238
+ // and search links by String(id); two researchers sharing an id (the LLM emitting `1.1`
239
+ // twice, or `1` and "1") collide last-writer-wins — both run but one's findings are silently
240
+ // dropped. Renumber collisions so every launched researcher's report is retained.
241
+ const seenIds = new Set<string>();
242
+ for (const r of plan.researchers) {
243
+ const rid = String(r.id);
244
+ if (!seenIds.has(rid)) { seenIds.add(rid); continue; }
245
+ let n = 2;
246
+ let candidate = `${rid}-${n}`;
247
+ while (seenIds.has(candidate)) { n++; candidate = `${rid}-${n}`; }
248
+ logger.warn(`[${serviceName}] Duplicate researcher id '${rid}' → renumbered '${candidate}'`);
249
+ r.id = candidate;
250
+ seenIds.add(candidate);
251
+ }
252
+
253
+ // 2. Enforce global round budget
254
+ let totalQueries = plan.researchers.reduce((sum, r) => sum + r.queries.length, 0);
255
+ if (totalQueries > ROUND_HARD_CAP) {
256
+ logger.warn(`[${serviceName}] Total round queries (${totalQueries}) exceeds hard cap (${ROUND_HARD_CAP}). Trimming...`);
257
+ while (totalQueries > ROUND_HARD_CAP) {
258
+ let maxCount = 0;
259
+ let maxIdx = -1;
260
+ if (!plan.researchers) break;
261
+ for (let i = 0; i < plan.researchers.length; i++) {
262
+ if (plan.researchers[i]!.queries.length > maxCount) {
263
+ maxCount = plan.researchers[i]!.queries.length;
264
+ maxIdx = i;
265
+ }
266
+ }
267
+ if (maxIdx === -1) break;
268
+ plan.researchers[maxIdx]!.queries.pop();
269
+ totalQueries--;
270
+ }
271
+ }
272
+
273
+ plan.allQueries = plan.researchers.flatMap(r => r.queries);
274
+
275
+ // If no researchers remain with valid queries, force synthesis
276
+ if (plan.action === 'delegate' && plan.researchers.length === 0) {
277
+ logger.warn(`[${serviceName}] No valid researchers after query cap/filtering, forcing synthesize`);
278
+ return { ...plan, action: 'synthesize', researchers: [], allQueries: [] };
279
+ }
280
+
281
+ return plan;
282
+ }
283
+
284
+ /**
285
+ * Generate researcher configurations from a plan
286
+ */
287
+ export function generateResearchers(plan: ResearchPlan, _query: string, _complexity: 1 | 2 | 3): ResearcherConfig[] {
288
+ if (!plan.researchers) {
289
+ return [];
290
+ }
291
+
292
+ // Normalize IDs to strings
293
+ const researchers = plan.researchers.map(r => ({
294
+ ...r,
295
+ id: String(r.id),
296
+ }));
297
+
298
+ return researchers;
299
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Scheduler Factory Interface
3
+ *
4
+ * Defines the contract for creating scheduler instances.
5
+ * This interface lives in the Core layer to follow Clean Architecture principles.
6
+ *
7
+ * Implementations should be provided by the Infrastructure layer.
8
+ */
9
+
10
+ import type { Config } from '../config.ts';
11
+ import type { IService } from './service-registry.ts';
12
+ import type { IScheduler } from './interfaces/scheduler-interfaces.ts';
13
+ export type { IScheduler };
14
+
15
+ /**
16
+ * Scheduler Factory Interface
17
+ *
18
+ * Provides methods for creating and managing scheduler instances.
19
+ * This interface allows the Core layer to depend on abstractions,
20
+ * not concrete implementations from the Infrastructure layer.
21
+ */
22
+ export interface ISchedulerFactory extends IService {
23
+ /**
24
+ * Get or create a scheduler instance.
25
+ * Handles leader election and client/server mode switching.
26
+ *
27
+ * @param config Optional configuration
28
+ * @returns A scheduler instance
29
+ */
30
+ getScheduler(config?: Config): Promise<IScheduler>;
31
+
32
+ /**
33
+ * Get the current scheduler version string.
34
+ * The version is derived from the configuration and changes when config changes.
35
+ *
36
+ * @param config Optional configuration
37
+ * @returns The scheduler version string
38
+ */
39
+ getSchedulerVersion(config?: Config): string;
40
+
41
+ /**
42
+ * Force a restart of the scheduler by clearing the global cache and state.
43
+ * This should be called when configuration changes are detected.
44
+ *
45
+ * @param forceClearRemoteState Whether to force clear state from remote processes
46
+ */
47
+ forceSchedulerRestart(forceClearRemoteState?: boolean): Promise<void>;
48
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Scheduler Service
3
+ *
4
+ * Coordinates the multi-process browser worker pool and task scheduling.
5
+ * Implements leader election to ensure only one browser pool is active.
6
+ */
7
+
8
+ import { ServiceLifecycle } from './service-registry.ts';
9
+ import { ServiceNames } from './service-interfaces.ts';
10
+ import type { IScheduler, ISchedulerService, ISchedulerInternals, ISchedulerInstance } from './service-interfaces.ts';
11
+ import { logger } from '../logger.ts';
12
+ import { raceWithDeadline } from '../utils/safe-unref.ts';
13
+
14
+ /**
15
+ * Scheduler Service Implementation
16
+ */
17
+ export class SchedulerService implements ISchedulerService, ISchedulerInternals {
18
+ readonly name = ServiceNames.SCHEDULER;
19
+ lifecycle = ServiceLifecycle.UNINITIALIZED;
20
+
21
+ private scheduler: IScheduler | null = null;
22
+ private initializationPromise: Promise<IScheduler> | null = null;
23
+ private schedulerVersion: string | null = null;
24
+ private pendingShutdownPromise: Promise<void> | null = null;
25
+ private restartInProgress = false;
26
+
27
+ async initialize(): Promise<void> {
28
+ this.lifecycle = ServiceLifecycle.INITIALIZED;
29
+ }
30
+
31
+ async dispose(): Promise<void> {
32
+ if (this.lifecycle === ServiceLifecycle.DISPOSED) return;
33
+ this.lifecycle = ServiceLifecycle.DISPOSING;
34
+
35
+ // Invalidate any in-flight initialization FIRST: the scheduler factory
36
+ // re-checks this slot (its "superseded" guard) after every await point,
37
+ // so nulling it here makes a mid-election init shut down its
38
+ // freshly-created scheduler (closing its HTTP server) instead of
39
+ // installing it onto a disposed service where nothing would ever tear it
40
+ // down. Then await the in-flight init (bounded — the election can block
41
+ // up to ~60s on the browser-init lock) so its product is torn down
42
+ // before we report disposal complete. The expected outcome is a
43
+ // rejection ("Initialization superseded"); swallow it.
44
+ const inFlightInit = this.initializationPromise;
45
+ this.initializationPromise = null;
46
+ if (inFlightInit) {
47
+ try {
48
+ await raceWithDeadline(inFlightInit.then(() => undefined, () => undefined), 10000);
49
+ } catch {
50
+ /* raceWithDeadline only rejects if the promise rejects; already swallowed above */
51
+ }
52
+ }
53
+
54
+ if (this.scheduler) {
55
+ try {
56
+ await this.scheduler.shutdown();
57
+ } catch (err) {
58
+ logger.error('[SchedulerService] Error during scheduler shutdown:', err);
59
+ }
60
+ this.scheduler = null;
61
+ }
62
+
63
+ this.lifecycle = ServiceLifecycle.DISPOSED;
64
+ }
65
+
66
+ /**
67
+ * Get the current scheduler instance if initialized
68
+ */
69
+ getScheduler(): IScheduler | null {
70
+ return this.scheduler;
71
+ }
72
+
73
+ /**
74
+ * Check if the scheduler is ready
75
+ */
76
+ isReady(): boolean {
77
+ return this.scheduler !== null;
78
+ }
79
+
80
+ // ============================================================================
81
+ // ISchedulerInternals Implementation
82
+ // ============================================================================
83
+
84
+ getSchedulerInstance(): ISchedulerInstance | null {
85
+ return this.scheduler as unknown as ISchedulerInstance | null;
86
+ }
87
+
88
+ setSchedulerInstance(instance: ISchedulerInstance | null): void {
89
+ this.scheduler = instance as unknown as IScheduler | null;
90
+ }
91
+
92
+ getSchedulerVersion(): string | null {
93
+ return this.schedulerVersion;
94
+ }
95
+
96
+ setSchedulerVersion(version: string | null): void {
97
+ this.schedulerVersion = version;
98
+ }
99
+
100
+ getSchedulerInitializationPromise(): Promise<ISchedulerInstance> | null {
101
+ return this.initializationPromise as unknown as Promise<ISchedulerInstance> | null;
102
+ }
103
+
104
+ setSchedulerInitializationPromise(promise: Promise<ISchedulerInstance> | null): void {
105
+ this.initializationPromise = promise as unknown as Promise<IScheduler> | null;
106
+ }
107
+
108
+ getPendingShutdownPromise(): Promise<void> | null {
109
+ return this.pendingShutdownPromise;
110
+ }
111
+
112
+ setPendingShutdownPromise(promise: Promise<void> | null): void {
113
+ this.pendingShutdownPromise = promise;
114
+ }
115
+
116
+ isSchedulerRestartInProgress(): boolean {
117
+ return this.restartInProgress;
118
+ }
119
+
120
+ setSchedulerRestartInProgress(inProgress: boolean): void {
121
+ this.restartInProgress = inProgress;
122
+ }
123
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Service Initialization Module
3
+ *
4
+ * This module registers all services with the service registry.
5
+ * This is the central place for service configuration and initialization.
6
+ */
7
+
8
+ import { registerService, getService, disposeAllServices, type IService, getServiceContainer } from './service-registry.ts';
9
+ import type { ServiceContainer } from './service-registry.ts';
10
+ import { ServiceNames } from './service-interfaces.ts';
11
+ import type { IStateManager } from './interfaces/state-manager-interfaces.ts';
12
+ import { SchedulerService } from './scheduler-service.ts';
13
+ import { HealthCheckService } from './health-check-service.ts';
14
+ import { PlanningService } from './planning-service.ts';
15
+ import { logger } from '../logger.ts';
16
+
17
+ /**
18
+ * Register all core services with the service registry
19
+ */
20
+ export function registerCoreServices(container: ServiceContainer = getServiceContainer()): void {
21
+ logger.debug('[ServiceInitialization] Registering core services...');
22
+
23
+ // Register Scheduler Service
24
+ registerService(
25
+ ServiceNames.SCHEDULER,
26
+ () => new SchedulerService(),
27
+ {
28
+ allowOverwrite: false,
29
+ enableLogging: true,
30
+ },
31
+ container
32
+ );
33
+
34
+ // Register Health Check Cache Service
35
+ registerService(
36
+ ServiceNames.HEALTH_CHECK_CACHE,
37
+ () => new HealthCheckService(),
38
+ {
39
+ allowOverwrite: false,
40
+ enableLogging: true,
41
+ },
42
+ container
43
+ );
44
+
45
+ // Register Planning Service
46
+ registerService(
47
+ ServiceNames.PLANNING,
48
+ () => new PlanningService(),
49
+ {
50
+ // (Planning must be available early — it is eager-initialized via the eagerServices list below.)
51
+ allowOverwrite: false,
52
+ enableLogging: true,
53
+ },
54
+ container
55
+ );
56
+
57
+ logger.debug('[ServiceInitialization] All core services registered');
58
+ }
59
+
60
+ /**
61
+ * Initialize all core services
62
+ * This is called early in the application startup
63
+ *
64
+ * @param ctx - Optional extension context to pass to services
65
+ * @param container - Service container instance
66
+ */
67
+ export async function initializeCoreServices(ctx?: any, container: ServiceContainer = getServiceContainer()): Promise<{ success: boolean; initialized: string[]; failed: string[] }> {
68
+ logger.log('[ServiceInitialization] Initializing core services...');
69
+
70
+ const initialized: string[] = [];
71
+ const failed: string[] = [];
72
+
73
+ // Critical infrastructure services (always initialize early)
74
+ const criticalInfrastructure = [
75
+ { name: ServiceNames.METRICS, label: 'Metrics Service' },
76
+ { name: ServiceNames.PROCESS_LIFECYCLE, label: 'Process Lifecycle Service' },
77
+ { name: ServiceNames.STATE_PATH_CONFIGURATION, label: 'State Path Configuration' },
78
+ { name: ServiceNames.FILE_LOCK_SERVICE, label: 'File Lock Service' },
79
+ { name: ServiceNames.STATE_BACKUP_MANAGER, label: 'State Backup Manager' },
80
+ { name: ServiceNames.STATE_SESSION_MANAGER, label: 'State Session Manager' },
81
+ { name: ServiceNames.STATE_BROWSER_MANAGER, label: 'State Browser Manager' },
82
+ { name: ServiceNames.STATE_METRICS_COLLECTOR, label: 'State Metrics Collector' },
83
+ { name: ServiceNames.STATE_VALIDATOR, label: 'State Validator' },
84
+ { name: ServiceNames.GPU_RESOURCE_SERVICE, label: 'GPU Resource Service' },
85
+ { name: ServiceNames.STATE_MANAGER, label: 'State Manager Service' },
86
+ { name: ServiceNames.HEALTH_CHECK_CACHE, label: 'Health Check Cache Service' },
87
+ { name: ServiceNames.HEALTH_REGISTRY, label: 'Health Registry Service' },
88
+ ];
89
+
90
+ // Services requiring eager initialization — proactively getService'd below so
91
+ // they are constructed at startup rather than on first use.
92
+ const eagerServices = [
93
+ { name: ServiceNames.PLANNING, label: 'Planning Service' },
94
+ ];
95
+
96
+ // Lazy services (initialized on first use)
97
+ const lazyServices = [
98
+ ServiceNames.SCHEDULER,
99
+ ServiceNames.KNOWLEDGE_STORE,
100
+ ServiceNames.RESEARCH_ORCHESTRATION,
101
+ ServiceNames.WORKER_POOL_MANAGER,
102
+ ];
103
+
104
+ try {
105
+ // Initialize critical infrastructure services
106
+ logger.log('[ServiceInitialization] Initializing critical infrastructure services...');
107
+ for (const service of criticalInfrastructure) {
108
+ try {
109
+ logger.debug(`[ServiceInitialization] Initializing ${service.label}...`);
110
+ // getService will call initialize(ctx) if not already initialized
111
+ await getService<IService>(service.name, ctx, container);
112
+ initialized.push(service.label);
113
+ logger.debug(`[ServiceInitialization] ${service.label} initialized`);
114
+ } catch (err) {
115
+ const errorMsg = `${service.label} initialization failed`;
116
+ logger.error(`[ServiceInitialization] FAILED: ${errorMsg}:`, err);
117
+ failed.push(errorMsg);
118
+ // Continue with other services even if one fails
119
+ }
120
+ }
121
+
122
+ // Initialize eagerly-marked services
123
+ logger.log('[ServiceInitialization] Initializing eagerly-marked services...');
124
+ for (const service of eagerServices) {
125
+ try {
126
+ logger.debug(`[ServiceInitialization] Initializing ${service.label}...`);
127
+ await getService<IService>(service.name, ctx, container);
128
+ initialized.push(service.label);
129
+ logger.debug(`[ServiceInitialization] ${service.label} initialized`);
130
+ } catch (err) {
131
+ const errorMsg = `${service.label} initialization failed`;
132
+ logger.error(`[ServiceInitialization] FAILED: ${errorMsg}:`, err);
133
+ failed.push(errorMsg);
134
+ // Continue with other services even if one fails
135
+ }
136
+ }
137
+
138
+ // Log lazy services that will be initialized on demand
139
+ if (lazyServices.length > 0) {
140
+ logger.log(`[ServiceInitialization] ${lazyServices.length} services configured for lazy initialization: ${lazyServices.join(', ')}`);
141
+ }
142
+
143
+ // Log summary
144
+ if (failed.length === 0) {
145
+ logger.log(`[ServiceInitialization] All ${initialized.length} critical services initialized successfully`);
146
+ } else {
147
+ logger.warn(`[ServiceInitialization] WARNING: ${initialized.length}/${initialized.length + failed.length} services initialized, ${failed.length} failed`);
148
+ for (const failure of failed) {
149
+ logger.warn(`[ServiceInitialization] - ${failure}`);
150
+ }
151
+ }
152
+
153
+ return { success: failed.length === 0, initialized, failed };
154
+ } catch (err) {
155
+ logger.error('[ServiceInitialization] Failed to initialize core services:', err);
156
+ throw err;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Dispose all core services
162
+ * This is called during application shutdown
163
+ *
164
+ * @param container - Service container instance
165
+ */
166
+ export async function disposeCoreServices(container: ServiceContainer = getServiceContainer()): Promise<void> {
167
+ logger.log('[ServiceInitialization] Disposing core services...');
168
+
169
+ try {
170
+ // Clear embedding server registration before parallel disposal so the
171
+ // EmbeddingServer can deregister itself while the StateManager is still alive.
172
+ // Use tryGet, not getService: on a teardown that follows a partial/failed init
173
+ // (e.g. STATE_MANAGER itself failed to initialize and _doInit's catch calls shutdown),
174
+ // getService would CONSTRUCT-and-initialize a fresh StateManager just to dispose it —
175
+ // a shutdown-resurrection. Only touch an already-live instance.
176
+ try {
177
+ const stateManager = container.tryGet<IStateManager>(ServiceNames.STATE_MANAGER);
178
+ if (stateManager) {
179
+ await stateManager.clearEmbeddingServer();
180
+ logger.debug('[ServiceInitialization] Cleared embedding server state before disposal');
181
+ }
182
+ } catch {
183
+ // Non-fatal: embedding server state will be cleaned up on next startup via PID check
184
+ }
185
+
186
+ // Dispose services in reverse dependency order
187
+ await disposeAllServices(container);
188
+
189
+ logger.log('[ServiceInitialization] Core services disposed successfully');
190
+ } catch (err) {
191
+ logger.error('[ServiceInitialization] Failed to dispose core services:', err);
192
+ throw err;
193
+ }
194
+ }