@vellumai/assistant 0.3.28 → 0.4.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 (199) hide show
  1. package/ARCHITECTURE.md +33 -3
  2. package/bun.lock +4 -1
  3. package/docs/trusted-contact-access.md +9 -2
  4. package/package.json +6 -3
  5. package/scripts/ipc/generate-swift.ts +3 -3
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  7. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +13 -5
  9. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  10. package/src/__tests__/asset-search-tool.test.ts +2 -0
  11. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  12. package/src/__tests__/attachments-store.test.ts +2 -0
  13. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  14. package/src/__tests__/call-controller.test.ts +30 -29
  15. package/src/__tests__/call-routes-http.test.ts +34 -32
  16. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  17. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  18. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  19. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  20. package/src/__tests__/clarification-resolver.test.ts +2 -0
  21. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  22. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  24. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  25. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  26. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  27. package/src/__tests__/config-schema.test.ts +5 -5
  28. package/src/__tests__/config-watcher.test.ts +3 -1
  29. package/src/__tests__/connection-policy.test.ts +14 -5
  30. package/src/__tests__/contacts-tools.test.ts +3 -1
  31. package/src/__tests__/contradiction-checker.test.ts +2 -0
  32. package/src/__tests__/conversation-pairing.test.ts +10 -0
  33. package/src/__tests__/conversation-routes.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  35. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  36. package/src/__tests__/credential-vault.test.ts +5 -4
  37. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  38. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  39. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  40. package/src/__tests__/encrypted-store.test.ts +10 -5
  41. package/src/__tests__/followup-tools.test.ts +3 -1
  42. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  43. package/src/__tests__/gmail-integration.test.ts +0 -1
  44. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  45. package/src/__tests__/guardian-dispatch.test.ts +2 -0
  46. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  47. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  48. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  49. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  50. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  51. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  52. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  53. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  55. package/src/__tests__/heartbeat-service.test.ts +20 -0
  56. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  57. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  58. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  59. package/src/__tests__/intent-routing.test.ts +2 -0
  60. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  61. package/src/__tests__/media-generate-image.test.ts +21 -0
  62. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  63. package/src/__tests__/memory-regressions.test.ts +20 -20
  64. package/src/__tests__/non-member-access-request.test.ts +183 -9
  65. package/src/__tests__/notification-decision-fallback.test.ts +2 -0
  66. package/src/__tests__/notification-decision-strategy.test.ts +61 -0
  67. package/src/__tests__/notification-guardian-path.test.ts +2 -0
  68. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  69. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  70. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  71. package/src/__tests__/pairing-routes.test.ts +171 -0
  72. package/src/__tests__/playbook-execution.test.ts +3 -1
  73. package/src/__tests__/playbook-tools.test.ts +3 -1
  74. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  75. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  76. package/src/__tests__/recording-handler.test.ts +11 -0
  77. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  78. package/src/__tests__/recording-state-machine.test.ts +13 -2
  79. package/src/__tests__/registry.test.ts +7 -3
  80. package/src/__tests__/relay-server.test.ts +148 -28
  81. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  82. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  83. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  84. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  85. package/src/__tests__/schedule-tools.test.ts +3 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  87. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  88. package/src/__tests__/session-agent-loop.test.ts +16 -0
  89. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  90. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  91. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  92. package/src/__tests__/session-profile-injection.test.ts +21 -0
  93. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  94. package/src/__tests__/session-queue.test.ts +23 -0
  95. package/src/__tests__/session-runtime-assembly.test.ts +50 -12
  96. package/src/__tests__/session-skill-tools.test.ts +27 -5
  97. package/src/__tests__/session-slash-known.test.ts +23 -0
  98. package/src/__tests__/session-slash-queue.test.ts +23 -0
  99. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  100. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  101. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  102. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  103. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  104. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  105. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  107. package/src/__tests__/skills.test.ts +8 -4
  108. package/src/__tests__/slack-channel-config.test.ts +3 -1
  109. package/src/__tests__/subagent-tools.test.ts +19 -0
  110. package/src/__tests__/swarm-recursion.test.ts +2 -0
  111. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  112. package/src/__tests__/swarm-tool.test.ts +2 -0
  113. package/src/__tests__/system-prompt.test.ts +3 -1
  114. package/src/__tests__/task-compiler.test.ts +3 -1
  115. package/src/__tests__/task-management-tools.test.ts +3 -1
  116. package/src/__tests__/task-tools.test.ts +3 -1
  117. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  118. package/src/__tests__/terminal-tools.test.ts +2 -0
  119. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  120. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  121. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  122. package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
  123. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  124. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  125. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  126. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  127. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  128. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  129. package/src/__tests__/view-image-tool.test.ts +3 -1
  130. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  131. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  132. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  133. package/src/__tests__/work-item-output.test.ts +3 -1
  134. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  135. package/src/calls/call-controller.ts +26 -23
  136. package/src/calls/guardian-action-sweep.ts +10 -2
  137. package/src/calls/relay-server.ts +216 -27
  138. package/src/calls/types.ts +1 -1
  139. package/src/calls/voice-session-bridge.ts +3 -3
  140. package/src/cli.ts +12 -0
  141. package/src/config/agent-schema.ts +14 -3
  142. package/src/config/calls-schema.ts +6 -6
  143. package/src/config/core-schema.ts +3 -3
  144. package/src/config/feature-flag-registry.json +8 -0
  145. package/src/config/mcp-schema.ts +1 -1
  146. package/src/config/memory-schema.ts +27 -19
  147. package/src/config/schema.ts +21 -21
  148. package/src/config/skills-schema.ts +7 -7
  149. package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
  150. package/src/daemon/handlers/config-inbox.ts +4 -4
  151. package/src/daemon/handlers/sessions.ts +148 -4
  152. package/src/daemon/ipc-contract/messages.ts +16 -0
  153. package/src/daemon/ipc-contract-inventory.json +1 -0
  154. package/src/daemon/lifecycle.ts +19 -0
  155. package/src/daemon/pairing-store.ts +86 -3
  156. package/src/daemon/session-agent-loop.ts +5 -5
  157. package/src/daemon/session-lifecycle.ts +25 -17
  158. package/src/daemon/session-memory.ts +2 -2
  159. package/src/daemon/session-process.ts +1 -20
  160. package/src/daemon/session-runtime-assembly.ts +28 -22
  161. package/src/daemon/session-tool-setup.ts +2 -2
  162. package/src/daemon/session.ts +3 -3
  163. package/src/memory/canonical-guardian-store.ts +63 -1
  164. package/src/memory/channel-guardian-store.ts +1 -0
  165. package/src/memory/conversation-crud.ts +7 -7
  166. package/src/memory/db-init.ts +4 -0
  167. package/src/memory/embedding-local.ts +257 -39
  168. package/src/memory/embedding-runtime-manager.ts +471 -0
  169. package/src/memory/guardian-bindings.ts +25 -1
  170. package/src/memory/indexer.ts +3 -3
  171. package/src/memory/ingress-invite-store.ts +45 -0
  172. package/src/memory/job-handlers/backfill.ts +16 -9
  173. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  174. package/src/memory/migrations/index.ts +1 -0
  175. package/src/memory/qdrant-client.ts +31 -22
  176. package/src/memory/schema.ts +4 -0
  177. package/src/notifications/copy-composer.ts +15 -0
  178. package/src/runtime/access-request-helper.ts +43 -7
  179. package/src/runtime/actor-trust-resolver.ts +46 -50
  180. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  181. package/src/runtime/channel-retry-sweep.ts +18 -6
  182. package/src/runtime/guardian-context-resolver.ts +38 -96
  183. package/src/runtime/guardian-reply-router.ts +31 -1
  184. package/src/runtime/ingress-service.ts +80 -3
  185. package/src/runtime/invite-redemption-service.ts +141 -2
  186. package/src/runtime/routes/channel-route-shared.ts +1 -1
  187. package/src/runtime/routes/channel-routes.ts +1 -1
  188. package/src/runtime/routes/conversation-routes.ts +2 -2
  189. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  190. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  191. package/src/runtime/routes/ingress-routes.ts +52 -4
  192. package/src/runtime/routes/pairing-routes.ts +3 -0
  193. package/src/tools/guardian-control-plane-policy.ts +2 -2
  194. package/src/tools/tool-approval-handler.ts +11 -11
  195. package/src/tools/types.ts +2 -2
  196. package/src/util/logger.ts +20 -8
  197. package/src/util/platform.ts +10 -0
  198. package/src/util/voice-code.ts +29 -0
  199. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -1,27 +1,48 @@
