@lincoln504/pi-research 0.1.12 → 1.0.0

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 +64 -115
  3. package/dist/cli.mjs +23211 -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 +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -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 +1044 -0
  30. package/src/config.ts +1036 -147
  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 +264 -7
  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 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  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,48 @@
1
+ /**
2
+ * Service Interfaces for pi-research
3
+ *
4
+ * Central re-export barrel for all service interfaces.
5
+ */
6
+
7
+ // Scheduler interfaces
8
+ export type { SearchResult, ScrapeResult, IScheduler, SchedulerMetadata, ISchedulerInstance, ISchedulerInternals, ISchedulerService } from './interfaces/scheduler-interfaces.ts';
9
+ export type { ISchedulerFactory } from './scheduler-factory.ts';
10
+
11
+ // Health check interfaces
12
+ export type { HealthCheckResult, HealthCheckCache, IHealthCheckService, IHealthRegistryService } from './interfaces/health-check-interfaces.ts';
13
+
14
+ // Knowledge store interfaces
15
+ export type {
16
+ IEmbedder,
17
+ IKnowledgeStore,
18
+ IKnowledgeStoreService,
19
+ IWriterQueue,
20
+ IMetricsSnapshot,
21
+ IMetricHistogram,
22
+ IngestionItem,
23
+ StoreUrlEntry,
24
+ StoreDocument,
25
+ EmbedderOptions,
26
+ EmbedderState,
27
+ HFEnv,
28
+ ONNXRuntimeEnv,
29
+ DisposablePipeline
30
+ } from './interfaces/knowledge-interfaces.ts';
31
+
32
+ // State manager interfaces
33
+ export type { IStateManager } from './interfaces/state-manager-interfaces.ts';
34
+
35
+ // Process interfaces
36
+ export type { IProcessLifecycle, IGPUResourceService } from './interfaces/process-interfaces.ts';
37
+
38
+ // Observer interfaces
39
+ export type { ResearchObserver } from './interfaces/observer-interfaces.ts';
40
+
41
+ // Orchestration interfaces
42
+ export type { IResearchOrchestration, RunResearchersOptions, IResearchSynthesisService, ResearchOptions } from './interfaces/orchestration-interfaces.ts';
43
+
44
+ // Planning interfaces
45
+ export type { ResearchPlan, ResearcherConfig, SessionContext, GeneratePlanOptions, GenerateQueriesOptions, UpdatePlanOptions, IPlanningService } from './interfaces/planning-interfaces.ts';
46
+
47
+ // Service names
48
+ export { ServiceNames, type ServiceName } from './interfaces/service-names.ts';
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Service Registry - Centralized Dependency Injection Container
3
+ *
4
+ * This module provides a service registry pattern to manage singleton services
5
+ * throughout the pi-research application. It replaces global state management
6
+ * with a proper dependency injection system.
7
+ *
8
+ * Benefits:
9
+ * - Explicit dependency management
10
+ * - Testability (easy to mock/replace services)
11
+ * - Lifecycle management (init, cleanup)
12
+ * - Type safety
13
+ * - No more globalThis pollution
14
+ */
15
+
16
+ import { logger } from '../logger.ts';
17
+ import { AsyncLocalStorage } from 'node:async_hooks';
18
+
19
+
20
+ /**
21
+ * Service lifecycle stages
22
+ */
23
+ export enum ServiceLifecycle {
24
+ UNINITIALIZED = 'uninitialized',
25
+ INITIALIZING = 'initializing',
26
+ INITIALIZED = 'initialized',
27
+ DISABLED = 'disabled',
28
+ DISPOSING = 'disposing',
29
+ DISPOSED = 'disposed',
30
+ }
31
+
32
+ /**
33
+ * Base interface for all services
34
+ */
35
+ export interface IService {
36
+ /**
37
+ * Unique identifier for this service
38
+ */
39
+ readonly name: string;
40
+
41
+ /**
42
+ * Current lifecycle state
43
+ */
44
+ lifecycle: ServiceLifecycle;
45
+
46
+ /**
47
+ * Initialize the service (called before first use)
48
+ */
49
+ initialize?(ctx?: any): Promise<void> | void;
50
+
51
+ /**
52
+ * Dispose the service (called during shutdown)
53
+ */
54
+ dispose?(): Promise<void> | void;
55
+ }
56
+
57
+ /**
58
+ * Service factory function type
59
+ */
60
+ export type ServiceFactory<T extends IService> = () => T | Promise<T>;
61
+
62
+ /**
63
+ * Service container options
64
+ */
65
+ export interface ServiceContainerOptions {
66
+ /**
67
+ * Whether to allow overwriting existing services
68
+ * @default false (service replacement is not allowed)
69
+ */
70
+ allowOverwrite?: boolean;
71
+
72
+ /**
73
+ * Whether to log service lifecycle events
74
+ * @default true
75
+ */
76
+ enableLogging?: boolean;
77
+ }
78
+
79
+ /**
80
+ * Service registration metadata
81
+ */
82
+ interface ServiceRegistration<T extends IService> {
83
+ factory: ServiceFactory<T>;
84
+ instance: T | null;
85
+ initializationPromise: Promise<T> | null;
86
+ options: ServiceContainerOptions;
87
+ }
88
+
89
+ /**
90
+ * AsyncLocalStorage to track which service is currently being initialized.
91
+ * Used to build a dependency graph for safe teardown.
92
+ */
93
+ const initializationContext = new AsyncLocalStorage<string>();
94
+
95
+ /**
96
+ * Centralized service container for dependency injection
97
+ */
98
+ export class ServiceContainer {
99
+ private services: Map<string, ServiceRegistration<any>> = new Map();
100
+ private dependencies: Map<string, Set<string>> = new Map();
101
+ public isDisposing: boolean = false;
102
+ // The single in-flight disposeAll() run, so concurrent disposeAll()/reset() calls
103
+ // coalesce onto (and AWAIT) it rather than a second caller observing "disposed" while
104
+ // teardown is still running, or reset() throwing mid-disposal.
105
+ private _disposalPromise: Promise<void> | null = null;
106
+ public isReady: boolean = false;
107
+ public cwd: string = process.cwd();
108
+ public config: any = null;
109
+ private readonly defaultOptions: Required<ServiceContainerOptions>;
110
+
111
+ constructor(options: ServiceContainerOptions = {}) {
112
+ this.defaultOptions = {
113
+ allowOverwrite: options.allowOverwrite ?? false,
114
+ enableLogging: options.enableLogging ?? true,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Register a service with the container
120
+ */
121
+ register<T extends IService>(
122
+ name: string,
123
+ factory: ServiceFactory<T>,
124
+ options: ServiceContainerOptions = {}
125
+ ): void {
126
+ if (this.isDisposing) {
127
+ throw new Error(`Cannot register service '${name}' during container disposal`);
128
+ }
129
+
130
+ const mergedOptions = { ...this.defaultOptions, ...options };
131
+
132
+ if (this.services.has(name)) {
133
+ if (!mergedOptions.allowOverwrite) {
134
+ throw new Error(`Service '${name}' is already registered. Use registerAndReplace() to overwrite.`);
135
+ }
136
+ if (mergedOptions.enableLogging) {
137
+ logger.warn(`[ServiceContainer] Replacing service '${name}'`);
138
+ }
139
+ } else {
140
+ if (mergedOptions.enableLogging) {
141
+ logger.debug(`[ServiceContainer] Registering service '${name}'`);
142
+ }
143
+ }
144
+
145
+ this.services.set(name, {
146
+ factory,
147
+ instance: null,
148
+ initializationPromise: null,
149
+ options: mergedOptions,
150
+ });
151
+
152
+ // Reset dependencies for this service
153
+ this.dependencies.set(name, new Set());
154
+ }
155
+
156
+ /**
157
+ * Register a service, replacing any existing service
158
+ */
159
+ async registerAndReplace<T extends IService>(
160
+ name: string,
161
+ factory: ServiceFactory<T>,
162
+ options: ServiceContainerOptions = {}
163
+ ): Promise<void> {
164
+ const mergedOptions = { ...this.defaultOptions, ...options, allowOverwrite: true };
165
+
166
+ // Dispose existing service if present
167
+ if (this.services.has(name)) {
168
+ const registration = this.services.get(name)!;
169
+ if (registration.instance && registration.instance.dispose) {
170
+ try {
171
+ await registration.instance.dispose();
172
+ } catch (err: unknown) {
173
+ logger.warn(`[ServiceContainer] Error disposing replaced service '${name}':`, err);
174
+ }
175
+ }
176
+ }
177
+
178
+ this.register(name, factory, mergedOptions);
179
+ }
180
+
181
+ /**
182
+ * Record a dependency between two services
183
+ */
184
+ private addDependency(dependent: string, dependency: string): void {
185
+ let deps = this.dependencies.get(dependent);
186
+ if (!deps) {
187
+ deps = new Set();
188
+ this.dependencies.set(dependent, deps);
189
+ }
190
+ deps.add(dependency);
191
+ }
192
+
193
+ /**
194
+ * Get a service instance, initializing it if necessary
195
+ */
196
+ async get<T extends IService>(name: string, ctx?: any): Promise<T> {
197
+ if (this.isDisposing) {
198
+ throw new Error(`Cannot get service '${name}' during container disposal`);
199
+ }
200
+
201
+ // Track dependency if we are currently inside an initialization or use-case of another service
202
+ const caller = initializationContext.getStore();
203
+ if (caller && caller !== name) {
204
+ this.addDependency(caller, name);
205
+ }
206
+
207
+ const registration = this.services.get(name);
208
+ if (!registration) {
209
+ throw new Error(`Service '${name}' is not registered`);
210
+ }
211
+
212
+ // Return existing instance if already initialized
213
+ if (registration.instance) {
214
+ // Re-initialize if ctx is provided and service supports it
215
+ if (ctx && registration.instance.initialize) {
216
+ // Wrap in initializationContext so nested get() calls are tracked
217
+ return initializationContext.run(name, async () => {
218
+ await registration.instance!.initialize!(ctx);
219
+ return registration.instance as T;
220
+ });
221
+ }
222
+ return registration.instance as T;
223
+ }
224
+
225
+ // Return existing initialization promise if in progress
226
+ if (registration.initializationPromise) {
227
+ return registration.initializationPromise as Promise<T>;
228
+ }
229
+
230
+ // Initialize the service within the tracking context
231
+ registration.initializationPromise = initializationContext.run(name, () =>
232
+ this._initializeService(registration, ctx)
233
+ );
234
+
235
+ try {
236
+ const instance = await registration.initializationPromise;
237
+ return instance as T;
238
+ } catch (error) {
239
+ // Clear initialization promise on error so next call retries
240
+ registration.initializationPromise = null;
241
+ throw error;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get a service instance synchronously (returns null if not initialized)
247
+ */
248
+ tryGet<T extends IService>(name: string): T | null {
249
+ if (this.isDisposing) {
250
+ return null;
251
+ }
252
+
253
+ const registration = this.services.get(name);
254
+ if (!registration) {
255
+ return null;
256
+ }
257
+
258
+ return registration.instance as T | null;
259
+ }
260
+
261
+ /**
262
+ * Check if a service is registered
263
+ */
264
+ has(name: string): boolean {
265
+ return this.services.has(name);
266
+ }
267
+
268
+ /**
269
+ * Check if a service is initialized
270
+ */
271
+ isInitialized(name: string): boolean {
272
+ const registration = this.services.get(name);
273
+ // Must check registration exists FIRST — undefined?.instance gives undefined,
274
+ // and undefined !== null is true, which would incorrectly report unregistered
275
+ // services as initialized.
276
+ if (!registration) return false;
277
+ return registration.instance !== null;
278
+ }
279
+
280
+ /**
281
+ * Clear (reset) a service instance, forcing re-initialization on next access
282
+ */
283
+ async clear(name: string): Promise<void> {
284
+ const registration = this.services.get(name);
285
+ if (!registration) {
286
+ throw new Error(`Service '${name}' is not registered`);
287
+ }
288
+
289
+ if (registration.instance && registration.instance.dispose) {
290
+ await registration.instance.dispose().catch((err: unknown) => {
291
+ logger.warn(`[ServiceContainer] Error disposing service '${name}':`, err);
292
+ });
293
+ }
294
+
295
+ registration.instance = null;
296
+ registration.initializationPromise = null;
297
+
298
+ if (registration.options.enableLogging) {
299
+ logger.debug(`[ServiceContainer] Cleared service '${name}'`);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Replace a service instance with a new one
305
+ */
306
+ async replace<T extends IService>(name: string, newInstance: T): Promise<void> {
307
+ const registration = this.services.get(name);
308
+ if (!registration) {
309
+ throw new Error(`Service '${name}' is not registered`);
310
+ }
311
+
312
+ // Dispose old instance if present
313
+ if (registration.instance && registration.instance.dispose) {
314
+ await registration.instance.dispose().catch((err: unknown) => {
315
+ logger.warn(`[ServiceContainer] Error disposing old service '${name}':`, err);
316
+ });
317
+ }
318
+
319
+ registration.instance = newInstance;
320
+ registration.initializationPromise = null;
321
+
322
+ if (registration.options.enableLogging) {
323
+ logger.debug(`[ServiceContainer] Replaced service '${name}'`);
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Dispose all services using a true Directed Acyclic Graph (DAG) teardown.
329
+ * This guarantees that services are only disposed after all their dependents
330
+ * have been successfully torn down.
331
+ */
332
+ async disposeAll(): Promise<void> {
333
+ // Synchronous re-entrancy guard: isDisposing is set as _runDisposeAll's first
334
+ // statement (before any await), so a service whose dispose() re-enters disposeAll()
335
+ // gets a safe no-op instead of recursing. Also publish the in-flight run as
336
+ // _disposalPromise so reset() (a separate, non-re-entrant caller) can AWAIT the
337
+ // teardown instead of throwing on the race — the Windows CI teardown fix.
338
+ if (this.isDisposing) return;
339
+ this._disposalPromise = this._runDisposeAll().finally(() => { this._disposalPromise = null; });
340
+ return this._disposalPromise;
341
+ }
342
+
343
+ private async _runDisposeAll(): Promise<void> {
344
+ this.isDisposing = true;
345
+ if (this.defaultOptions.enableLogging) {
346
+ logger.log('[ServiceContainer] Disposing all services (DAG-ordered teardown)...');
347
+ }
348
+
349
+ try {
350
+ const activeServices = Array.from(this.services.keys()).filter(name =>
351
+ this.services.get(name)?.instance !== null
352
+ );
353
+
354
+ const disposed = new Set<string>();
355
+
356
+ // Perform iterative disposal of services that have no active dependents
357
+ while (disposed.size < activeServices.length) {
358
+ const toDispose: string[] = [];
359
+
360
+ for (const name of activeServices) {
361
+ if (disposed.has(name)) continue;
362
+
363
+ // A service can be disposed if no OTHER active service depends on it
364
+ // that hasn't been disposed yet.
365
+ const hasUndisposedDependents = activeServices.some(other =>
366
+ !disposed.has(other) &&
367
+ other !== name &&
368
+ this.dependencies.get(other)?.has(name)
369
+ );
370
+
371
+ if (!hasUndisposedDependents) {
372
+ toDispose.push(name);
373
+ }
374
+ }
375
+
376
+ if (toDispose.length === 0) {
377
+ // Circular dependency detected or logic error
378
+ const remaining = activeServices.filter(n => !disposed.has(n));
379
+ logger.warn(`[ServiceContainer] Circular dependency or stuck disposal detected for: ${remaining.join(', ')}. Falling back to reverse-registration order.`);
380
+
381
+ const reverseRegistrations = Array.from(this.services.entries()).reverse();
382
+ for (const [name, registration] of reverseRegistrations) {
383
+ if (!disposed.has(name) && registration.instance && registration.instance.dispose) {
384
+ try { await registration.instance.dispose(); } catch (err) { logger.warn(`[ServiceContainer] Error disposing '${name}':`, err); }
385
+ registration.instance = null;
386
+ registration.initializationPromise = null;
387
+ }
388
+ }
389
+ break;
390
+ }
391
+
392
+ // IMPORTANT: Sort toDispose in reverse-registration order.
393
+ // This ensures that if multiple services are independent, we still dispose
394
+ // the one registered latest first, maintaining strict LIFO behavior by default.
395
+ const serviceNamesInOrder = Array.from(this.services.keys());
396
+ toDispose.sort((a, b) => serviceNamesInOrder.indexOf(b) - serviceNamesInOrder.indexOf(a));
397
+
398
+ // Dispose independent services in parallel (but sequentially within this batch
399
+ // if we want to be 100% sure about order, though Promise.all is fine if they are truly independent)
400
+ // Actually, to satisfy the test expectations of strict order, we should do them sequentially.
401
+ for (const name of toDispose) {
402
+ const registration = this.services.get(name)!;
403
+ if (registration.instance && registration.instance.dispose) {
404
+ try {
405
+ await registration.instance.dispose();
406
+ } catch (err: unknown) {
407
+ logger.warn(`[ServiceContainer] Error disposing service '${name}':`, err);
408
+ }
409
+ }
410
+ registration.instance = null;
411
+ registration.initializationPromise = null;
412
+ disposed.add(name);
413
+ }
414
+ }
415
+ } finally {
416
+ this.isDisposing = false;
417
+ if (this.defaultOptions.enableLogging) {
418
+ logger.log('[ServiceContainer] All services disposed (registrations preserved)');
419
+ }
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Get the number of registered services
425
+ */
426
+ get size(): number {
427
+ return this.services.size;
428
+ }
429
+
430
+ /**
431
+ * Get names of all registered services
432
+ */
433
+ getServiceNames(): string[] {
434
+ return Array.from(this.services.keys());
435
+ }
436
+
437
+ /**
438
+ * Get lifecycle state of a service
439
+ */
440
+ getServiceLifecycle(name: string): ServiceLifecycle | null {
441
+ const registration = this.services.get(name);
442
+ if (!registration) {
443
+ return null;
444
+ }
445
+
446
+ if (registration.instance) {
447
+ return ServiceLifecycle.INITIALIZED;
448
+ }
449
+
450
+ if (registration.initializationPromise) {
451
+ return ServiceLifecycle.INITIALIZING;
452
+ }
453
+
454
+ return ServiceLifecycle.UNINITIALIZED;
455
+ }
456
+
457
+ /**
458
+ * Reset the container, clearing all services
459
+ * This is primarily used for testing to ensure clean state between test runs
460
+ */
461
+ async reset(): Promise<void> {
462
+ // If a disposal is already in flight (e.g. a test's un-awaited shutdown, or a
463
+ // concurrent teardown), WAIT for it rather than throwing — reset()'s contract is to
464
+ // end up disposed+cleared, which the in-flight disposal is already doing. This fixed
465
+ // a Windows-only CI teardown race that surfaced as unhandled
466
+ // "Cannot reset container while disposing" rejections from an afterEach.
467
+ if (this._disposalPromise) {
468
+ await this._disposalPromise.catch(() => { /* disposal errors are logged in _runDisposeAll */ });
469
+ }
470
+
471
+ if (this.defaultOptions.enableLogging) {
472
+ logger.debug('[ServiceContainer] Resetting container...');
473
+ }
474
+
475
+ // Use the safe DAG disposal logic
476
+ await this.disposeAll();
477
+
478
+ // Drain any disposal still in flight before clearing registrations. disposeAll()
479
+ // returns immediately (isDisposing guard) when ANOTHER caller's disposal is already
480
+ // running, so our await above may not have waited for it — and a concurrent
481
+ // disposeAll iterating this.services while we clear() would make its services.get()
482
+ // throw. Loop until no disposal promise remains (each clears _disposalPromise in its
483
+ // finally). Non-reentrant and terminating: no new disposal can start once we hold the
484
+ // sync path here with isDisposing still true.
485
+ while (this._disposalPromise) {
486
+ await this._disposalPromise.catch(() => { /* logged in _runDisposeAll */ });
487
+ }
488
+
489
+ // Clear all registrations
490
+ this.services.clear();
491
+ this.dependencies.clear();
492
+ this.isDisposing = false;
493
+ this.isReady = false;
494
+
495
+ if (this.defaultOptions.enableLogging) {
496
+ logger.debug('[ServiceContainer] Container reset complete');
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Internal method to initialize a service
502
+ */
503
+ private async _initializeService<T extends IService>(
504
+ registration: ServiceRegistration<T>,
505
+ ctx?: any
506
+ ): Promise<T> {
507
+ let instance: T | null = null;
508
+ try {
509
+ instance = await registration.factory();
510
+ registration.instance = instance;
511
+
512
+ // Update lifecycle to initializing
513
+ instance.lifecycle = ServiceLifecycle.INITIALIZING;
514
+
515
+ // Call initialize hook if present
516
+ if (instance.initialize) {
517
+ await instance.initialize(ctx);
518
+ }
519
+
520
+ // Update lifecycle to initialized — but PRESERVE a DISABLED verdict the service's own
521
+ // initialize() set for itself (e.g. the knowledge store when Knowledge Mode is 'none', or
522
+ // a missing native binding). Unconditionally forcing INITIALIZED here left the store
523
+ // "initialized" with null components: it mis-reported as "initializing" instead of
524
+ // "disabled", and — because the mode-reenable revival keys off lifecycle === DISABLED — it
525
+ // defeated live re-enabling via /research-config, so the store stayed dead until a restart.
526
+ // Cast defeats control-flow narrowing: TS still thinks lifecycle is INITIALIZING (set
527
+ // above), unaware that the awaited initialize() may have mutated it to DISABLED via `this`.
528
+ if ((instance.lifecycle as ServiceLifecycle) !== ServiceLifecycle.DISABLED) {
529
+ instance.lifecycle = ServiceLifecycle.INITIALIZED;
530
+ }
531
+ registration.initializationPromise = null;
532
+
533
+ if (registration.options.enableLogging) {
534
+ logger.debug(`[ServiceContainer] Service '${instance.name}' initialized`);
535
+ }
536
+
537
+ return instance;
538
+ } catch (error) {
539
+ // Clean up instance on failure
540
+ if (instance) {
541
+ instance.lifecycle = ServiceLifecycle.UNINITIALIZED;
542
+ registration.instance = null;
543
+ }
544
+ registration.initializationPromise = null;
545
+ throw error;
546
+ }
547
+ }
548
+ }
549
+
550
+ // ============================================================================
551
+ // Global Service Container Instance
552
+ // ============================================================================
553
+
554
+ /**
555
+ * Global service container instance
556
+ * This is the default container for the application.
557
+ * CLI mode typically uses this instance.
558
+ */
559
+ let globalServiceContainer = new ServiceContainer({
560
+ allowOverwrite: false,
561
+ enableLogging: true,
562
+ });
563
+
564
+ /**
565
+ * Get the global service container instance
566
+ */
567
+ export function getServiceContainer(): ServiceContainer {
568
+ return globalServiceContainer;
569
+ }
570
+
571
+ /**
572
+ * Create a new service container instance
573
+ */
574
+ export function createServiceContainer(options: ServiceContainerOptions = {}): ServiceContainer {
575
+ return new ServiceContainer(options);
576
+ }
577
+
578
+ /**
579
+ * Convenience function to register a service
580
+ */
581
+ export function registerService<T extends IService>(
582
+ name: string,
583
+ factory: ServiceFactory<T>,
584
+ options?: ServiceContainerOptions,
585
+ container: ServiceContainer = globalServiceContainer
586
+ ): void {
587
+ container.register(name, factory, options);
588
+ }
589
+
590
+ /**
591
+ * Convenience function to replace a service
592
+ */
593
+ export function replaceService<T extends IService>(
594
+ name: string,
595
+ factory: ServiceFactory<T>,
596
+ options?: ServiceContainerOptions,
597
+ container: ServiceContainer = globalServiceContainer
598
+ ): Promise<void> {
599
+ return container.registerAndReplace(name, factory, options);
600
+ }
601
+
602
+ /**
603
+ * Convenience function to get a service
604
+ */
605
+ export function getService<T extends IService>(name: string, ctx?: any, container: ServiceContainer = globalServiceContainer): Promise<T> {
606
+ return container.get<T>(name, ctx);
607
+ }
608
+
609
+ /**
610
+ * Convenience function to try getting a service synchronously
611
+ */
612
+ export function tryGetService<T extends IService>(name: string, container: ServiceContainer = globalServiceContainer): T | null {
613
+ return container.tryGet<T>(name);
614
+ }
615
+
616
+ /**
617
+ * Convenience function to clear a service
618
+ */
619
+ export function clearService(name: string, container: ServiceContainer = globalServiceContainer): Promise<void> {
620
+ return container.clear(name);
621
+ }
622
+
623
+ /**
624
+ * Convenience function to check if a service is registered
625
+ */
626
+ export function hasService(name: string, container: ServiceContainer = globalServiceContainer): boolean {
627
+ return container.has(name);
628
+ }
629
+
630
+ /**
631
+ * Convenience function to check if a service is initialized
632
+ */
633
+ export function isServiceInitialized(name: string, container: ServiceContainer = globalServiceContainer): boolean {
634
+ return container.isInitialized(name);
635
+ }
636
+
637
+ /**
638
+ * Helper to extract a service container from a context object, if present.
639
+ */
640
+ export function tryGetServiceContainerFromCtx(ctx: any): ServiceContainer {
641
+ if (ctx?.container && typeof ctx.container.get === 'function' && typeof ctx.container.register === 'function') {
642
+ return ctx.container;
643
+ }
644
+ return globalServiceContainer;
645
+ }
646
+
647
+ /**
648
+ * Dispose all services
649
+ */
650
+ export function disposeAllServices(container: ServiceContainer = globalServiceContainer): Promise<void> {
651
+ return container.disposeAll();
652
+ }
653
+
654
+ /**
655
+ * Reset the global service container
656
+ */
657
+ export function resetServiceContainer(container: ServiceContainer = globalServiceContainer): Promise<void> {
658
+ return container.reset();
659
+ }