@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.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { logger } from '../../logger.ts';
|
|
2
|
+
|
|
3
|
+
export type TaskType = 'search' | 'scrape' | 'healthcheck';
|
|
4
|
+
|
|
5
|
+
export interface QueuedTask<T> {
|
|
6
|
+
type: TaskType;
|
|
7
|
+
fn: () => Promise<T>;
|
|
8
|
+
resolve: (val: T) => void;
|
|
9
|
+
reject: (err: any) => void;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A priority-aware task queue for browser operations.
|
|
15
|
+
*
|
|
16
|
+
* Ensures that 'search' tasks (used by evaluators and for planning) jump ahead
|
|
17
|
+
* of 'scrape' tasks (heavy data gathering) in the queue.
|
|
18
|
+
*
|
|
19
|
+
* This helps prevent search starvation when the worker pool is saturated by
|
|
20
|
+
* long-running scrapes.
|
|
21
|
+
*/
|
|
22
|
+
export class PriorityTaskQueue {
|
|
23
|
+
private searchQueue: QueuedTask<any>[] = [];
|
|
24
|
+
private healthcheckQueue: QueuedTask<any>[] = [];
|
|
25
|
+
private scrapeQueue: QueuedTask<any>[] = [];
|
|
26
|
+
private activeCount = 0;
|
|
27
|
+
private maxTotalConcurrency: number;
|
|
28
|
+
private readonly maxQueueDepth: number;
|
|
29
|
+
private isShutdown = false;
|
|
30
|
+
|
|
31
|
+
constructor(maxTotalConcurrency: number, maxQueueDepth = 500) {
|
|
32
|
+
this.maxTotalConcurrency = maxTotalConcurrency;
|
|
33
|
+
this.maxQueueDepth = maxQueueDepth;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enqueue a task with priority.
|
|
38
|
+
* Searches and healthchecks have high priority.
|
|
39
|
+
*/
|
|
40
|
+
enqueue<T>(type: TaskType, fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
41
|
+
return new Promise<T>((resolve, reject) => {
|
|
42
|
+
const task: QueuedTask<T> = { type, fn, resolve, reject, signal };
|
|
43
|
+
|
|
44
|
+
if (this.isShutdown) {
|
|
45
|
+
return reject(new Error(`PriorityTaskQueue is shutting down; rejecting ${type} task`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
return reject(new Error(`Task ${type} aborted before enqueuing`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const totalDepth = this.healthcheckQueue.length + this.searchQueue.length + this.scrapeQueue.length;
|
|
53
|
+
if (totalDepth >= this.maxQueueDepth) {
|
|
54
|
+
return reject(new Error(`PriorityTaskQueue at capacity (${this.maxQueueDepth} tasks). Dropping ${type} task.`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let onAbort: (() => void) | undefined;
|
|
58
|
+
if (signal) {
|
|
59
|
+
onAbort = () => {
|
|
60
|
+
if (this.removeFromQueue(task)) {
|
|
61
|
+
task.reject(new Error(`Task ${type} aborted while in queue`));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Wrap resolve/reject to ensure listener cleanup
|
|
68
|
+
const wrappedResolve = (val: T) => {
|
|
69
|
+
if (signal && onAbort) signal.removeEventListener('abort', onAbort);
|
|
70
|
+
resolve(val);
|
|
71
|
+
};
|
|
72
|
+
const wrappedReject = (err: any) => {
|
|
73
|
+
if (signal && onAbort) signal.removeEventListener('abort', onAbort);
|
|
74
|
+
reject(err);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
task.resolve = wrappedResolve;
|
|
78
|
+
task.reject = wrappedReject;
|
|
79
|
+
|
|
80
|
+
if (type === 'healthcheck') {
|
|
81
|
+
this.healthcheckQueue.push(task);
|
|
82
|
+
} else if (type === 'search') {
|
|
83
|
+
this.searchQueue.push(task);
|
|
84
|
+
} else {
|
|
85
|
+
this.scrapeQueue.push(task);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.debug(`[PriorityQueue] Task enqueued: ${type}. Active: ${this.activeCount}, Capacity: ${this.maxTotalConcurrency}. Queues: H:${this.healthcheckQueue.length} S:${this.searchQueue.length} SC:${this.scrapeQueue.length}`);
|
|
89
|
+
this.process();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private removeFromQueue(task: QueuedTask<any>): boolean {
|
|
94
|
+
const queues = [this.healthcheckQueue, this.searchQueue, this.scrapeQueue];
|
|
95
|
+
for (const q of queues) {
|
|
96
|
+
const idx = q.indexOf(task);
|
|
97
|
+
if (idx !== -1) {
|
|
98
|
+
q.splice(idx, 1);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private process() {
|
|
106
|
+
// Fill available slots
|
|
107
|
+
while (this.activeCount < this.maxTotalConcurrency) {
|
|
108
|
+
let task: QueuedTask<any> | undefined;
|
|
109
|
+
|
|
110
|
+
if (this.healthcheckQueue.length > 0) {
|
|
111
|
+
task = this.healthcheckQueue.shift();
|
|
112
|
+
} else if (this.searchQueue.length > 0) {
|
|
113
|
+
task = this.searchQueue.shift();
|
|
114
|
+
} else if (this.scrapeQueue.length > 0) {
|
|
115
|
+
task = this.scrapeQueue.shift();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!task) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If task was aborted while in queue, reject it and continue to the next one
|
|
123
|
+
// without incrementing activeCount or calling runTask.
|
|
124
|
+
if (task.signal?.aborted) {
|
|
125
|
+
task.reject(new Error(`Task ${task.type} aborted before starting`));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.runTask(task);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async runTask(task: QueuedTask<any>) {
|
|
134
|
+
// Double check abortion right before incrementing activeCount (redundant but safe)
|
|
135
|
+
if (task.signal?.aborted) {
|
|
136
|
+
task.reject(new Error(`Task ${task.type} aborted before starting`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.activeCount++;
|
|
141
|
+
logger.debug(`[PriorityQueue] Starting task: ${task.type}. Active: ${this.activeCount}/${this.maxTotalConcurrency}`);
|
|
142
|
+
|
|
143
|
+
// Race the task against caller abort. Without this, an abort while the
|
|
144
|
+
// task is RUNNING is a no-op (the queue-level abort listener only removes
|
|
145
|
+
// queued tasks), so a long task pins its concurrency slot until its own
|
|
146
|
+
// timeout fires — starving every other queued task. We cannot kill the
|
|
147
|
+
// underlying worker job from here (that needs IPC-level cancellation, a
|
|
148
|
+
// separate concern), so the orphaned fn() keeps running until it settles
|
|
149
|
+
// on its own; we swallow its late settlement and free the slot now so the
|
|
150
|
+
// queue can dispatch the next task immediately.
|
|
151
|
+
let onAbort: (() => void) | undefined;
|
|
152
|
+
const fnPromise = task.fn();
|
|
153
|
+
// Prevent an unhandled rejection if abort wins the race and fn() rejects later.
|
|
154
|
+
fnPromise.catch(() => { /* handled via the race below or abandoned on abort */ });
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
let result: any;
|
|
158
|
+
if (task.signal) {
|
|
159
|
+
const abortPromise = new Promise<never>((_, rej) => {
|
|
160
|
+
onAbort = () => rej(new Error(`Task ${task.type} aborted while running`));
|
|
161
|
+
task.signal!.addEventListener('abort', onAbort, { once: true });
|
|
162
|
+
});
|
|
163
|
+
result = await Promise.race([fnPromise, abortPromise]);
|
|
164
|
+
} else {
|
|
165
|
+
result = await fnPromise;
|
|
166
|
+
}
|
|
167
|
+
task.resolve(result);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
task.reject(err);
|
|
170
|
+
} finally {
|
|
171
|
+
if (task.signal && onAbort) task.signal.removeEventListener('abort', onAbort);
|
|
172
|
+
this.activeCount--;
|
|
173
|
+
logger.debug(`[PriorityQueue] Task finished: ${task.type}. Active: ${this.activeCount}/${this.maxTotalConcurrency}`);
|
|
174
|
+
// Check for more tasks on next tick
|
|
175
|
+
process.nextTick(() => this.process());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Update the maximum concurrency limit (e.g. if config changes).
|
|
181
|
+
*/
|
|
182
|
+
updateConcurrency(maxTotalConcurrency: number) {
|
|
183
|
+
this.maxTotalConcurrency = maxTotalConcurrency;
|
|
184
|
+
logger.debug(`[PriorityQueue] Concurrency updated to ${maxTotalConcurrency}`);
|
|
185
|
+
this.process();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* FIX (#18): Shut down the queue, rejecting all pending tasks.
|
|
190
|
+
* In-progress tasks are allowed to complete. This prevents the queue from
|
|
191
|
+
* holding unsettled promises that would hang the calling orchestrator.
|
|
192
|
+
*/
|
|
193
|
+
shutdown(): void {
|
|
194
|
+
// Latch shutdown so any enqueue() racing with (or following) teardown is
|
|
195
|
+
// rejected instead of landing in a just-cleared queue and dispatching onto a
|
|
196
|
+
// pool that is mid-destroy.
|
|
197
|
+
this.isShutdown = true;
|
|
198
|
+
const error = new Error('PriorityTaskQueue is shutting down');
|
|
199
|
+
for (const task of this.healthcheckQueue) {
|
|
200
|
+
task.reject(error);
|
|
201
|
+
}
|
|
202
|
+
for (const task of this.searchQueue) {
|
|
203
|
+
task.reject(error);
|
|
204
|
+
}
|
|
205
|
+
for (const task of this.scrapeQueue) {
|
|
206
|
+
task.reject(error);
|
|
207
|
+
}
|
|
208
|
+
this.healthcheckQueue.length = 0;
|
|
209
|
+
this.searchQueue.length = 0;
|
|
210
|
+
this.scrapeQueue.length = 0;
|
|
211
|
+
logger.debug(`[PriorityQueue] Shutdown complete. Active tasks: ${this.activeCount}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get current status for metrics or logging.
|
|
216
|
+
*/
|
|
217
|
+
getStats() {
|
|
218
|
+
return {
|
|
219
|
+
activeCount: this.activeCount,
|
|
220
|
+
searchQueueDepth: this.searchQueue.length,
|
|
221
|
+
scrapeQueueDepth: this.scrapeQueue.length,
|
|
222
|
+
healthcheckQueueDepth: this.healthcheckQueue.length,
|
|
223
|
+
capacity: this.maxTotalConcurrency
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler Factory
|
|
3
|
+
*
|
|
4
|
+
* Handles the creation and management of scheduler instances.
|
|
5
|
+
* Refactored from browser-manager.ts for better separation of concerns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
import * as net from 'node:net';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import type { Config } from '../../config.ts';
|
|
12
|
+
import { logger } from '../../logger.ts';
|
|
13
|
+
import { metrics } from '../../utils/metrics.ts';
|
|
14
|
+
import { getService, tryGetService, getServiceContainer } from '../../core/service-registry.ts';
|
|
15
|
+
import type { ServiceContainer } from '../../core/service-registry.ts';
|
|
16
|
+
import { ServiceNames } from '../../core/service-interfaces.ts';
|
|
17
|
+
import type { IProcessLifecycle } from '../../core/service-interfaces.ts';
|
|
18
|
+
import type { ISchedulerInternals } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
19
|
+
import type { IStateManager } from '../../core/interfaces/state-manager-interfaces.ts';
|
|
20
|
+
import type { StatePathConfiguration } from '../state/state-path-configuration.ts';
|
|
21
|
+
import { FileLockService } from '../file-lock-service.ts';
|
|
22
|
+
import { generateSchedulerVersion } from './config.ts';
|
|
23
|
+
import { BrowserClient } from './browser-client.ts';
|
|
24
|
+
import { BrowserTaskScheduler } from './browser-task-scheduler.ts';
|
|
25
|
+
import type { IScheduler } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
26
|
+
import { getBrowserServerAuthSecret } from './browser-server.ts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Global map for browser scheduler initialization locks to prevent "thundering herd"
|
|
30
|
+
* while allowing isolation per service container.
|
|
31
|
+
*/
|
|
32
|
+
const browserInitLocks = new Map<ServiceContainer, FileLockService>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Dispose the browser initialization lock for a specific container.
|
|
36
|
+
*/
|
|
37
|
+
export async function disposeBrowserInitLock(container: ServiceContainer = getServiceContainer()): Promise<void> {
|
|
38
|
+
const lock = browserInitLocks.get(container);
|
|
39
|
+
if (lock) {
|
|
40
|
+
try {
|
|
41
|
+
await lock.dispose();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.debug('[SchedulerFactory] Error disposing browserInitLock:', err);
|
|
44
|
+
}
|
|
45
|
+
browserInitLocks.delete(container);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the browser initialization lock for a specific container.
|
|
51
|
+
*/
|
|
52
|
+
async function getBrowserInitLock(container: ServiceContainer): Promise<FileLockService> {
|
|
53
|
+
let lock = browserInitLocks.get(container);
|
|
54
|
+
if (lock) return lock;
|
|
55
|
+
|
|
56
|
+
const pathConfig = await getService<StatePathConfiguration>(ServiceNames.STATE_PATH_CONFIGURATION, undefined, container);
|
|
57
|
+
const processLifecycle = await getService<IProcessLifecycle>(ServiceNames.PROCESS_LIFECYCLE, undefined, container);
|
|
58
|
+
const lockFilePath = path.join(pathConfig.getLockDirPath(), 'browser-init.lock');
|
|
59
|
+
|
|
60
|
+
lock = new FileLockService({
|
|
61
|
+
lockFilePath,
|
|
62
|
+
lockTimeout: 60000, // 60s timeout for browser startup
|
|
63
|
+
lockRetries: 600,
|
|
64
|
+
lockRetryDelay: 100,
|
|
65
|
+
lockStaleThreshold: 60000, // 60s stale threshold (must be <= lockTimeout)
|
|
66
|
+
processLifecycle,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await lock.initialize();
|
|
70
|
+
browserInitLocks.set(container, lock);
|
|
71
|
+
return lock;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Probe whether a TCP port is accepting connections.
|
|
76
|
+
*/
|
|
77
|
+
async function isPortListening(port: number, timeoutMs = 2000): Promise<boolean> {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const socket = new net.Socket();
|
|
80
|
+
let settled = false;
|
|
81
|
+
const settle = (ok: boolean) => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
socket.destroy();
|
|
85
|
+
resolve(ok);
|
|
86
|
+
};
|
|
87
|
+
socket.setTimeout(timeoutMs);
|
|
88
|
+
socket.once('connect', () => settle(true));
|
|
89
|
+
socket.once('error', () => settle(false));
|
|
90
|
+
socket.once('timeout', () => settle(false));
|
|
91
|
+
socket.connect(port, '127.0.0.1');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Force a restart of the scheduler by clearing the global cache and state.
|
|
97
|
+
*/
|
|
98
|
+
export async function forceSchedulerRestart(forceClearRemoteState: boolean = false, container: ServiceContainer = getServiceContainer()): Promise<void> {
|
|
99
|
+
// Try to get scheduler service
|
|
100
|
+
const schedulerService = tryGetService<ISchedulerInternals>(ServiceNames.SCHEDULER, container);
|
|
101
|
+
|
|
102
|
+
if (!schedulerService) {
|
|
103
|
+
logger.warn('[Scheduler] Scheduler service not available for restart');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (schedulerService.isSchedulerRestartInProgress()) {
|
|
108
|
+
logger.log('[Scheduler] Restart already in progress, skipping concurrent call.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
schedulerService.setSchedulerRestartInProgress(true);
|
|
112
|
+
try {
|
|
113
|
+
logger.log('[Scheduler] Forcing scheduler restart due to config change...');
|
|
114
|
+
|
|
115
|
+
const oldScheduler = schedulerService.getSchedulerInstance();
|
|
116
|
+
|
|
117
|
+
schedulerService.setSchedulerInstance(null);
|
|
118
|
+
schedulerService.setSchedulerVersion(null);
|
|
119
|
+
schedulerService.setSchedulerInitializationPromise(null);
|
|
120
|
+
|
|
121
|
+
const stateManager = await getService<IStateManager>(ServiceNames.STATE_MANAGER, undefined, container);
|
|
122
|
+
const serverInfo = await stateManager.getBrowserServer();
|
|
123
|
+
let shouldClearState = true;
|
|
124
|
+
if (serverInfo) {
|
|
125
|
+
const isAlive = await stateManager.isPidAlive(serverInfo.pid, serverInfo.schedulerId);
|
|
126
|
+
if (isAlive && serverInfo.pid !== process.pid && !forceClearRemoteState) {
|
|
127
|
+
logger.log(`[Scheduler] Skipping clearBrowserServer — live scheduler (PID ${serverInfo.pid}) owns state.`);
|
|
128
|
+
shouldClearState = false;
|
|
129
|
+
} else if (forceClearRemoteState) {
|
|
130
|
+
logger.log(`[Scheduler] Force clearing remote state for PID ${serverInfo.pid} due to unreachability.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (shouldClearState) {
|
|
135
|
+
await stateManager.clearBrowserServer().catch((error) => {
|
|
136
|
+
logger.warn('[Scheduler] Failed to clear browser server from state:', error);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (oldScheduler && 'schedulerId' in oldScheduler && oldScheduler.schedulerId) {
|
|
141
|
+
if (oldScheduler instanceof BrowserTaskScheduler) {
|
|
142
|
+
const shutdownPromise: Promise<void> = oldScheduler.shutdown().catch((err: unknown) => {
|
|
143
|
+
logger.warn('[Scheduler] Error during old scheduler shutdown after restart:', err);
|
|
144
|
+
});
|
|
145
|
+
schedulerService.setPendingShutdownPromise(shutdownPromise);
|
|
146
|
+
shutdownPromise.finally(() => {
|
|
147
|
+
if (schedulerService.getPendingShutdownPromise() === shutdownPromise) {
|
|
148
|
+
schedulerService.setPendingShutdownPromise(null);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logger.log('[Scheduler] Restart complete. Next call will create fresh scheduler.');
|
|
155
|
+
} finally {
|
|
156
|
+
schedulerService.setSchedulerRestartInProgress(false);
|
|
157
|
+
await disposeBrowserInitLock(container).catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get or create a scheduler instance.
|
|
163
|
+
*/
|
|
164
|
+
export async function getScheduler(config?: Config, container: ServiceContainer = getServiceContainer()): Promise<IScheduler> {
|
|
165
|
+
const schedulerService = await getService<ISchedulerInternals>(ServiceNames.SCHEDULER, undefined, container);
|
|
166
|
+
|
|
167
|
+
const currentVersion = generateSchedulerVersion(config);
|
|
168
|
+
let existing = schedulerService.getSchedulerInstance();
|
|
169
|
+
const cachedVersion = schedulerService.getSchedulerVersion();
|
|
170
|
+
|
|
171
|
+
if (existing && cachedVersion && cachedVersion !== currentVersion) {
|
|
172
|
+
logger.log(`[Scheduler] Config changed (old: ${cachedVersion}, new: ${currentVersion}), forcing restart...`);
|
|
173
|
+
await forceSchedulerRestart(false, container);
|
|
174
|
+
existing = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (existing) {
|
|
178
|
+
if ('resetIdleTimerOnActivity' in existing && typeof existing['resetIdleTimerOnActivity'] === 'function') {
|
|
179
|
+
existing['resetIdleTimerOnActivity']();
|
|
180
|
+
}
|
|
181
|
+
return existing as IScheduler;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const existingPromise = schedulerService.getSchedulerInitializationPromise();
|
|
185
|
+
if (existingPromise) return existingPromise as Promise<IScheduler>;
|
|
186
|
+
|
|
187
|
+
let p: Promise<IScheduler>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* True once this initialization must no longer install its product:
|
|
191
|
+
* either the owning container started disposing, or the init-promise slot
|
|
192
|
+
* no longer holds `p` (cleared by forceSchedulerRestart or by
|
|
193
|
+
* SchedulerService.dispose()). Must be re-checked after EVERY await point
|
|
194
|
+
* that could complete post-disposal — a scheduler installed past this
|
|
195
|
+
* check would be a live BrowserTaskScheduler (with a ref'd, listening
|
|
196
|
+
* HTTP server) that nothing will ever shut down.
|
|
197
|
+
*/
|
|
198
|
+
const isSuperseded = () =>
|
|
199
|
+
container.isDisposing || schedulerService.getSchedulerInitializationPromise() !== p;
|
|
200
|
+
|
|
201
|
+
const initializationFunction = async () => {
|
|
202
|
+
const lock = await getBrowserInitLock(container);
|
|
203
|
+
|
|
204
|
+
return await lock.withLock(async () => {
|
|
205
|
+
if (isSuperseded()) {
|
|
206
|
+
throw new Error('Initialization superseded');
|
|
207
|
+
}
|
|
208
|
+
const schedulerVersion = currentVersion;
|
|
209
|
+
const schedulerId = crypto.randomUUID();
|
|
210
|
+
|
|
211
|
+
const stateManager = await getService<IStateManager>(ServiceNames.STATE_MANAGER, undefined, container);
|
|
212
|
+
const serverInfo = await stateManager.getBrowserServer();
|
|
213
|
+
|
|
214
|
+
if (serverInfo) {
|
|
215
|
+
const isAlive = await stateManager.isPidAlive(serverInfo.pid, serverInfo.schedulerId);
|
|
216
|
+
if (isAlive) {
|
|
217
|
+
const state = await stateManager.readState();
|
|
218
|
+
const storedVersion = state.schedulerVersion;
|
|
219
|
+
|
|
220
|
+
if (storedVersion && storedVersion !== currentVersion) {
|
|
221
|
+
logger.log(`[Scheduler] Existing scheduler has stale config (old: ${storedVersion}, new: ${currentVersion}), forcing restart...`);
|
|
222
|
+
if (serverInfo.pid !== process.pid) {
|
|
223
|
+
logger.log(`[Scheduler] Bypassing stale scheduler process (PID ${serverInfo.pid}) by clearing state...`);
|
|
224
|
+
}
|
|
225
|
+
await stateManager.clearBrowserServer();
|
|
226
|
+
} else {
|
|
227
|
+
const portOk = await isPortListening(serverInfo.port);
|
|
228
|
+
if (!portOk) {
|
|
229
|
+
logger.warn(`[Scheduler] PID ${serverInfo.pid} is alive but port ${serverInfo.port} is not listening — clearing stale state and starting fresh.`);
|
|
230
|
+
await stateManager.clearBrowserServer().catch((e) => {
|
|
231
|
+
logger.warn('[Scheduler] Failed to clear stale browser server state:', e);
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
logger.log(`[Scheduler] Connecting to existing scheduler (version: ${currentVersion})`);
|
|
235
|
+
const client = new BrowserClient(serverInfo.port, serverInfo.authSecret);
|
|
236
|
+
|
|
237
|
+
if (!isSuperseded()) {
|
|
238
|
+
schedulerService.setSchedulerInstance(client);
|
|
239
|
+
schedulerService.setSchedulerVersion(currentVersion);
|
|
240
|
+
} else {
|
|
241
|
+
logger.warn('[Scheduler] Initialization finished but was superseded by a restart or disposal. Disposing...');
|
|
242
|
+
await client.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
243
|
+
throw new Error('Initialization superseded');
|
|
244
|
+
}
|
|
245
|
+
return client;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Last check before creating a scheduler with a listening server —
|
|
252
|
+
// several state-manager awaits above could have completed after a
|
|
253
|
+
// disposal or restart began.
|
|
254
|
+
if (isSuperseded()) {
|
|
255
|
+
throw new Error('Initialization superseded');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const scheduler = new BrowserTaskScheduler(schedulerId, stateManager, container);
|
|
259
|
+
let port: number;
|
|
260
|
+
try {
|
|
261
|
+
port = await scheduler.startServer();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error('[Scheduler] Failed to start server, running standalone:', error);
|
|
264
|
+
if (!isSuperseded()) {
|
|
265
|
+
schedulerService.setSchedulerInstance(scheduler);
|
|
266
|
+
schedulerService.setSchedulerVersion(currentVersion);
|
|
267
|
+
} else {
|
|
268
|
+
await scheduler.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
269
|
+
throw new Error('Initialization superseded', { cause: error });
|
|
270
|
+
}
|
|
271
|
+
return scheduler;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// startServer() bound a ref'd HTTP server; if disposal or a restart
|
|
275
|
+
// won the race while it was starting, tear it down instead of
|
|
276
|
+
// proceeding to register it as election leader.
|
|
277
|
+
if (isSuperseded()) {
|
|
278
|
+
await scheduler.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
279
|
+
throw new Error('Initialization superseded');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let wonElection = false;
|
|
283
|
+
let winnerPort = port;
|
|
284
|
+
let winnerAuthSecret: string | undefined;
|
|
285
|
+
try {
|
|
286
|
+
await stateManager.updateState(async (state) => {
|
|
287
|
+
if (state.browserServer) {
|
|
288
|
+
const alive = await stateManager.isPidAlive(state.browserServer.pid, state.browserServer.schedulerId, true);
|
|
289
|
+
if (alive) {
|
|
290
|
+
winnerPort = state.browserServer.port;
|
|
291
|
+
winnerAuthSecret = state.browserServer.authSecret;
|
|
292
|
+
wonElection = false;
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const secret = getBrowserServerAuthSecret();
|
|
297
|
+
state.browserServer = { port, pid: process.pid, schedulerId, authSecret: secret };
|
|
298
|
+
state.schedulerVersion = schedulerVersion;
|
|
299
|
+
wonElection = true;
|
|
300
|
+
return state;
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
logger.error('[Scheduler] Failed to register as leader, running standalone:', error);
|
|
304
|
+
if (!isSuperseded()) {
|
|
305
|
+
schedulerService.setSchedulerInstance(scheduler);
|
|
306
|
+
schedulerService.setSchedulerVersion(currentVersion);
|
|
307
|
+
} else {
|
|
308
|
+
await scheduler.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
309
|
+
throw new Error('Initialization superseded', { cause: error });
|
|
310
|
+
}
|
|
311
|
+
return scheduler;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!wonElection) {
|
|
315
|
+
logger.log(`[Scheduler] Lost election, connecting to winner at port ${winnerPort}`);
|
|
316
|
+
await scheduler.shutdown();
|
|
317
|
+
const client = new BrowserClient(winnerPort, winnerAuthSecret);
|
|
318
|
+
if (!isSuperseded()) {
|
|
319
|
+
schedulerService.setSchedulerInstance(client);
|
|
320
|
+
schedulerService.setSchedulerVersion(schedulerVersion);
|
|
321
|
+
} else {
|
|
322
|
+
await client.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
323
|
+
throw new Error('Initialization superseded');
|
|
324
|
+
}
|
|
325
|
+
return client;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
logger.log(`[Scheduler] Won election, serving as leader on port ${port} (PID ${process.pid})`);
|
|
329
|
+
metrics.increment('browser_leadership_wins_total', 1);
|
|
330
|
+
if (!isSuperseded()) {
|
|
331
|
+
schedulerService.setSchedulerInstance(scheduler);
|
|
332
|
+
schedulerService.setSchedulerVersion(schedulerVersion);
|
|
333
|
+
// Only now that we are the registered leader is it safe to arm the
|
|
334
|
+
// leadership self-check — otherwise it could race its own
|
|
335
|
+
// registration and self-shut-down mid-init under lock contention.
|
|
336
|
+
scheduler.startLeadershipMonitor();
|
|
337
|
+
} else {
|
|
338
|
+
logger.warn('[Scheduler] Won election but was superseded by restart or disposal. Shutting down pool...');
|
|
339
|
+
await scheduler.shutdown().catch((err) => logger.debug('Swallowed shutdown error:', err));
|
|
340
|
+
throw new Error('Initialization superseded');
|
|
341
|
+
}
|
|
342
|
+
return scheduler;
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
p = initializationFunction();
|
|
347
|
+
schedulerService.setSchedulerInitializationPromise(p);
|
|
348
|
+
p.catch(() => {
|
|
349
|
+
if (schedulerService.getSchedulerInitializationPromise() === p) {
|
|
350
|
+
schedulerService.setSchedulerInitializationPromise(null);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return p;
|
|
354
|
+
}
|