@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,129 @@
1
+ /**
2
+ * Metrics Service
3
+ *
4
+ * Service wrapper that exposes the session-scoped metrics singleton through
5
+ * the service registry. All methods delegate to the global `metrics` export.
6
+ * The service owns no state of its own; metrics belong to the process lifetime
7
+ * and are managed by the SessionMetrics class in utils/metrics.ts.
8
+ */
9
+
10
+ import type { IService } from '../core/service-registry.ts';
11
+ import { ServiceLifecycle } from '../core/service-registry.ts';
12
+ import { logger } from '../logger.ts';
13
+ import { metrics } from '../utils/metrics.ts';
14
+ import type { Labels, IMetricHistogram, IMetricsSnapshot } from '../utils/metrics.ts';
15
+ import { ServiceNames } from '../core/interfaces/service-names.ts';
16
+
17
+ export class MetricsService implements IService {
18
+ readonly name = ServiceNames.METRICS;
19
+ lifecycle = ServiceLifecycle.UNINITIALIZED;
20
+
21
+ async initialize(): Promise<void> {
22
+ if (this.lifecycle === ServiceLifecycle.INITIALIZED) return;
23
+ this.lifecycle = ServiceLifecycle.INITIALIZING;
24
+ logger.debug('[MetricsService] Initializing...');
25
+ this.lifecycle = ServiceLifecycle.INITIALIZED;
26
+ logger.debug('[MetricsService] Initialized');
27
+ }
28
+
29
+ async dispose(): Promise<void> {
30
+ if (this.lifecycle === ServiceLifecycle.DISPOSED) return;
31
+ this.lifecycle = ServiceLifecycle.DISPOSING;
32
+ logger.debug('[MetricsService] Disposing...');
33
+ // Session metrics belong to the process lifetime, not this service.
34
+ // Nothing to clean up — the process is exiting and memory is freed.
35
+ this.lifecycle = ServiceLifecycle.DISPOSED;
36
+ logger.debug('[MetricsService] Disposed');
37
+ }
38
+
39
+ increment(name: string, value: number = 1, labels?: Labels): void {
40
+ metrics.increment(name, value, labels);
41
+ }
42
+
43
+ setGauge(name: string, value: number, labels?: Labels): void {
44
+ metrics.setGauge(name, value, labels);
45
+ }
46
+
47
+ observe(name: string, value: number, labels?: Labels): void {
48
+ metrics.observe(name, value, labels);
49
+ }
50
+
51
+ async measure<T>(name: string, action: () => Promise<T>, labels?: Labels): Promise<T> {
52
+ return metrics.measure(name, action, labels);
53
+ }
54
+
55
+ getSnapshot(): IMetricsSnapshot {
56
+ return metrics.getSessionSnapshot();
57
+ }
58
+
59
+ clear(): void {
60
+ metrics.clearSession();
61
+ }
62
+
63
+ getCounters(): Record<string, number> {
64
+ return metrics.getSessionSnapshot().counters ?? {};
65
+ }
66
+
67
+ getGauges(): Record<string, number> {
68
+ return metrics.getSessionSnapshot().gauges ?? {};
69
+ }
70
+
71
+ getHistograms(): Record<string, IMetricHistogram> {
72
+ return metrics.getSessionSnapshot().histograms ?? {};
73
+ }
74
+
75
+ getCounter(name: string, labels?: Labels): number {
76
+ const counters = this.getCounters();
77
+ if (!labels) return counters[name] ?? 0;
78
+ const keys = Object.keys(labels).sort();
79
+ const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
80
+ const key = labelStr ? `${name}{${labelStr}}` : name;
81
+ return counters[key] ?? 0;
82
+ }
83
+
84
+ getGauge(name: string, labels?: Labels): number {
85
+ const gauges = this.getGauges();
86
+ if (!labels) return gauges[name] ?? 0;
87
+ const keys = Object.keys(labels).sort();
88
+ const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
89
+ const key = labelStr ? `${name}{${labelStr}}` : name;
90
+ return gauges[key] ?? 0;
91
+ }
92
+
93
+ getHistogramStats(name: string, labels?: Labels): IMetricHistogram | null {
94
+ const histograms = this.getHistograms();
95
+ if (!labels) return histograms[name] ?? null;
96
+ const keys = Object.keys(labels).sort();
97
+ const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
98
+ const key = labelStr ? `${name}{${labelStr}}` : name;
99
+ return histograms[key] ?? null;
100
+ }
101
+
102
+ exportPrometheus(): string {
103
+ const snapshot = metrics.getSessionSnapshot();
104
+ const lines: string[] = [];
105
+
106
+ for (const [key, value] of Object.entries(snapshot.counters ?? {})) {
107
+ lines.push(`${key} ${value}`);
108
+ }
109
+ for (const [key, value] of Object.entries(snapshot.gauges ?? {})) {
110
+ lines.push(`${key} ${value}`);
111
+ }
112
+ for (const [key, s] of Object.entries(snapshot.histograms ?? {})) {
113
+ lines.push(`${key}_count ${s.count}`);
114
+ lines.push(`${key}_sum ${(s.avg * s.count).toFixed(2)}`);
115
+ lines.push(`${key}_min ${s.min}`);
116
+ lines.push(`${key}_max ${s.max}`);
117
+ lines.push(`${key}_avg ${s.avg.toFixed(2)}`);
118
+ lines.push(`${key}_p50 ${s.p50.toFixed(2)}`);
119
+ lines.push(`${key}_p90 ${s.p90.toFixed(2)}`);
120
+ lines.push(`${key}_p95 ${s.p95.toFixed(2)}`);
121
+ lines.push(`${key}_p99 ${s.p99.toFixed(2)}`);
122
+ }
123
+
124
+ return lines.join('\n');
125
+ }
126
+ }
127
+
128
+ // Re-export for consumers that import from this module
129
+ export { type Labels, MetricsRegistry } from '../utils/metrics.ts';
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Process Lifecycle Service
3
+ *
4
+ * Provides PID checks and process monitoring functionality.
5
+ * Stateless service for checking if processes are alive.
6
+ */
7
+
8
+ import * as fs from 'node:fs/promises';
9
+ import { execFile } from 'node:child_process';
10
+ import type { IProcessLifecycle } from '../core/interfaces/process-interfaces.ts';
11
+ import { ServiceLifecycle } from '../core/service-registry.ts';
12
+ import { ServiceNames } from '../core/interfaces/service-names.ts';
13
+
14
+ /**
15
+ * Process Lifecycle Service
16
+ *
17
+ * Provides utilities for checking process liveness and monitoring.
18
+ */
19
+ export class ProcessLifecycleService implements IProcessLifecycle {
20
+ readonly name = ServiceNames.PROCESS_LIFECYCLE;
21
+ lifecycle = ServiceLifecycle.UNINITIALIZED;
22
+
23
+ private cachedStartTime: number | null = null;
24
+
25
+ async initialize(): Promise<void> {
26
+ this.lifecycle = ServiceLifecycle.INITIALIZED;
27
+ }
28
+
29
+ async dispose(): Promise<void> {
30
+ // Stateless service
31
+ }
32
+
33
+ /**
34
+ * Get the start time of a process (Linux only).
35
+ * Combined with PID, this provides a globally unique identifier for a process
36
+ * even if PIDs are recycled by the OS.
37
+ *
38
+ * @param pid Process ID
39
+ * @returns Start time identifier (Linux: jiffies; cross-platform: epoch seconds) or null
40
+ */
41
+ async getProcessStartTime(pid: number): Promise<number | null> {
42
+ // Linux: read from /proc/{pid}/stat (field 22 = starttime in jiffies)
43
+ if (process.platform === 'linux') {
44
+ try {
45
+ const stat = await fs.readFile(`/proc/${pid}/stat`, 'utf8');
46
+ const lastParenIndex = stat.lastIndexOf(')');
47
+ if (lastParenIndex === -1) return null;
48
+ const partsAfterName = stat.substring(lastParenIndex + 2).trim().split(/\s+/);
49
+ const startTimeStr = partsAfterName[19];
50
+ return startTimeStr ? parseInt(startTimeStr, 10) : null;
51
+ } catch (_err) {
52
+ // ENOENT is common if process just died
53
+ return null;
54
+ }
55
+ }
56
+
57
+ // Windows: use powershell to get process creation time
58
+ if (process.platform === 'win32') {
59
+ try {
60
+ const output = await new Promise<string>((resolve, reject) => {
61
+ const child = execFile(
62
+ 'powershell',
63
+ ['-NoProfile', '-Command', `[Math]::Floor(([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeSeconds())`],
64
+ { encoding: 'utf8', timeout: 5000 },
65
+ (err, stdout) => err ? reject(err) : resolve(stdout),
66
+ );
67
+ child.unref?.();
68
+ });
69
+ if (!output || !output.trim()) return null;
70
+ const startTime = parseInt(output.trim(), 10);
71
+ return isNaN(startTime) ? null : startTime;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ // Cross-platform fallback (macOS/Unix): `ps -o etimes=` returns elapsed seconds.
78
+ try {
79
+ const output = await new Promise<string>((resolve, reject) => {
80
+ const child = execFile(
81
+ 'ps', ['-o', 'etimes=', '-p', String(pid)],
82
+ { encoding: 'utf8', timeout: 3000 },
83
+ (err, stdout) => err ? reject(err) : resolve(stdout),
84
+ );
85
+ child.unref?.();
86
+ });
87
+ if (!output || !output.trim()) return null;
88
+ const elapsedSec = parseInt(output.trim(), 10);
89
+ if (isNaN(elapsedSec)) return null;
90
+ return Math.floor(Date.now() / 1000) - elapsedSec;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get the start time for the current process
98
+ */
99
+ async getCurrentProcessStartTime(): Promise<number | null> {
100
+ if (this.cachedStartTime !== null) return this.cachedStartTime;
101
+ this.cachedStartTime = await this.getProcessStartTime(process.pid);
102
+ return this.cachedStartTime;
103
+ }
104
+
105
+ /**
106
+ * Check if a process is alive by sending signal 0.
107
+ * Optionally verifies that the start time matches to prevent PID reuse races.
108
+ *
109
+ * @param pid Process ID to check
110
+ * @param expectedStartTime Optional start time to verify (from getProcessStartTime)
111
+ * @returns true if process is alive (and matches start time if provided)
112
+ */
113
+ async isProcessAlive(pid: number, expectedStartTime?: number | null): Promise<boolean> {
114
+ try {
115
+ // 1. Basic liveness check via signal 0
116
+ process.kill(pid, 0);
117
+
118
+ // 2. If start time verification requested, check it
119
+ if (expectedStartTime !== undefined && expectedStartTime !== null) {
120
+ const actualStartTime = await this.getProcessStartTime(pid);
121
+ if (actualStartTime === null) {
122
+ // If we can't get start time but expected it, we can't be sure it's the same process
123
+ // On Linux this usually means the process just exited between the signal and the read.
124
+ return false;
125
+ }
126
+ // Allow ±1s slack. The macOS/BSD fallback derives start time as
127
+ // floor(Date.now()/1000) - `ps etimes` (elapsed whole seconds); both terms are
128
+ // independently quantized, so the SAME process can compute start times 1s apart
129
+ // across two reads. A strict === there makes a live lock/leader owner read as
130
+ // dead → the lock is reclaimed from under a live writer (lost update). PID reuse
131
+ // within a 1s window at the identical PID is not a realistic false-match.
132
+ return Math.abs(actualStartTime - expectedStartTime) <= 1;
133
+ }
134
+
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Sync version of basic liveness check (no start-time verification)
143
+ */
144
+ isProcessAliveSync(pid: number): boolean {
145
+ try {
146
+ process.kill(pid, 0);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Check if a process is alive with optional scheduler ID verification.
155
+ * This method is designed to work with state that contains scheduler information.
156
+ *
157
+ * @param pid Process ID to check
158
+ * @param expectedSchedulerId Optional scheduler ID to verify
159
+ * @param getState Function to retrieve current state for scheduler ID verification
160
+ * @param skipLock If true, skip locking when retrieving state (useful to prevent deadlocks)
161
+ * @returns true if process is alive and (if provided) matches expected scheduler ID
162
+ */
163
+ async isPidAlive<TState>(
164
+ pid: number,
165
+ expectedSchedulerId?: string,
166
+ options?: {
167
+ getState?: (skipLock?: boolean) => Promise<TState>;
168
+ skipLock?: boolean;
169
+ getSchedulerIdFromState?: (state: TState) => string | undefined;
170
+ expectedStartTime?: number | null;
171
+ }
172
+ ): Promise<boolean> {
173
+ const alive = await this.isProcessAlive(pid, options?.expectedStartTime);
174
+ if (!alive) return false;
175
+
176
+ if (expectedSchedulerId && options?.getState && options?.getSchedulerIdFromState) {
177
+ // Use skipLock if true to prevent deadlocks when called inside updateState
178
+ const state = await options.getState(options.skipLock);
179
+ const schedulerId = options.getSchedulerIdFromState(state);
180
+ return schedulerId === expectedSchedulerId;
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+ /**
187
+ * Get the current process ID
188
+ */
189
+ getCurrentPid(): number {
190
+ return process.pid;
191
+ }
192
+
193
+ /**
194
+ * Wait for a process to terminate
195
+ *
196
+ * @param pid Process ID to wait for
197
+ * @param timeoutMs Maximum time to wait in milliseconds
198
+ * @param checkIntervalMs Interval between checks in milliseconds
199
+ * @param expectedStartTime Optional start time to verify
200
+ * @returns true if process terminated, false if timeout
201
+ */
202
+ async waitForProcessTermination(
203
+ pid: number,
204
+ timeoutMs: number = 5000,
205
+ checkIntervalMs: number = 100,
206
+ expectedStartTime?: number | null
207
+ ): Promise<boolean> {
208
+ const startTime = Date.now();
209
+
210
+ while (Date.now() - startTime < timeoutMs) {
211
+ if (!(await this.isProcessAlive(pid, expectedStartTime))) {
212
+ return true;
213
+ }
214
+ await this.sleep(checkIntervalMs);
215
+ }
216
+
217
+ return false;
218
+ }
219
+
220
+ /**
221
+ * Check if the current process is the one with the given PID
222
+ *
223
+ * @param pid Process ID to check
224
+ * @returns true if the current process has the given PID
225
+ */
226
+ isCurrentProcess(pid: number): boolean {
227
+ return process.pid === pid;
228
+ }
229
+
230
+ /**
231
+ * Get process information (platform-specific)
232
+ *
233
+ * @param pid Process ID to get information for
234
+ * @returns Process information or null if not available
235
+ */
236
+ async getProcessInfo(pid: number): Promise<{ pid: number; alive: boolean; startTime?: number | null } | null> {
237
+ const startTime = await this.getProcessStartTime(pid);
238
+ const alive = await this.isProcessAlive(pid, startTime);
239
+ return {
240
+ pid,
241
+ alive,
242
+ startTime,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Sleep for a specified number of milliseconds
248
+ * @param ms The number of milliseconds to sleep
249
+ */
250
+ private sleep(ms: number): Promise<void> {
251
+ return new Promise<void>((resolve) => setTimeout(resolve, ms));
252
+ }
253
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Scheduler Factory Service
3
+ *
4
+ * Implements the ISchedulerFactory interface for creating scheduler instances.
5
+ * This module lives in the Infrastructure layer and provides concrete implementations
6
+ * of scheduler creation logic.
7
+ */
8
+
9
+ import type { Config } from '../config.ts';
10
+ import { ServiceLifecycle, getServiceContainer, type ServiceContainer } from '../core/service-registry.ts';
11
+ import type { IService } from '../core/service-registry.ts';
12
+ import type { ISchedulerFactory, IScheduler } from '../core/scheduler-factory.ts';
13
+ import { getScheduler as _getScheduler, forceSchedulerRestart as _forceSchedulerRestart, disposeBrowserInitLock } from './browser/scheduler-factory.ts';
14
+ import { getSchedulerVersion as _getSchedulerVersion } from './browser/config.ts';
15
+ import { ServiceNames } from '../core/interfaces/service-names.ts';
16
+
17
+ /**
18
+ * Scheduler Factory Service Implementation
19
+ */
20
+ export class SchedulerFactoryService implements ISchedulerFactory, IService {
21
+ readonly name = ServiceNames.SCHEDULER_FACTORY;
22
+ lifecycle = ServiceLifecycle.UNINITIALIZED;
23
+ private container: ServiceContainer = getServiceContainer();
24
+
25
+ async initialize(ctx?: any): Promise<void> {
26
+ if (ctx?.container) {
27
+ this.container = ctx.container;
28
+ }
29
+ this.lifecycle = ServiceLifecycle.INITIALIZED;
30
+ }
31
+
32
+ async dispose(): Promise<void> {
33
+ await disposeBrowserInitLock(this.container);
34
+ this.lifecycle = ServiceLifecycle.DISPOSED;
35
+ }
36
+
37
+ /**
38
+ * Get or create a scheduler instance.
39
+ * Handles leader election and client/server mode switching.
40
+ */
41
+ async getScheduler(config?: Config): Promise<IScheduler> {
42
+ return _getScheduler(config, this.container);
43
+ }
44
+
45
+ /**
46
+ * Get the current scheduler version string.
47
+ */
48
+ getSchedulerVersion(config?: Config): string {
49
+ return _getSchedulerVersion(config);
50
+ }
51
+
52
+ /**
53
+ * Force a restart of the scheduler by clearing the global cache and state.
54
+ */
55
+ async forceSchedulerRestart(forceClearRemoteState?: boolean): Promise<void> {
56
+ return _forceSchedulerRestart(forceClearRemoteState, this.container);
57
+ }
58
+ }