@lincoln504/pi-research 0.1.12 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,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
+ }