1
- import { dirname, join } from 'node:path';
1
+ import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
2
3
 
3
4
  import { getLogger } from '../util/logger.js';
5
+ import { getEmbeddingModelsDir, getRootDir } from '../util/platform.js';
4
6
  import { PromiseGuard } from '../util/promise-guard.js';
5
7
  import type { EmbeddingBackend, EmbeddingRequestOptions } from './embedding-backend.js';
8
+ import { EmbeddingRuntimeManager } from './embedding-runtime-manager.js';
6
9
 
7
10
  const log = getLogger('memory-embedding-local');
8
11
 
9
- type FeatureExtractionPipeline = (
10
- texts: string | string[],
11
- options?: { pooling?: string; normalize?: boolean },
12
- ) => Promise<{ tolist: () => number[][] }>;
12
+ interface WorkerResponse {
13
+ id?: number;
14
+ type?: string;
15
+ vectors?: number[][];
16
+ error?: string;
17
+ }
13
18
 
14
19
  /**
15
20
  * Local embedding backend using @huggingface/transformers (ONNX Runtime).
16
21
  * Runs BAAI/bge-small-en-v1.5 locally — no API calls, no network required.
17
22
  *
18
- * The model is downloaded on first use and cached by the transformers library.
23
+ * Embeddings run in a **separate bun process** because compiled Bun binaries
24
+ * cannot resolve bare specifier imports in dynamically loaded files. The embed
25
+ * worker communicates via JSON-lines over stdin/stdout.
26
+ *
27
+ * The embedding runtime (onnxruntime-node + transformers + bun) is downloaded
28
+ * post-hatch by EmbeddingRuntimeManager.
29
+ *
19
30
  * Produces 384-dimensional embeddings.
20
31
  */
