@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,292 @@
1
+ /**
2
+ * Infrastructure Service Initialization Module
3
+ *
4
+ * This module registers all infrastructure-level services with the service registry.
5
+ * Infrastructure services are registered by this module to avoid Core layer
6
+ * depending on Infrastructure layer.
7
+ */
8
+
9
+ import { registerService, getService, disposeAllServices, type IService, getServiceContainer } from '../core/service-registry.ts';
10
+ import type { ServiceContainer } from '../core/service-registry.ts';
11
+ import { ServiceNames } from '../core/service-interfaces.ts';
12
+ import type { IKnowledgeStoreService } from '../core/service-interfaces.ts';
13
+ import { SchedulerFactoryService } from './scheduler-factory-service.ts';
14
+ import { StateManagerService } from './state/state-manager-service.ts';
15
+ import { MetricsService } from './metrics-service.ts';
16
+ import { ProcessLifecycleService } from './process-lifecycle-service.ts';
17
+ import { GPUResourceService } from './gpu-resource-service.ts';
18
+ import { StateSessionManager } from './state/state-session-manager.ts';
19
+ import { StateBrowserManager } from './state/state-browser-manager.ts';
20
+ import { StateMetricsCollector } from './state/state-metrics.ts';
21
+ import { StateValidator } from './state/state-validator.ts';
22
+ import { WorkerPoolManager } from './browser/worker-pool-manager.ts';
23
+ import { StatePathConfiguration } from './state/state-path-configuration.ts';
24
+ import { FileLockService } from './file-lock-service.ts';
25
+ import { StateBackupManager } from './state/state-backup-manager.ts';
26
+ import { logger } from '../logger.ts';
27
+
28
+ /**
29
+ * Register all infrastructure services with the service registry
30
+ */
31
+ export function registerInfrastructureServices(container: ServiceContainer = getServiceContainer()): void {
32
+ logger.debug('[InfrastructureServiceInit] Registering infrastructure services...');
33
+
34
+ // Register Process Lifecycle Service
35
+ registerService(
36
+ ServiceNames.PROCESS_LIFECYCLE,
37
+ () => new ProcessLifecycleService(),
38
+ {
39
+ allowOverwrite: false,
40
+ enableLogging: true,
41
+ },
42
+ container
43
+ );
44
+
45
+ // Register State Path Configuration Service
46
+ // Must be initialized before FileLockService and StateBackupManager
47
+ registerService(
48
+ ServiceNames.STATE_PATH_CONFIGURATION,
49
+ () => new StatePathConfiguration(process.env['PI_RESEARCH_STATE_DIR']),
50
+ {
51
+ allowOverwrite: false,
52
+ enableLogging: true,
53
+ },
54
+ container
55
+ );
56
+
57
+ // Register File Lock Service
58
+ // Depends on StatePathConfiguration
59
+ registerService(
60
+ ServiceNames.FILE_LOCK_SERVICE,
61
+ async () => {
62
+ const pathConfig = await getService<StatePathConfiguration>(ServiceNames.STATE_PATH_CONFIGURATION, undefined, container);
63
+ const processLifecycle = await getService<ProcessLifecycleService>(ServiceNames.PROCESS_LIFECYCLE, undefined, container);
64
+ return new FileLockService({
65
+ lockFilePath: pathConfig.getLockFilePath(),
66
+ processLifecycle,
67
+ });
68
+ },
69
+ {
70
+ allowOverwrite: false,
71
+ enableLogging: true,
72
+ },
73
+ container
74
+ );
75
+
76
+ // Register State Backup Manager Service
77
+ // Depends on StatePathConfiguration
78
+ registerService(
79
+ ServiceNames.STATE_BACKUP_MANAGER,
80
+ async () => {
81
+ const pathConfig = await getService<StatePathConfiguration>(ServiceNames.STATE_PATH_CONFIGURATION, undefined, container);
82
+ return new StateBackupManager(
83
+ pathConfig.getStateFilePath(),
84
+ pathConfig.getBackupDirPath(),
85
+ 10 // maxBackups
86
+ );
87
+ },
88
+ {
89
+ allowOverwrite: false,
90
+ enableLogging: true,
91
+ },
92
+ container
93
+ );
94
+
95
+ // Register Scheduler Factory Service
96
+ // This is the factory that creates scheduler instances
97
+ registerService(
98
+ ServiceNames.SCHEDULER_FACTORY,
99
+ () => new SchedulerFactoryService(),
100
+ {
101
+ allowOverwrite: false,
102
+ enableLogging: true,
103
+ },
104
+ container
105
+ );
106
+
107
+ // Register State Manager Service
108
+ registerService(
109
+ ServiceNames.STATE_MANAGER,
110
+ () => new StateManagerService(),
111
+ {
112
+ allowOverwrite: false,
113
+ enableLogging: true,
114
+ },
115
+ container
116
+ );
117
+
118
+ // Register Knowledge Store Service.
119
+ // Loaded via dynamic import so the native ML/vector stack
120
+ // (@huggingface/transformers / onnxruntime-node, @lancedb/lancedb) is only
121
+ // pulled in when the store is actually requested — never at extension load.
122
+ // The default mode is 'global', but this factory is still fully lazy: it only
123
+ // runs the first time the store is actually accessed (a scrape cache-check or
124
+ // write), so startup stays native-free. When KNOWLEDGE_STORE_MODE is 'none'
125
+ // this factory never runs at all.
126
+ registerService(
127
+ ServiceNames.KNOWLEDGE_STORE,
128
+ async () => {
129
+ const { KnowledgeStoreService } = await import('./knowledge-store-service.ts');
130
+ return new KnowledgeStoreService();
131
+ },
132
+ {
133
+ allowOverwrite: false,
134
+ enableLogging: true,
135
+ },
136
+ container
137
+ );
138
+
139
+ // Register Metrics Service
140
+ registerService(
141
+ ServiceNames.METRICS,
142
+ () => new MetricsService(),
143
+ {
144
+ allowOverwrite: false,
145
+ enableLogging: true,
146
+ },
147
+ container
148
+ );
149
+
150
+ // Register Writer Queue Service
151
+ // This depends on KnowledgeStoreService being initialized
152
+ registerService(
153
+ ServiceNames.WRITER_QUEUE,
154
+ async () => {
155
+ const storeService = await getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, undefined, container);
156
+ const queue = await storeService.getWriterQueue();
157
+ if (!queue) {
158
+ throw new Error('Writer queue is disabled in configuration');
159
+ }
160
+ return queue;
161
+ },
162
+ {
163
+ allowOverwrite: false,
164
+ enableLogging: true,
165
+ },
166
+ container
167
+ );
168
+
169
+ // Register GPU Resource Service
170
+ // Depends on ProcessLifecycleService
171
+ registerService(
172
+ ServiceNames.GPU_RESOURCE_SERVICE,
173
+ async () => {
174
+ const processLifecycle = await getService<ProcessLifecycleService>(ServiceNames.PROCESS_LIFECYCLE, undefined, container);
175
+ return new GPUResourceService({ processLifecycle });
176
+ },
177
+ {
178
+ allowOverwrite: false,
179
+ enableLogging: true,
180
+ },
181
+ container
182
+ );
183
+
184
+ // Register State Session Manager Service
185
+ // Depends on ProcessLifecycleService
186
+ registerService(
187
+ ServiceNames.STATE_SESSION_MANAGER,
188
+ async () => {
189
+ const processLifecycle = await getService<ProcessLifecycleService>(ServiceNames.PROCESS_LIFECYCLE, undefined, container);
190
+ return new StateSessionManager(processLifecycle);
191
+ },
192
+ {
193
+ allowOverwrite: false,
194
+ enableLogging: true,
195
+ },
196
+ container
197
+ );
198
+
199
+ // Register State Browser Manager Service
200
+ registerService(
201
+ ServiceNames.STATE_BROWSER_MANAGER,
202
+ () => new StateBrowserManager(),
203
+ {
204
+ allowOverwrite: false,
205
+ enableLogging: true,
206
+ },
207
+ container
208
+ );
209
+
210
+ // Register State Metrics Collector Service
211
+ registerService(
212
+ ServiceNames.STATE_METRICS_COLLECTOR,
213
+ () => new StateMetricsCollector(),
214
+ {
215
+ allowOverwrite: false,
216
+ enableLogging: true,
217
+ },
218
+ container
219
+ );
220
+
221
+ // Register State Validator Service
222
+ registerService(
223
+ ServiceNames.STATE_VALIDATOR,
224
+ () => new StateValidator(),
225
+ {
226
+ allowOverwrite: false,
227
+ enableLogging: true,
228
+ },
229
+ container
230
+ );
231
+
232
+ // Register Worker Pool Manager Service
233
+ registerService(
234
+ ServiceNames.WORKER_POOL_MANAGER,
235
+ () => new WorkerPoolManager(),
236
+ {
237
+ allowOverwrite: false,
238
+ enableLogging: true,
239
+ },
240
+ container
241
+ );
242
+
243
+ logger.debug('[InfrastructureServiceInit] Infrastructure services registered');
244
+ }
245
+
246
+ /**
247
+ * Initialize all infrastructure services
248
+ *
249
+ * @param ctx - Optional extension context
250
+ * @param container - Service container instance
251
+ */
252
+ export async function initializeInfrastructureServices(ctx?: any, container: ServiceContainer = getServiceContainer()): Promise<{ success: boolean; initialized: string[]; failed: string[] }> {
253
+ logger.log('[InfrastructureServiceInit] Initializing infrastructure services...');
254
+
255
+ const initialized: string[] = [];
256
+ const failed: string[] = [];
257
+
258
+ // Infrastructure services that should be eagerly initialized
259
+ const eagerServices = [
260
+ { name: ServiceNames.PROCESS_LIFECYCLE, label: 'Process Lifecycle' },
261
+ { name: ServiceNames.STATE_PATH_CONFIGURATION, label: 'Path Configuration' },
262
+ { name: ServiceNames.STATE_MANAGER, label: 'State Manager' },
263
+ { name: ServiceNames.METRICS, label: 'Metrics' },
264
+ ];
265
+
266
+ for (const service of eagerServices) {
267
+ try {
268
+ await getService<IService>(service.name, ctx, container);
269
+ initialized.push(service.label);
270
+ } catch (err) {
271
+ const msg = `${service.label} init failed`;
272
+ logger.error(`[InfrastructureServiceInit] ${msg}:`, err);
273
+ failed.push(msg);
274
+ }
275
+ }
276
+
277
+ return {
278
+ success: failed.length === 0,
279
+ initialized,
280
+ failed
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Shutdown all infrastructure services
286
+ *
287
+ * @param container - Service container instance
288
+ */
289
+ export async function shutdownInfrastructureServices(container: ServiceContainer = getServiceContainer()): Promise<void> {
290
+ logger.log('[InfrastructureServiceInit] Shutting down infrastructure services...');
291
+ await disposeAllServices(container);
292
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * State Backup Manager
3
+ *
4
+ * Handles backup creation, cleanup, and recovery for state files.
5
+ */
6
+
7
+ import * as fs from 'node:fs/promises';
8
+ import * as crypto from 'node:crypto';
9
+ import * as path from 'node:path';
10
+ import { logger } from '../../logger.ts';
11
+ import type { IService } from '../../core/service-registry.ts';
12
+ import { ServiceLifecycle } from '../../core/service-registry.ts';
13
+ import { ServiceNames } from '../../core/interfaces/service-names.ts';
14
+ import { StateValidator } from './state-validator.ts';
15
+ import { CURRENT_STATE_VERSION } from '../types/state-types.ts';
16
+
17
+ // How many newest .quarantine copies to retain. Small: they exist only for forensic
18
+ // inspection of a newer-build/older-reader collision, never for restore.
19
+ const QUARANTINE_KEEP = 5;
20
+
21
+ /**
22
+ * Manages backup and recovery for state files
23
+ */
24
+ export class StateBackupManager implements IService {
25
+ readonly name = ServiceNames.STATE_BACKUP_MANAGER;
26
+ lifecycle = ServiceLifecycle.UNINITIALIZED;
27
+ private _initialized = false;
28
+
29
+ private readonly stateFilePath: string;
30
+ private readonly backupDirPath: string;
31
+ private readonly maxBackups: number;
32
+ // Stateless schema validation/migration; constructed inline (no DI plumbing
33
+ // needed since validateState/migrateAndValidate don't depend on initialize()).
34
+ private readonly validator = new StateValidator();
35
+
36
+ constructor(stateFilePath: string, backupDirPath: string, maxBackups: number = 5) {
37
+ this.stateFilePath = stateFilePath;
38
+ this.backupDirPath = backupDirPath;
39
+ this.maxBackups = maxBackups;
40
+ }
41
+
42
+ async initialize(): Promise<void> {
43
+ if (this._initialized) {
44
+ return;
45
+ }
46
+ this.lifecycle = ServiceLifecycle.INITIALIZING;
47
+ // Ensure backup directory exists
48
+ await fs.mkdir(this.backupDirPath, { recursive: true, mode: 0o700 });
49
+ this._initialized = true;
50
+ this.lifecycle = ServiceLifecycle.INITIALIZED;
51
+ }
52
+
53
+ async dispose(): Promise<void> {
54
+ // Nothing to dispose
55
+ this.lifecycle = ServiceLifecycle.DISPOSED;
56
+ }
57
+
58
+ /**
59
+ * Create a backup of the current state file
60
+ * @throws Error if unable to create backup
61
+ */
62
+ async createBackup(): Promise<void> {
63
+ try {
64
+ // Check if state file exists
65
+ try {
66
+ await fs.access(this.stateFilePath);
67
+ } catch {
68
+ // No state file to backup
69
+ return;
70
+ }
71
+
72
+ // Ensure backup directory exists
73
+ await fs.mkdir(this.backupDirPath, { recursive: true, mode: 0o700 });
74
+
75
+ // Timestamp + random suffix: two writers in the same millisecond would
76
+ // otherwise produce the same filename and silently overwrite one backup
77
+ // generation. The random suffix keeps both. Prefix/suffix still match the
78
+ // `research-state-*.json` patterns used by cleanup and recovery.
79
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
80
+ const backupFileName = `research-state-${timestamp}-${crypto.randomBytes(4).toString('hex')}.json`;
81
+ const backupFilePath = path.join(this.backupDirPath, backupFileName);
82
+
83
+ await fs.copyFile(this.stateFilePath, backupFilePath);
84
+ } catch (error: unknown) {
85
+ throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Clean up old backups, keeping only the most recent maxBackups
91
+ */
92
+ async cleanupOldBackups(): Promise<void> {
93
+ try {
94
+ const entries = await fs.readdir(this.backupDirPath);
95
+
96
+ if (entries.length <= this.maxBackups) {
97
+ return;
98
+ }
99
+
100
+ // Get file stats and sort by modification time (newest first)
101
+ const backupFiles: Array<{ name: string; mtime: Date }> = [];
102
+
103
+ for (const entry of entries) {
104
+ const filePath = path.join(this.backupDirPath, entry);
105
+ const stats = await fs.stat(filePath);
106
+
107
+ if (stats.isFile() && entry.startsWith('research-state-') && entry.endsWith('.json')) {
108
+ backupFiles.push({ name: entry, mtime: stats.mtime });
109
+ }
110
+ }
111
+
112
+ // Sort by mtime, newest first
113
+ backupFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
114
+
115
+ // Remove backups beyond the max count
116
+ const backupsToRemove = backupFiles.slice(this.maxBackups);
117
+ for (const backupFile of backupsToRemove) {
118
+ const filePath = path.join(this.backupDirPath, backupFile.name);
119
+ await fs.unlink(filePath);
120
+ }
121
+ } catch (error: unknown) {
122
+ logger.error(`Failed to cleanup old backups: ${error instanceof Error ? error.message : String(error)}`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Recover from corruption by restoring a backup or writing a default state.
128
+ */
129
+ async recoverFromCorruption(): Promise<void> {
130
+ try {
131
+ // Collect candidate backups, newest first. The newest backup can itself be a
132
+ // partial write (e.g. a crash during createBackup) OR well-formed JSON that
133
+ // nonetheless fails the state schema, so we do NOT blindly restore it. We
134
+ // parse, migrate, and schema-validate each, and restore the first that
135
+ // passes. JSON-parse alone is insufficient: a parseable-but-invalid backup
136
+ // would be restored, fail validation on the next read, and trigger recovery
137
+ // again — a loop that never converges and never persists a usable state.
138
+ let backups: Array<{ name: string; mtime: Date }> = [];
139
+ try {
140
+ const entries = await fs.readdir(this.backupDirPath);
141
+ for (const entry of entries) {
142
+ const filePath = path.join(this.backupDirPath, entry);
143
+ const stats = await fs.stat(filePath);
144
+ if (stats.isFile() && entry.startsWith('research-state-') && entry.endsWith('.json')) {
145
+ backups.push({ name: entry, mtime: stats.mtime });
146
+ }
147
+ }
148
+ } catch {
149
+ // Backup directory unreadable — fall through to default state
150
+ }
151
+ backups.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
152
+
153
+ for (const backup of backups) {
154
+ const backupPath = path.join(this.backupDirPath, backup.name);
155
+ let restoreContent: string;
156
+ try {
157
+ const content = await fs.readFile(backupPath, 'utf-8');
158
+ // Parse + migrate-forward + schema-validate. Persist the validated
159
+ // (and possibly migrated) object so the restored file is a current,
160
+ // well-formed state — never a raw older/odd backup. A future-version
161
+ // or schema-invalid backup throws here and is skipped.
162
+ const validated = this.validator.migrateAndValidate(JSON.parse(content));
163
+ restoreContent = JSON.stringify(validated, null, 2);
164
+ } catch (err) {
165
+ logger.warn(`[StateManager] Skipping invalid backup ${backup.name}: ${err instanceof Error ? err.message : String(err)}`);
166
+ continue;
167
+ }
168
+ // Atomic restore so a crash mid-recovery can't leave a half-written state file.
169
+ await this.atomicWriteStateFile(restoreContent);
170
+ logger.log(`[StateManager] Recovered state from backup: ${backup.name}`);
171
+ return;
172
+ }
173
+
174
+ // No backups (or none valid) — write default state directly (atomic, no lock needed)
175
+ await this.writeDefaultState();
176
+ logger.log('[StateManager] Recovered with default state (no valid backups available)');
177
+ } catch (error: unknown) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ logger.error(`[StateManager] Failed to recover from corruption: ${message}`);
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Preserve a copy of a state file written by a NEWER build before this (older)
186
+ * process continues on an in-memory default. The original is left untouched —
187
+ * the newer build still owns and reads it. The quarantine copy is named so it
188
+ * matches neither the backup-recovery nor cleanup glob (`research-state-*.json`):
189
+ * it must never be restored into an older reader or pruned as an old backup.
190
+ */
191
+ async quarantineFutureState(fileVersion: number): Promise<void> {
192
+ try {
193
+ await fs.access(this.stateFilePath);
194
+ } catch {
195
+ return; // nothing on disk to preserve
196
+ }
197
+ await fs.mkdir(this.backupDirPath, { recursive: true, mode: 0o700 });
198
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
199
+ const name = `research-state-future-v${fileVersion}-${timestamp}.quarantine`;
200
+ const dest = path.join(this.backupDirPath, name);
201
+ await fs.copyFile(this.stateFilePath, dest);
202
+ logger.warn(`[StateManager] Quarantined newer-build state (v${fileVersion}) copy at ${dest}`);
203
+ // Quarantine files are deliberately excluded from cleanupOldBackups (they must never be
204
+ // restored into an older reader), so nothing else prunes them. Cap retention here so a
205
+ // machine that repeatedly runs an older build against newer state (rollback, bisect,
206
+ // flip-flopping installs) can't accumulate them without bound.
207
+ await this.pruneQuarantineFiles(QUARANTINE_KEEP);
208
+ }
209
+
210
+ /** Keep only the newest `keep` *.quarantine files; delete the rest. Best-effort. */
211
+ private async pruneQuarantineFiles(keep: number): Promise<void> {
212
+ try {
213
+ const entries = await fs.readdir(this.backupDirPath);
214
+ const quarantines: Array<{ name: string; mtime: number }> = [];
215
+ for (const entry of entries) {
216
+ if (!entry.endsWith('.quarantine')) continue;
217
+ try {
218
+ const stats = await fs.stat(path.join(this.backupDirPath, entry));
219
+ if (stats.isFile()) quarantines.push({ name: entry, mtime: stats.mtimeMs });
220
+ } catch { /* raced away — ignore */ }
221
+ }
222
+ if (quarantines.length <= keep) return;
223
+ quarantines.sort((a, b) => b.mtime - a.mtime);
224
+ for (const stale of quarantines.slice(keep)) {
225
+ try {
226
+ await fs.unlink(path.join(this.backupDirPath, stale.name));
227
+ } catch { /* already gone — ignore */ }
228
+ }
229
+ } catch { /* backup dir unreadable — nothing to prune */ }
230
+ }
231
+
232
+ /**
233
+ * Atomically write the given content to the state file (temp file + rename, with
234
+ * a Windows copy+delete fallback since rename fails on NTFS when the target exists).
235
+ */
236
+ private async atomicWriteStateFile(content: string): Promise<void> {
237
+ const stateDir = path.dirname(this.stateFilePath);
238
+ await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
239
+ const tempFile = `research-state-${crypto.randomBytes(16).toString('hex')}.tmp`;
240
+ const tempPath = path.join(stateDir, tempFile);
241
+ // 0o600: the state file holds browserServer.authSecret. The primary writer
242
+ // (state-manager) forces owner-only; this recovery/default path must match so a
243
+ // corruption-recovery write never lands world-readable.
244
+ // Open + write + fsync + close before rename, mirroring the primary writer's
245
+ // durability: a crash immediately after recovery must not resurrect the very
246
+ // corruption we just recovered from (an un-synced rename can expose a
247
+ // zero-length file after a power loss).
248
+ const fh = await fs.open(tempPath, 'w', 0o600);
249
+ try {
250
+ await fh.writeFile(content, { encoding: 'utf-8' });
251
+ await fh.sync();
252
+ } finally {
253
+ await fh.close();
254
+ }
255
+ try {
256
+ await fs.rename(tempPath, this.stateFilePath);
257
+ } catch (renameErr) {
258
+ if (process.platform === 'win32') {
259
+ await fs.copyFile(tempPath, this.stateFilePath);
260
+ await fs.unlink(tempPath).catch(() => { /* best-effort cleanup */ });
261
+ } else {
262
+ await fs.unlink(tempPath).catch(() => { /* best-effort cleanup */ });
263
+ throw renameErr;
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Write a default state to the state file
270
+ */
271
+ async writeDefaultState(): Promise<void> {
272
+ const defaultState = this.getDefaultState();
273
+ defaultState.lastUpdated = Date.now();
274
+ await this.atomicWriteStateFile(JSON.stringify(defaultState, null, 2));
275
+ }
276
+
277
+ /**
278
+ * Get the default state object
279
+ */
280
+ private getDefaultState() {
281
+ return {
282
+ version: CURRENT_STATE_VERSION,
283
+ containerId: '',
284
+ containerName: '',
285
+ port: 0,
286
+ sessions: {},
287
+ lastUpdated: Date.now(),
288
+ };
289
+ }
290
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * State Browser API
3
+ *
4
+ * Browser server management API that delegates to StateBrowserManager.
5
+ */
6
+
7
+ import type { StateBrowserManager } from './state-browser-manager.ts';
8
+ import type { SingletonState } from '../types/state-types.ts';
9
+
10
+ /**
11
+ * Provides browser server management operations
12
+ */
13
+ export class StateBrowserApi {
14
+ constructor(private readonly browserManager: StateBrowserManager) {}
15
+
16
+ /**
17
+ * Get the current browser server information
18
+ * @param readState Function to read state
19
+ * @returns Browser server info or null if not set
20
+ */
21
+ async getBrowserServer(
22
+ readState: () => Promise<SingletonState>
23
+ ): Promise<{ port: number; pid: number; schedulerId?: string; startTime?: number | null; authSecret?: string } | null> {
24
+ const state = await readState();
25
+ return this.browserManager.getBrowserServer(state);
26
+ }
27
+
28
+ /**
29
+ * Set the current browser server information (atomic: only overwrites if no live server exists)
30
+ * @param port The browser server port
31
+ * @param pid The browser server process ID
32
+ * @param schedulerId Optional scheduler ID
33
+ * @param updateState Function to update state atomically
34
+ * @param getStartTime Function to get process start time
35
+ */
36
+ async setBrowserServer(
37
+ port: number,
38
+ pid: number,
39
+ schedulerId: string | undefined,
40
+ updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>,
41
+ getStartTime: (pid: number) => Promise<number | null>,
42
+ authSecret?: string
43
+ ): Promise<void> {
44
+ const startTime = await getStartTime(pid);
45
+ await updateState((state) => {
46
+ return this.browserManager.setBrowserServer(state, port, pid, schedulerId, startTime, authSecret);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Clear the browser server information
52
+ * @param updateState Function to update state atomically
53
+ */
54
+ async clearBrowserServer(
55
+ updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>
56
+ ): Promise<void> {
57
+ await updateState((state) => {
58
+ return this.browserManager.clearBrowserServer(state);
59
+ });
60
+ }
61
+
62
+ }