@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,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Embedding Service using HuggingFace Transformers
|
|
3
|
+
*
|
|
4
|
+
* Provides embeddings for text using ONNX runtime with WebGPU/CPU support.
|
|
5
|
+
* Includes automatic fallback from WebGPU to CPU on errors and idle timeout
|
|
6
|
+
* to release GPU memory when not in use.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FeatureExtractionPipeline } from '@huggingface/transformers';
|
|
10
|
+
|
|
11
|
+
import { logger } from '../logger.ts';
|
|
12
|
+
import { safeUnref } from '../utils/safe-unref.ts';
|
|
13
|
+
import { metrics } from '../utils/metrics.ts';
|
|
14
|
+
import type { IStateManager } from '../core/service-interfaces.ts';
|
|
15
|
+
import type {
|
|
16
|
+
EmbedderOptions,
|
|
17
|
+
EmbedderState,
|
|
18
|
+
DisposablePipeline,
|
|
19
|
+
} from '../core/interfaces/knowledge-interfaces.ts';
|
|
20
|
+
import {
|
|
21
|
+
resetWebGpuFallbackFlag,
|
|
22
|
+
hasWebGpuFallback,
|
|
23
|
+
markWebGpuFallback,
|
|
24
|
+
getModelCacheDir,
|
|
25
|
+
registerGlobalEmbedder,
|
|
26
|
+
unregisterGlobalEmbedder,
|
|
27
|
+
initializeDawnWebGPU,
|
|
28
|
+
} from './embedder-utils.ts';
|
|
29
|
+
import { getHFEnv, initializeONNXEnv } from './onnx-env.ts';
|
|
30
|
+
import { resolveEmbeddingDevice } from './webgpu-viability.ts';
|
|
31
|
+
import {
|
|
32
|
+
isWebGpuDeviceError,
|
|
33
|
+
isModelCached,
|
|
34
|
+
acquireGpuLock,
|
|
35
|
+
releaseGpuLock,
|
|
36
|
+
loadPipelineWithTimeout,
|
|
37
|
+
warmupPipeline,
|
|
38
|
+
handleWebGPULoadError,
|
|
39
|
+
handleWebGPUWarmupError,
|
|
40
|
+
isCorruptModelError,
|
|
41
|
+
purgeModelCache,
|
|
42
|
+
} from './embedder-init.ts';
|
|
43
|
+
|
|
44
|
+
export { resetWebGpuFallbackFlag, hasWebGpuFallback, getModelCacheDir };
|
|
45
|
+
export type { EmbedderOptions, EmbedderState };
|
|
46
|
+
|
|
47
|
+
export class Embedder {
|
|
48
|
+
private state: EmbedderState = 'idle';
|
|
49
|
+
private pipeline: FeatureExtractionPipeline | null = null;
|
|
50
|
+
private initializingPromise: Promise<void> | null = null;
|
|
51
|
+
private disposePromise: Promise<void> | null = null;
|
|
52
|
+
private recoveryPromise: Promise<void> | null = null;
|
|
53
|
+
|
|
54
|
+
private model: string;
|
|
55
|
+
private poolingMode: 'mean' | 'cls' | 'last_token';
|
|
56
|
+
private queryPrefix: string;
|
|
57
|
+
private dimension: number | null = null;
|
|
58
|
+
private initializationTimeoutMs: number;
|
|
59
|
+
private device: string;
|
|
60
|
+
private maxTokens: number;
|
|
61
|
+
private batchSize: number;
|
|
62
|
+
private charsPerToken: number;
|
|
63
|
+
private documentPrefix: string;
|
|
64
|
+
private stateManager: IStateManager | null;
|
|
65
|
+
private gpuLockHeld = false;
|
|
66
|
+
private originalDevice: string;
|
|
67
|
+
private useCache: boolean;
|
|
68
|
+
private idleTimer: NodeJS.Timeout | null = null;
|
|
69
|
+
private readonly IDLE_TIMEOUT_MS = 60 * 1000;
|
|
70
|
+
private activeEmbeddings = 0;
|
|
71
|
+
|
|
72
|
+
constructor(options: EmbedderOptions) {
|
|
73
|
+
this.model = options.model;
|
|
74
|
+
this.poolingMode = options.pooling ?? 'mean';
|
|
75
|
+
this.queryPrefix = options.queryPrefix ?? '';
|
|
76
|
+
this.initializationTimeoutMs = options.initializationTimeoutMs ?? 300000;
|
|
77
|
+
this.originalDevice = options.device ?? 'auto';
|
|
78
|
+
|
|
79
|
+
// If WebGPU already proved unusable earlier this session, skip it entirely
|
|
80
|
+
// for any GPU-capable request ('webgpu' or 'auto') — no point re-probing or
|
|
81
|
+
// re-attempting a path known to fail.
|
|
82
|
+
const couldUseWebGpu = this.originalDevice === 'webgpu' || this.originalDevice === 'auto';
|
|
83
|
+
|
|
84
|
+
if (hasWebGpuFallback() && couldUseWebGpu) {
|
|
85
|
+
this.device = 'cpu';
|
|
86
|
+
logger.info('[embedder] Skipping WebGPU (previous fallback detected), using CPU directly');
|
|
87
|
+
} else {
|
|
88
|
+
this.device = this.originalDevice;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.maxTokens = options.maxTokens ?? 512;
|
|
92
|
+
this.batchSize = options.batchSize ?? 8;
|
|
93
|
+
this.charsPerToken = options.charsPerToken ?? 4;
|
|
94
|
+
this.documentPrefix = options.documentPrefix ?? '';
|
|
95
|
+
this.stateManager = options.stateManager ?? null;
|
|
96
|
+
this.useCache = options.useCache ?? true;
|
|
97
|
+
|
|
98
|
+
// Register this instance so the beforeExit handler can dispose it before
|
|
99
|
+
// the ONNX C++ runtime tears down its global logger singleton (prevents crash).
|
|
100
|
+
registerGlobalEmbedder(this);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private resetIdleTimer(): void {
|
|
104
|
+
if (this.idleTimer) {
|
|
105
|
+
clearTimeout(this.idleTimer);
|
|
106
|
+
}
|
|
107
|
+
this.idleTimer = setTimeout(() => {
|
|
108
|
+
// FIX (#17): Only dispose if no embeddings are active, otherwise re-schedule
|
|
109
|
+
if (this.activeEmbeddings > 0) {
|
|
110
|
+
logger.debug(`[embedder] Idle timeout but ${this.activeEmbeddings} embeddings active, rescheduling...`);
|
|
111
|
+
this.resetIdleTimer();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (this.state === 'ready') {
|
|
115
|
+
logger.info(`[embedder] Idle timeout reached (${this.IDLE_TIMEOUT_MS}ms), releasing GPU memory...`);
|
|
116
|
+
this.dispose().catch(err => logger.warn('[embedder] Failed to dispose on idle:', err));
|
|
117
|
+
}
|
|
118
|
+
}, this.IDLE_TIMEOUT_MS);
|
|
119
|
+
if (this.idleTimer) safeUnref(this.idleTimer);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private stopIdleTimer(): void {
|
|
123
|
+
if (this.idleTimer) {
|
|
124
|
+
clearTimeout(this.idleTimer);
|
|
125
|
+
this.idleTimer = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async initialize(): Promise<void> {
|
|
130
|
+
if (this.state === 'ready') return;
|
|
131
|
+
if (this.state === 'disposing') {
|
|
132
|
+
throw new Error('Cannot initialize while disposing');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.initializingPromise) {
|
|
136
|
+
return this.initializingPromise;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.state = 'initializing';
|
|
140
|
+
this.initializingPromise = this._initializeInternal();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await this.initializingPromise;
|
|
144
|
+
const currentState = this.state as EmbedderState;
|
|
145
|
+
if (currentState === 'disposing') {
|
|
146
|
+
logger.warn('[embedder] Initialization finished but embedder was disposed in the meantime.');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.state = 'ready';
|
|
150
|
+
// Re-point the global ref on the idle→active revive: a prior idle dispose() called
|
|
151
|
+
// unregisterGlobalEmbedder() (nulling the ref), which would otherwise leave the
|
|
152
|
+
// beforeExit ORT-teardown safety net pointing at nothing for the now-live pipeline.
|
|
153
|
+
// Idempotent — the shutdown task is WeakSet-guarded, so no callback is leaked.
|
|
154
|
+
registerGlobalEmbedder(this);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this.state = 'failed';
|
|
157
|
+
throw err;
|
|
158
|
+
} finally {
|
|
159
|
+
this.initializingPromise = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async _initializeInternal(): Promise<void> {
|
|
164
|
+
try {
|
|
165
|
+
// Lazy initialization of the ONNX/transformers environment. Kept here (not
|
|
166
|
+
// in initialize()) so initialize()'s synchronous state setup — state =
|
|
167
|
+
// 'initializing' and the initializingPromise — runs before the first await,
|
|
168
|
+
// preserving the dispose-during-init ordering. This is also the first point
|
|
169
|
+
// the native ML stack is touched (via the lazy transformers loader).
|
|
170
|
+
await initializeONNXEnv();
|
|
171
|
+
|
|
172
|
+
// Resolve 'auto' to a concrete backend BEFORE any native ONNX/Dawn code runs.
|
|
173
|
+
// For 'auto' this runs an out-of-process probe (cached per host) so that a
|
|
174
|
+
// native SIGSEGV on a software/paravirtual GPU can never reach this process.
|
|
175
|
+
if (this.device === 'auto') {
|
|
176
|
+
this.device = await resolveEmbeddingDevice('auto', this.model, this.initializationTimeoutMs);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Acquire GPU lock if using WebGPU
|
|
180
|
+
if (this.device === 'webgpu') {
|
|
181
|
+
const { acquired, shouldFallback } = await acquireGpuLock(this.stateManager);
|
|
182
|
+
if (shouldFallback) {
|
|
183
|
+
this.device = 'cpu';
|
|
184
|
+
} else if (acquired) {
|
|
185
|
+
this.gpuLockHeld = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try to initialize WebGPU via Dawn for Node.js environments. A false
|
|
190
|
+
// return means a software/fallback adapter was detected in-process —
|
|
191
|
+
// downgrade to CPU before any native compute runs (and release the GPU lock).
|
|
192
|
+
if (this.device === 'webgpu') {
|
|
193
|
+
const webgpuOk = await initializeDawnWebGPU();
|
|
194
|
+
if (!webgpuOk) {
|
|
195
|
+
await releaseGpuLock(this.stateManager, this.gpuLockHeld);
|
|
196
|
+
this.gpuLockHeld = false;
|
|
197
|
+
this.device = 'cpu';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const cached = await isModelCached(this.model);
|
|
202
|
+
logger.info(
|
|
203
|
+
`[embedder] Loading model: ${this.model} (${cached ? 'from local cache' : 'downloading from HuggingFace'})...`
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const env = await getHFEnv();
|
|
207
|
+
const prevAllowRemote = env.allowRemoteModels;
|
|
208
|
+
if (cached) {
|
|
209
|
+
env.allowRemoteModels = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const timeoutMs = cached ? 30_000 : this.initializationTimeoutMs;
|
|
214
|
+
const { pipeline: loadedPipeline } = await loadPipelineWithTimeout(this.model, this.device, timeoutMs, this.useCache);
|
|
215
|
+
this.pipeline = loadedPipeline;
|
|
216
|
+
logger.info(`[embedder] Pipeline loaded (device: ${this.device})`);
|
|
217
|
+
} catch (loadErr) {
|
|
218
|
+
if (this.device === 'webgpu' && isWebGpuDeviceError(loadErr)) {
|
|
219
|
+
const result = await handleWebGPULoadError(
|
|
220
|
+
loadErr,
|
|
221
|
+
this.pipeline,
|
|
222
|
+
this.stateManager,
|
|
223
|
+
this.gpuLockHeld,
|
|
224
|
+
this.model,
|
|
225
|
+
this.initializationTimeoutMs,
|
|
226
|
+
this.useCache
|
|
227
|
+
);
|
|
228
|
+
if (!result.success) {
|
|
229
|
+
throw result.error;
|
|
230
|
+
}
|
|
231
|
+
this.pipeline = result.pipeline ?? null;
|
|
232
|
+
this.device = 'cpu';
|
|
233
|
+
this.gpuLockHeld = false;
|
|
234
|
+
} else if (cached && isCorruptModelError(loadErr)) {
|
|
235
|
+
// The on-disk cache is truncated/corrupt (e.g. an interrupted weights download that
|
|
236
|
+
// slipped past isModelCached). Retrying the same files fails identically and, on one
|
|
237
|
+
// path, surfaced as an uncaughtException that took down the host. Purge the cache and
|
|
238
|
+
// re-download exactly once; allowRemoteModels must be re-enabled for the fetch.
|
|
239
|
+
const errMsg = loadErr instanceof Error ? loadErr.message : String(loadErr);
|
|
240
|
+
logger.warn(`[embedder] Corrupt model cache detected on load (${errMsg}); purging and re-downloading once`);
|
|
241
|
+
await purgeModelCache(this.model);
|
|
242
|
+
env.allowRemoteModels = true;
|
|
243
|
+
const { pipeline: reloadedPipeline } = await loadPipelineWithTimeout(
|
|
244
|
+
this.model,
|
|
245
|
+
this.device,
|
|
246
|
+
this.initializationTimeoutMs,
|
|
247
|
+
this.useCache
|
|
248
|
+
);
|
|
249
|
+
this.pipeline = reloadedPipeline;
|
|
250
|
+
logger.info(`[embedder] Re-download after cache purge succeeded (device: ${this.device})`);
|
|
251
|
+
} else {
|
|
252
|
+
throw loadErr;
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
env.allowRemoteModels = prevAllowRemote;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Warmup
|
|
259
|
+
let dummy: any;
|
|
260
|
+
try {
|
|
261
|
+
const warmupResult = await warmupPipeline(this.pipeline!, this.poolingMode, this.useCache);
|
|
262
|
+
if (!warmupResult.success) {
|
|
263
|
+
throw warmupResult.error;
|
|
264
|
+
}
|
|
265
|
+
dummy = warmupResult.dummy;
|
|
266
|
+
} catch (warmupErr) {
|
|
267
|
+
if (this.device === 'webgpu' && isWebGpuDeviceError(warmupErr)) {
|
|
268
|
+
const result = await handleWebGPUWarmupError(
|
|
269
|
+
warmupErr as Error,
|
|
270
|
+
this.pipeline,
|
|
271
|
+
this.stateManager,
|
|
272
|
+
this.gpuLockHeld,
|
|
273
|
+
this.model,
|
|
274
|
+
this.initializationTimeoutMs,
|
|
275
|
+
this.useCache
|
|
276
|
+
);
|
|
277
|
+
if (!result.success) {
|
|
278
|
+
throw result.error;
|
|
279
|
+
}
|
|
280
|
+
this.pipeline = result.pipeline ?? null;
|
|
281
|
+
this.device = 'cpu';
|
|
282
|
+
this.gpuLockHeld = false;
|
|
283
|
+
dummy = result.dummy;
|
|
284
|
+
} else {
|
|
285
|
+
throw warmupErr;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.dimension = dummy.dims[dummy.dims.length - 1] ?? null;
|
|
290
|
+
|
|
291
|
+
await releaseGpuLock(this.stateManager, this.gpuLockHeld);
|
|
292
|
+
this.gpuLockHeld = false;
|
|
293
|
+
|
|
294
|
+
logger.info(`[embedder] Ready. Dimension: ${this.dimension}, device: ${this.device}`);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
await releaseGpuLock(this.stateManager, this.gpuLockHeld);
|
|
297
|
+
this.gpuLockHeld = false;
|
|
298
|
+
if (this.pipeline) {
|
|
299
|
+
try { if (typeof (this.pipeline as any).dispose === 'function') await (this.pipeline as DisposablePipeline).dispose(); } catch (err) { logger.warn('[embedder] Error disposing pipeline:', err); }
|
|
300
|
+
this.pipeline = null;
|
|
301
|
+
}
|
|
302
|
+
logger.error(`[embedder] Failed to initialize:`, err);
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
isInitialized(): boolean {
|
|
308
|
+
return this.state === 'ready' && this.pipeline !== null && this.dimension !== null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getDevice(): string {
|
|
312
|
+
return this.device;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getOriginalDevice(): string {
|
|
316
|
+
return this.originalDevice;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getDimension(): number | null {
|
|
320
|
+
return this.dimension;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Set the embedding dimension explicitly.
|
|
325
|
+
* Used by KnowledgeStore when restoring dimension from an existing table schema
|
|
326
|
+
* before the embedder has been fully warmed up.
|
|
327
|
+
*/
|
|
328
|
+
setDimension(dim: number): void {
|
|
329
|
+
this.dimension = dim;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private pipelineOpts(): { pooling: 'mean' | 'cls' | 'last_token'; normalize: boolean; use_cache?: boolean } {
|
|
333
|
+
return {
|
|
334
|
+
pooling: this.poolingMode as 'mean' | 'cls' | 'last_token',
|
|
335
|
+
normalize: true,
|
|
336
|
+
...(this.useCache === false ? { use_cache: false } : {}),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private truncateText(text: string): string {
|
|
341
|
+
const maxChars = this.maxTokens * this.charsPerToken;
|
|
342
|
+
if (text.length > maxChars) {
|
|
343
|
+
// Hard OOM guard: never feed the model more than its context window (~maxTokens,
|
|
344
|
+
// approximated in chars). Only the tail-end of an oversized input is dropped from
|
|
345
|
+
// the VECTOR — the full text is still stored verbatim and covered by FTS — but the
|
|
346
|
+
// truncation is otherwise silent, so surface it: a spike here means chunks are
|
|
347
|
+
// arriving larger than the embed cap and vector recall on their tail is degraded.
|
|
348
|
+
metrics.increment('embedder_truncations_total', 1, { model: this.model });
|
|
349
|
+
logger.debug(`[embedder] Truncated input for embedding: ${text.length} -> ${maxChars} chars (model=${this.model})`);
|
|
350
|
+
return text.slice(0, maxChars);
|
|
351
|
+
}
|
|
352
|
+
return text;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async embed(text: string): Promise<Float32Array> {
|
|
356
|
+
await this.initialize();
|
|
357
|
+
// Compute the input BEFORE the active-count increment: truncateText is a pure
|
|
358
|
+
// string op, and keeping it (and nothing else) ahead of the try/finally means
|
|
359
|
+
// every path that can throw or await is inside the finally that decrements the
|
|
360
|
+
// counter. Otherwise a throw here (or in acquireGpuLock) would leak the count,
|
|
361
|
+
// permanently wedging recoverToCpu (15s spin) and dispose (5s hang).
|
|
362
|
+
const input = this.truncateText(this.queryPrefix ? this.queryPrefix + text : text);
|
|
363
|
+
this.stopIdleTimer();
|
|
364
|
+
this.activeEmbeddings++;
|
|
365
|
+
|
|
366
|
+
let lockAcquired = false;
|
|
367
|
+
try {
|
|
368
|
+
if (this.device === 'webgpu' && this.stateManager) {
|
|
369
|
+
lockAcquired = await this.stateManager.acquireGpuLock(undefined, 15_000);
|
|
370
|
+
if (!lockAcquired) {
|
|
371
|
+
logger.warn('[embedder] GPU per-call lock timeout after 15s — proceeding without lock');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const output = await logger.runCapturingStderr(async () => {
|
|
375
|
+
return await this.pipeline!(input, this.pipelineOpts());
|
|
376
|
+
});
|
|
377
|
+
return output.data as Float32Array;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
if (isWebGpuDeviceError(err)) {
|
|
380
|
+
if (lockAcquired && this.stateManager) {
|
|
381
|
+
await this.stateManager.releaseGpuLock().catch(err => {
|
|
382
|
+
logger.warn('[embedder] Failed to release GPU lock:', err);
|
|
383
|
+
});
|
|
384
|
+
lockAcquired = false;
|
|
385
|
+
}
|
|
386
|
+
await this.recoverToCpu();
|
|
387
|
+
// recoverToCpu early-returns (no re-init) when disposal/idle already started;
|
|
388
|
+
// dereferencing a null pipeline would raise a TypeError that MASKS the
|
|
389
|
+
// original device error — rethrow the original instead.
|
|
390
|
+
if (!this.pipeline) throw err;
|
|
391
|
+
const output = await logger.runCapturingStderr(async () => {
|
|
392
|
+
return await this.pipeline!(input, this.pipelineOpts());
|
|
393
|
+
});
|
|
394
|
+
return output.data as Float32Array;
|
|
395
|
+
}
|
|
396
|
+
throw err;
|
|
397
|
+
} finally {
|
|
398
|
+
if (lockAcquired && this.stateManager) {
|
|
399
|
+
await this.stateManager.releaseGpuLock().catch((err) => logger.warn('[embedder] Failed to release per-call GPU lock:', err));
|
|
400
|
+
}
|
|
401
|
+
this.activeEmbeddings--;
|
|
402
|
+
this.resetIdleTimer();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async embedMany(texts: string[]): Promise<Float32Array[]> {
|
|
407
|
+
await this.initialize();
|
|
408
|
+
this.stopIdleTimer();
|
|
409
|
+
|
|
410
|
+
return metrics.measure('embedMany_latency', async () => {
|
|
411
|
+
// Increment INSIDE the measured closure and inside the try/finally below, so
|
|
412
|
+
// the dimension-null throw and acquireGpuLock can't leak the active count
|
|
413
|
+
// (the increment used to sit outside this closure while the decrement was in
|
|
414
|
+
// the finally — a throw before the try wedged the counter permanently).
|
|
415
|
+
this.activeEmbeddings++;
|
|
416
|
+
let lockAcquired = false;
|
|
417
|
+
try {
|
|
418
|
+
const dim = this.getDimension();
|
|
419
|
+
if (dim === null) throw new Error('Embedder not initialized (dimension unknown)');
|
|
420
|
+
const results: Float32Array[] = [];
|
|
421
|
+
|
|
422
|
+
if (this.device === 'webgpu' && this.stateManager) {
|
|
423
|
+
lockAcquired = await this.stateManager.acquireGpuLock(undefined, 45_000);
|
|
424
|
+
if (!lockAcquired) {
|
|
425
|
+
logger.warn('[embedder] GPU batch lock timeout after 45s — proceeding without lock');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < texts.length; i += this.batchSize) {
|
|
430
|
+
// Prefix BEFORE truncation (matching embed()'s queryPrefix handling) so
|
|
431
|
+
// the model input never exceeds the char cap by the prefix length.
|
|
432
|
+
const batch = texts.slice(i, i + this.batchSize).map(t =>
|
|
433
|
+
this.truncateText(this.documentPrefix ? this.documentPrefix + t : t)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
let output: any;
|
|
437
|
+
try {
|
|
438
|
+
output = await logger.runCapturingStderr(async () => {
|
|
439
|
+
return await this.pipeline!(batch, this.pipelineOpts());
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
if (isWebGpuDeviceError(err)) {
|
|
443
|
+
if (lockAcquired && this.stateManager) {
|
|
444
|
+
await this.stateManager.releaseGpuLock().catch(err => {
|
|
445
|
+
logger.warn('[embedder] Failed to release GPU lock:', err);
|
|
446
|
+
});
|
|
447
|
+
lockAcquired = false;
|
|
448
|
+
}
|
|
449
|
+
await this.recoverToCpu();
|
|
450
|
+
// Same guard as embed(): recoverToCpu may have skipped (disposing/idle),
|
|
451
|
+
// leaving pipeline null — rethrow the original error, not a TypeError.
|
|
452
|
+
if (!this.pipeline) throw err;
|
|
453
|
+
output = await logger.runCapturingStderr(async () => {
|
|
454
|
+
return await this.pipeline!(batch, this.pipelineOpts());
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (let j = 0; j < batch.length; j++) {
|
|
462
|
+
results.push(output.data.slice(j * dim, (j + 1) * dim) as Float32Array);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
} finally {
|
|
467
|
+
if (lockAcquired && this.stateManager) {
|
|
468
|
+
await this.stateManager.releaseGpuLock().catch(err => {
|
|
469
|
+
logger.warn('[embedder] Failed to release GPU lock:', err);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
this.activeEmbeddings--;
|
|
473
|
+
this.resetIdleTimer();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private async recoverToCpu(): Promise<void> {
|
|
479
|
+
if (this.recoveryPromise) return this.recoveryPromise;
|
|
480
|
+
|
|
481
|
+
// Guard: if disposal has already started, skip recovery — the embedder is going away.
|
|
482
|
+
if (this.state === 'disposing' || this.state === 'idle') {
|
|
483
|
+
logger.debug('[embedder] recoverToCpu called during disposal/idle — skipping');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.recoveryPromise = (async () => {
|
|
488
|
+
logger.warn('[embedder] WebGPU device error detected during operation — falling back to CPU for the remainder of this session');
|
|
489
|
+
|
|
490
|
+
markWebGpuFallback();
|
|
491
|
+
|
|
492
|
+
await releaseGpuLock(this.stateManager, this.gpuLockHeld);
|
|
493
|
+
this.gpuLockHeld = false;
|
|
494
|
+
|
|
495
|
+
this.state = 'initializing';
|
|
496
|
+
|
|
497
|
+
// Wait for other concurrent embeddings to finish before disposing the pipeline
|
|
498
|
+
const maxWaitMs = 15000;
|
|
499
|
+
const startTime = Date.now();
|
|
500
|
+
// We expect activeEmbeddings to be at least 1 (the current caller).
|
|
501
|
+
// If multiple callers are in recoverToCpu, they all wait for recoveryPromise.
|
|
502
|
+
while (this.activeEmbeddings > 1 && (Date.now() - startTime) < maxWaitMs) {
|
|
503
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (this.pipeline) {
|
|
507
|
+
try { if (typeof (this.pipeline as any).dispose === 'function') await (this.pipeline as DisposablePipeline).dispose(); } catch (err) { logger.warn('[embedder] Error disposing pipeline:', err); }
|
|
508
|
+
this.pipeline = null;
|
|
509
|
+
}
|
|
510
|
+
this.device = 'cpu';
|
|
511
|
+
|
|
512
|
+
this.initializingPromise = this._initializeInternal();
|
|
513
|
+
try {
|
|
514
|
+
await this.initializingPromise;
|
|
515
|
+
this.state = 'ready';
|
|
516
|
+
} catch (e) {
|
|
517
|
+
this.state = 'failed';
|
|
518
|
+
throw e;
|
|
519
|
+
} finally {
|
|
520
|
+
this.initializingPromise = null;
|
|
521
|
+
}
|
|
522
|
+
logger.warn('[embedder] CPU fallback recovery complete.');
|
|
523
|
+
})();
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
await this.recoveryPromise;
|
|
527
|
+
} finally {
|
|
528
|
+
this.recoveryPromise = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async dispose(): Promise<void> {
|
|
533
|
+
if (this.state === 'idle') return;
|
|
534
|
+
this.stopIdleTimer();
|
|
535
|
+
|
|
536
|
+
if (this.state === 'disposing' && this.disposePromise) {
|
|
537
|
+
return this.disposePromise;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.state = 'disposing';
|
|
541
|
+
this.disposePromise = (async () => {
|
|
542
|
+
// Wait for all active embeddings to complete
|
|
543
|
+
const maxWaitMs = 5000;
|
|
544
|
+
const startTime = Date.now();
|
|
545
|
+
while (this.activeEmbeddings > 0 && (Date.now() - startTime) < maxWaitMs) {
|
|
546
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (this.activeEmbeddings > 0) {
|
|
550
|
+
logger.warn(`[embedder] Disposing with ${this.activeEmbeddings} active embeddings (timed out)`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (this.initializingPromise) {
|
|
554
|
+
try {
|
|
555
|
+
await this.initializingPromise;
|
|
556
|
+
} catch (_e) {
|
|
557
|
+
// Initialize failed, which is fine
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (this.pipeline) {
|
|
562
|
+
try {
|
|
563
|
+
if (typeof (this.pipeline as any).dispose === 'function') {
|
|
564
|
+
await (this.pipeline as DisposablePipeline).dispose();
|
|
565
|
+
}
|
|
566
|
+
} catch (err) {
|
|
567
|
+
logger.warn('[embedder] Error during pipeline dispose:', err);
|
|
568
|
+
}
|
|
569
|
+
this.pipeline = null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Always release the GPU lock once on dispose (defensive: the gpuLockHeld flag
|
|
573
|
+
// may be stale, and a normal init already released it). A single unconditional
|
|
574
|
+
// release covers both cases — previously this also called releaseGpuLock(sm,
|
|
575
|
+
// gpuLockHeld) first, double-releasing when the flag was still true.
|
|
576
|
+
if (this.stateManager) {
|
|
577
|
+
await this.stateManager.releaseGpuLock().catch(err => {
|
|
578
|
+
logger.warn('[embedder] Failed to release GPU lock during dispose:', err);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
this.gpuLockHeld = false;
|
|
582
|
+
|
|
583
|
+
this.state = 'idle';
|
|
584
|
+
this.disposePromise = null;
|
|
585
|
+
|
|
586
|
+
// Unregister from the global beforeExit handler — cleanup is done.
|
|
587
|
+
unregisterGlobalEmbedder();
|
|
588
|
+
})();
|
|
589
|
+
|
|
590
|
+
return this.disposePromise;
|
|
591
|
+
}
|
|
592
|
+
}
|