21
32
  export class LocalEmbeddingBackend implements EmbeddingBackend {
22
33
  readonly provider = 'local' as const;
23
34
  readonly model: string;
24
- private extractor: FeatureExtractionPipeline | null = null;
35
+
36
+ // Subprocess — typed loosely to avoid coupling to Bun's Subprocess generics
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ private workerProc: any = null;
39
+ private stdoutBuffer = '';
40
+ private requestCounter = 0;
41
+ private pendingRequests = new Map<number, {
42
+ resolve: (response: WorkerResponse) => void;
43
+ }>();
44
+ private stdoutReaderActive = false;
45
+
25
46
  private readonly initGuard = new PromiseGuard<void>();
26
47
 
27
48
  constructor(model: string) {
@@ -35,55 +56,252 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
35
56
  await this.ensureInitialized();
36
57
 
37
58
  const results: number[][] = [];
38
- // Process in batches of 32 to avoid OOM with large inputs
39
59
  const batchSize = 32;
40
60
  for (let i = 0; i < texts.length; i += batchSize) {
41
61
  if (options?.signal?.aborted) throw new DOMException('Aborted', 'AbortError');
42
62
  const batch = texts.slice(i, i + batchSize);
43
- const output = await this.extractor!(batch, {
44
- pooling: 'cls',
45
- normalize: true,
46
- });
47
- const vectors = output.tolist();
48
- results.push(...vectors);
63
+ const response = await this.sendRequest(batch);
64
+ if (response.error) {
65
+ throw new Error(`Embedding worker error: ${response.error}`);
66
+ }
67
+ if (!response.vectors) {
68
+ throw new Error('Embedding worker returned no vectors');
69
+ }
70
+ results.push(...response.vectors);
49
71
  }
50
-
51
72
  return results;
52
73
  }
53
74
 
75
+ private sendRequest(texts: string[]): Promise<WorkerResponse> {
76
+ const id = ++this.requestCounter;
77
+ return new Promise((resolve) => {
78
+ if (!this.workerProc) {
79
+ resolve({ id, error: 'Worker not initialized' });
80
+ return;
81
+ }
82
+ this.pendingRequests.set(id, { resolve });
83
+ this.workerProc.stdin.write(JSON.stringify({ id, texts }) + '\n');
84
+ this.workerProc.stdin.flush();
85
+ });
86
+ }
87
+
54
88
  private async ensureInitialized(): Promise<void> {
55
- if (this.extractor) return;
89
+ if (this.workerProc) return;
56
90
  await this.initGuard.run(() => this.initialize());
57
91
  }
58
92
 
59
93
  private async initialize(): Promise<void> {
60
- log.info({ model: this.model }, 'Loading local embedding model (first load downloads the model)');
61
-
62
- // In compiled Bun binaries, bare specifier resolution for packages with
63
- // subdirectory entry points (like onnxruntime-common's dist/esm/index.js)
64
- // fails. Additionally, CJS/ESM dual-instance issues cause onnxruntime-node's
65
- // backend registration to be invisible to transformers. To solve both, the
66
- // build step pre-bundles all JS deps into a single file placed inside
67
- // onnxruntime-node/dist/ so native .node binary relative paths resolve.
68
- const execDir = dirname(process.execPath);
69
- const bundlePath = join(execDir, 'node_modules', 'onnxruntime-node', 'dist', 'transformers-bundle.mjs');
70
- let transformers: typeof import('@huggingface/transformers');
94
+ log.info({ model: this.model }, 'Initializing local embedding backend');
95
+
96
+ const runtimeManager = new EmbeddingRuntimeManager();
97
+
98
+ // Wait for download if in progress
99
+ if (!runtimeManager.isReady()) {
100
+ log.info('Embedding runtime not yet available, waiting for download...');
101
+ await runtimeManager.ensureInstalled();
102
+ }
103
+
104
+ const bunPath = runtimeManager.getBunPath();
105
+ const workerPath = runtimeManager.getWorkerPath();
106
+
107
+ if (!bunPath) {
108
+ throw new Error('Local embedding backend unavailable: no bun binary found');
109
+ }
110
+ if (!existsSync(workerPath)) {
111
+ throw new Error(`Local embedding backend unavailable: worker script not found at ${workerPath}`);
112
+ }
113
+
114
+ await this.startWorker(bunPath, workerPath);
115
+ }
116
+
117
+ private async startWorker(bunPath: string, workerPath: string): Promise<void> {
118
+ const embeddingModelsDir = getEmbeddingModelsDir();
119
+ const modelCacheDir = `${embeddingModelsDir}/model-cache`;
120
+
121
+ log.info({ bunPath, workerPath, model: this.model }, 'Spawning embedding worker process');
122
+
123
+ const proc = Bun.spawn({
124
+ cmd: [bunPath, workerPath, this.model, modelCacheDir],
125
+ stdin: 'pipe',
126
+ stdout: 'pipe',
127
+ stderr: 'pipe',
128
+ cwd: embeddingModelsDir,
129
+ });
130
+
131
+ // Type-compatible assignment
132
+ this.workerProc = proc;
133
+
134
+ // Start reading stdout for responses (needed for waitForReady)
135
+ this.startStdoutReader();
136
+
71
137
  try {
72
- transformers = await import(bundlePath);
73
- } catch {
74
- // Fall back to bare specifier for dev mode (running via `bun run`, not compiled)
138
+ // Wait for the worker to signal it's ready (model loaded)
139
+ await this.waitForReady();
140
+ } catch (err) {
141
+ // Worker failed to start — collect stderr for diagnosis
142
+ this.workerProc = null;
143
+ const exitCode = await proc.exited.catch(() => undefined);
144
+ const stderr = await new Response(proc.stderr).text().catch(() => '');
145
+ if (stderr.trim()) {
146
+ log.warn({ stderr: stderr.trim(), exitCode, bunPath }, 'Embedding worker stderr');
147
+ }
148
+ throw new Error(
149
+ `Embedding worker exited (code ${exitCode ?? 'unknown'}): ${stderr.trim() || (err instanceof Error ? err.message : String(err))}`,
150
+ );
151
+ }
152
+
153
+ // Worker is running — drain stderr in background for ongoing logging
154
+ this.drainStderr(proc.stderr);
155
+
156
+ // Write PID file so `vellum ps` can see the embed worker
157
+ this.writePidFile(proc.pid);
158
+
159
+ log.info({ pid: proc.pid, model: this.model }, 'Embedding worker process started');
160
+ }
161
+
162
+ private drainStderr(stderr: ReadableStream<Uint8Array>): void {
163
+ const reader = stderr.getReader();
164
+ const decoder = new TextDecoder();
165
+ (async () => {
75
166
  try {
76
- transformers = await import('@huggingface/transformers');
77
- } catch (err) {
78
- throw new Error(
79
- `Local embedding backend unavailable: failed to load @huggingface/transformers (${err instanceof Error ? err.message : String(err)})`,
80
- );
167
+ while (true) {
168
+ const { done, value } = await reader.read();
169
+ if (done) break;
170
+ const text = decoder.decode(value, { stream: true }).trim();
171
+ if (text) log.debug({ workerStderr: text }, 'Embedding worker stderr');
172
+ }
173
+ } catch {
174
+ // Reader cancelled or stream errored — expected on shutdown
175
+ }
176
+ })();
177
+ }
178
+
179
+ private startStdoutReader(): void {
180
+ if (this.stdoutReaderActive || !this.workerProc) return;
181
+ this.stdoutReaderActive = true;
182
+
183
+ const reader = this.workerProc.stdout.getReader();
184
+ const decoder = new TextDecoder();
185
+
186
+ (async () => {
187
+ try {
188
+ while (true) {
189
+ const { done, value } = await reader.read();
190
+ if (done) break;
191
+ this.stdoutBuffer += decoder.decode(value, { stream: true });
192
+ this.processStdoutBuffer();
193
+ }
194
+ } catch {
195
+ // Reader cancelled or stream errored
196
+ }
197
+
198
+ // Worker exited — reject all pending requests and clean up
199
+ for (const [, pending] of this.pendingRequests) {
200
+ pending.resolve({ error: 'Embedding worker process exited unexpectedly' });
201
+ }
202
+ this.pendingRequests.clear();
203
+ this.workerProc = null;
204
+ this.stdoutReaderActive = false;
205
+ this.removePidFile();
206
+ this.stdoutBuffer = '';
207
+ // Allow re-initialization on next embed() call
208
+ this.initGuard.reset();
209
+ })();
210
+ }
211
+
212
+ private readyResolve: (() => void) | null = null;
213
+ private readyReject: ((err: Error) => void) | null = null;
214
+
215
+ private processStdoutBuffer(): void {
216
+ let idx: number;
217
+ while ((idx = this.stdoutBuffer.indexOf('\n')) !== -1) {
218
+ const line = this.stdoutBuffer.slice(0, idx);
219
+ this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
220
+ if (!line.trim()) continue;
221
+
222
+ let msg: WorkerResponse;
223
+ try {
224
+ msg = JSON.parse(line);
225
+ } catch {
226
+ continue; // Skip malformed lines
227
+ }
228
+
229
+ // Handle ready/error signals during initialization
230
+ if (msg.type === 'ready') {
231
+ this.readyResolve?.();
232
+ this.readyResolve = null;
233
+ this.readyReject = null;
234
+ continue;
235
+ }
236
+ if (msg.type === 'error' && this.readyReject) {
237
+ this.readyReject(new Error(msg.error ?? 'Worker initialization failed'));
238
+ this.readyResolve = null;
239
+ this.readyReject = null;
240
+ continue;
241
+ }
242
+
243
+ // Handle embed responses
244
+ if (msg.id !== undefined) {
245
+ const pending = this.pendingRequests.get(msg.id);
246
+ if (pending) {
247
+ this.pendingRequests.delete(msg.id);
248
+ pending.resolve(msg);
249
+ }
81
250
  }
82
251
  }
252
+ }
253
+
254
+ private waitForReady(): Promise<void> {
255
+ return new Promise<void>((resolve, reject) => {
256
+ this.readyResolve = resolve;
257
+ this.readyReject = reject;
258
+
259
+ // Timeout after 2 minutes (first model download can be slow)
260
+ const timeout = setTimeout(() => {
261
+ this.readyResolve = null;
262
+ this.readyReject = null;
263
+ reject(new Error('Embedding worker timed out waiting for model to load'));
264
+ }, 120_000);
265
+
266
+ // Clear timeout when resolved
267
+ const originalResolve = resolve;
268
+ this.readyResolve = () => {
269
+ clearTimeout(timeout);
270
+ originalResolve();
271
+ };
272
+ const originalReject = reject;
273
+ this.readyReject = (err: Error) => {
274
+ clearTimeout(timeout);
275
+ originalReject(err);
276
+ };
277
+
278
+ // Also handle early worker exit
279
+ this.workerProc?.exited.then(() => {
280
+ if (this.readyResolve) {
281
+ clearTimeout(timeout);
282
+ this.readyResolve = null;
283
+ this.readyReject = null;
284
+ reject(new Error('Embedding worker process exited before becoming ready'));
285
+ }
286
+ });
287
+ });
288
+ }
289
+
290
+ private static readonly PID_FILENAME = 'embed-worker.pid';
83
291
 
84
- this.extractor = await transformers.pipeline('feature-extraction', this.model, {
85
- dtype: 'fp32',
86
- }) as unknown as FeatureExtractionPipeline;
87
- log.info({ model: this.model }, 'Local embedding model loaded');
292
+ private writePidFile(pid: number): void {
293
+ try {
294
+ writeFileSync(join(getRootDir(), LocalEmbeddingBackend.PID_FILENAME), String(pid));
295
+ } catch {
296
+ // Best-effort — doesn't affect functionality
297
+ }
298
+ }
299
+
300
+ private removePidFile(): void {
301
+ try {
302
+ unlinkSync(join(getRootDir(), LocalEmbeddingBackend.PID_FILENAME));
303
+ } catch {
304
+ // Best-effort
305
+ }
88
306
  }
89
307
  }