specmem-hardwicksoftware 3.7.9 → 3.7.11

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.
@@ -4,6 +4,9 @@
4
4
  import * as fs from 'fs/promises';
5
5
  import * as path from 'path';
6
6
  import * as crypto from 'crypto';
7
+ import * as os from 'os';
8
+ import { Worker } from 'worker_threads';
9
+ import { fileURLToPath } from 'url';
7
10
  import { v4 as uuidv4 } from 'uuid';
8
11
  import { logger } from '../utils/logger.js';
9
12
  import { getProjectPath } from '../config.js';
@@ -12,6 +15,73 @@ import { WhatLanguageIsThis } from './languageDetection.js';
12
15
  import { processBatchesWithConcurrency, DEFAULT_CONCURRENCY_LIMIT } from '../db/batchOperations.js';
13
16
  import { TEXT_LIMITS } from '../constants.js';
14
17
  import { getCurrentProjectId } from '../services/ProjectContext.js';
18
+ const __filename_ingestion = fileURLToPath(import.meta.url);
19
+ const __dirname_ingestion = path.dirname(__filename_ingestion);
20
+ /**
21
+ * FileReadWorkerPool - distributes file I/O across worker threads
22
+ * Uses worker_threads to parallelize file reading across CPU cores
23
+ */
24
+ class FileReadWorkerPool {
25
+ workers = [];
26
+ poolSize;
27
+ workerPath;
28
+ constructor() {
29
+ this.poolSize = Math.min(os.cpus().length, 8);
30
+ this.workerPath = path.join(__dirname_ingestion, 'fileReadWorker.js');
31
+ }
32
+ async processFiles(filePaths, rootPath, maxFileSizeBytes) {
33
+ if (filePaths.length === 0) return [];
34
+ // Split files across workers
35
+ const filesPerWorker = Math.ceil(filePaths.length / this.poolSize);
36
+ const chunks = [];
37
+ for (let i = 0; i < filePaths.length; i += filesPerWorker) {
38
+ chunks.push(filePaths.slice(i, i + filesPerWorker));
39
+ }
40
+ const workerPromises = chunks.map(chunk => {
41
+ return new Promise((resolve, reject) => {
42
+ const worker = new Worker(this.workerPath);
43
+ const timeout = setTimeout(() => {
44
+ worker.terminate();
45
+ reject(new Error(`Worker timeout after 120s processing ${chunk.length} files`));
46
+ }, 120000);
47
+ worker.on('message', (msg) => {
48
+ if (msg.type === 'results') {
49
+ clearTimeout(timeout);
50
+ worker.terminate();
51
+ resolve(msg.results);
52
+ } else if (msg.type === 'error') {
53
+ clearTimeout(timeout);
54
+ worker.terminate();
55
+ reject(new Error(msg.error));
56
+ }
57
+ });
58
+ worker.on('error', (err) => {
59
+ clearTimeout(timeout);
60
+ reject(err);
61
+ });
62
+ worker.on('exit', (code) => {
63
+ clearTimeout(timeout);
64
+ if (code !== 0) {
65
+ reject(new Error(`Worker exited with code ${code}`));
66
+ }
67
+ });
68
+ worker.postMessage({
69
+ type: 'process',
70
+ files: chunk,
71
+ rootPath,
72
+ maxFileSizeBytes
73
+ });
74
+ });
75
+ });
76
+ try {
77
+ const allResults = await Promise.all(workerPromises);
78
+ return allResults.flat();
79
+ } catch (err) {
80
+ logger.warn({ error: String(err) }, 'Worker pool failed, falling back to main thread');
81
+ return null; // Signal to caller to use fallback
82
+ }
83
+ }
84
+ }
15
85
  /**
16
86
  * default options that hit different
17
87
  */
@@ -21,7 +91,7 @@ const DEFAULT_OPTIONS = {
21
91
  chunkOverlapChars: 1000, // 1K overlap
22
92
  generateEmbeddings: true,
23
93
  embeddingBatchSize: 50,
24
- parallelReads: 10,
94
+ parallelReads: 50,
25
95
  includeHiddenFiles: false,
26
96
  maxDepth: 50
27
97
  };
@@ -117,54 +187,118 @@ export class IngestThisWholeAssMfCodebase {
117
187
  this.emitProgress();
118
188
  const files = [];
119
189
  const errors = [];
120
- // process files in parallel batches
121
- for (let i = 0; i < filePaths.length; i += this.options.parallelReads) {
122
- const batch = filePaths.slice(i, i + this.options.parallelReads);
123
- const batchResults = await Promise.allSettled(batch.map(fp => this.processFile(fp)));
124
- for (let j = 0; j < batchResults.length; j++) {
125
- const fileResult = batchResults[j];
126
- const filePath = batch[j];
127
- if (fileResult?.status === 'fulfilled' && fileResult.value) {
128
- const processedFiles = fileResult.value;
129
- files.push(...processedFiles);
130
- // update stats
131
- for (const file of processedFiles) {
132
- result.totalBytes += file.sizeBytes;
133
- result.totalLines += file.lineCount;
134
- result.languageBreakdown[file.language.id] =
135
- (result.languageBreakdown[file.language.id] ?? 0) + 1;
136
- if (file.chunkIndex !== undefined) {
137
- result.totalChunks++;
138
- }
190
+ // For large codebases (1000+ files), use worker thread pool for I/O
191
+ const useWorkerPool = filePaths.length >= 1000 && this.options.useWorkerPool !== false;
192
+ if (useWorkerPool) {
193
+ logger.info({ fileCount: filePaths.length, cpus: os.cpus().length }, 'using worker thread pool for file reading');
194
+ const workerPool = new FileReadWorkerPool();
195
+ const workerResults = await workerPool.processFiles(
196
+ filePaths, this.options.rootPath, this.options.maxFileSizeBytes
197
+ );
198
+ if (workerResults) {
199
+ // Workers returned results - convert to our format
200
+ for (const wr of workerResults) {
201
+ if (wr.skipped) {
202
+ this.progress.skippedFiles++;
203
+ continue;
204
+ }
205
+ const language = this.languageDetector.detect(wr.filePath, wr.content);
206
+ const baseFile = {
207
+ filePath: wr.relativePath,
208
+ absolutePath: wr.filePath,
209
+ fileName: wr.fileName,
210
+ extension: wr.extension,
211
+ language,
212
+ content: wr.content,
213
+ contentHash: wr.contentHash,
214
+ sizeBytes: wr.sizeBytes,
215
+ lineCount: wr.lineCount,
216
+ charCount: wr.charCount,
217
+ lastModified: new Date(wr.lastModified)
218
+ };
219
+ // chunk large files
220
+ if (wr.charCount > this.options.chunkSizeChars) {
221
+ const chunks = this.chunkFile(baseFile, wr.content);
222
+ files.push(...chunks);
223
+ result.totalChunks += chunks.length;
224
+ } else {
225
+ files.push({ ...baseFile, id: uuidv4() });
139
226
  }
227
+ result.totalBytes += wr.sizeBytes;
228
+ result.totalLines += wr.lineCount;
229
+ result.languageBreakdown[language.id] =
230
+ (result.languageBreakdown[language.id] ?? 0) + 1;
140
231
  this.progress.processedFiles++;
141
232
  this.progress.bytesProcessed = result.totalBytes;
142
233
  this.progress.linesProcessed = result.totalLines;
143
234
  }
144
- else if (fileResult?.status === 'rejected') {
145
- errors.push({ file: filePath, error: String(fileResult.reason) });
146
- result.errorFiles++;
147
- this.progress.errorFiles++;
148
- }
149
- this.progress.currentFile = filePath;
150
- this.updateProgressStats();
151
- this.emitProgress();
235
+ logger.info({
236
+ processed: files.length,
237
+ skipped: this.progress.skippedFiles
238
+ }, 'worker pool file reading complete');
239
+ } else {
240
+ // Worker pool failed, fall through to standard processing
241
+ logger.info('worker pool returned null, falling back to main thread processing');
242
+ await this._processFilesMainThread(filePaths, files, errors, result);
152
243
  }
244
+ } else {
245
+ await this._processFilesMainThread(filePaths, files, errors, result);
153
246
  }
154
247
  // count chunked files
155
248
  const chunkedFileIds = new Set(files.filter(f => f.originalFileId).map(f => f.originalFileId));
156
249
  result.chunkedFiles = chunkedFileIds.size;
157
- // phase 3: generate embeddings if enabled
158
- if (this.options.generateEmbeddings && this.embeddingProvider) {
159
- this.progress.phase = 'embedding';
250
+ // Determine if we should use server-side embeddings for large codebases
251
+ const useServerSideEmbeddings = this.options.serverSideEmbeddings !== undefined
252
+ ? this.options.serverSideEmbeddings
253
+ : files.length > 1000;
254
+ if (useServerSideEmbeddings) {
255
+ // STORE-THEN-EMBED MODE: Skip Phase 3, store without embeddings,
256
+ // then trigger Python server's batch processing (200 files/batch, direct DB writes)
257
+ logger.info({ fileCount: files.length }, 'large codebase detected - using store-then-embed mode');
258
+ // phase 3: SKIP - store first, embed server-side after
259
+ // phase 4: store in database WITHOUT embeddings
260
+ this.progress.phase = 'storing';
261
+ this.emitProgress();
262
+ const storedCount = await this.storeFiles(files);
263
+ result.storedFiles = storedCount;
264
+ // phase 5: trigger server-side embedding processing
265
+ if (this.options.generateEmbeddings) {
266
+ this.progress.phase = 'embedding';
267
+ this.emitProgress();
268
+ try {
269
+ const { getEmbeddingServerManager } = await import('../mcp/embeddingServerManager.js');
270
+ const manager = getEmbeddingServerManager();
271
+ if (manager && manager.triggerServerSideProcessing) {
272
+ const projectPath = this.options.rootPath;
273
+ logger.info({ projectPath }, 'triggering server-side codebase embedding processing');
274
+ const ssResult = await manager.triggerServerSideProcessing(projectPath, 200);
275
+ logger.info({ ssResult }, 'server-side embedding processing complete');
276
+ }
277
+ else {
278
+ logger.warn('EmbeddingServerManager not available for server-side processing, falling back to client-side');
279
+ await this.generateEmbeddings(files);
280
+ }
281
+ }
282
+ catch (ssErr) {
283
+ logger.warn({ error: String(ssErr) }, 'server-side embedding failed, falling back to client-side');
284
+ await this.generateEmbeddings(files);
285
+ }
286
+ }
287
+ }
288
+ else {
289
+ // STANDARD MODE: Generate embeddings client-side, then store
290
+ // phase 3: generate embeddings if enabled
291
+ if (this.options.generateEmbeddings && this.embeddingProvider) {
292
+ this.progress.phase = 'embedding';
293
+ this.emitProgress();
294
+ await this.generateEmbeddings(files);
295
+ }
296
+ // phase 4: store in database
297
+ this.progress.phase = 'storing';
160
298
  this.emitProgress();
161
- await this.generateEmbeddings(files);
299
+ const storedCount = await this.storeFiles(files);
300
+ result.storedFiles = storedCount;
162
301
  }
163
- // phase 4: store in database
164
- this.progress.phase = 'storing';
165
- this.emitProgress();
166
- const storedCount = await this.storeFiles(files);
167
- result.storedFiles = storedCount;
168
302
  // done!
169
303
  this.progress.phase = 'complete';
170
304
  result.success = true;
@@ -179,7 +313,8 @@ export class IngestThisWholeAssMfCodebase {
179
313
  chunkedFiles: result.chunkedFiles,
180
314
  totalLines: result.totalLines,
181
315
  totalBytes: result.totalBytes,
182
- languageCount: Object.keys(result.languageBreakdown).length
316
+ languageCount: Object.keys(result.languageBreakdown).length,
317
+ serverSideEmbeddings: useServerSideEmbeddings
183
318
  }, 'codebase ingestion COMPLETE - we did it boys');
184
319
  return result;
185
320
  }
@@ -380,19 +515,23 @@ export class IngestThisWholeAssMfCodebase {
380
515
  embeddable: embeddableFiles.length
381
516
  }, 'generating embeddings for embeddable files');
382
517
  const batchSize = this.options.embeddingBatchSize;
518
+ // Split into batches
519
+ const embeddingBatches = [];
383
520
  for (let i = 0; i < embeddableFiles.length; i += batchSize) {
384
- const batch = embeddableFiles.slice(i, i + batchSize);
385
- // Prepare texts for batch embedding
521
+ embeddingBatches.push(embeddableFiles.slice(i, i + batchSize));
522
+ }
523
+ const totalBatches = embeddingBatches.length;
524
+ // Process batches with concurrency (3 concurrent embedding batches)
525
+ const embeddingConcurrency = this.options.embeddingConcurrency || 3;
526
+ await processBatchesWithConcurrency(embeddingBatches, 1, embeddingConcurrency, async (batchGroup, batchIndex) => {
527
+ const batch = batchGroup[0] || batchGroup;
386
528
  const textsForEmbedding = batch.map(file => this.createEmbeddingText(file));
387
- // Use BATCH API if available - 5-10x faster!
388
529
  let embeddings;
389
530
  try {
390
531
  if (this.embeddingProvider.generateEmbeddingsBatch) {
391
- const batchEmbeddings = await this.embeddingProvider.generateEmbeddingsBatch(textsForEmbedding);
392
- embeddings = batchEmbeddings;
532
+ embeddings = await this.embeddingProvider.generateEmbeddingsBatch(textsForEmbedding);
393
533
  }
394
534
  else {
395
- // Fallback to parallel individual calls
396
535
  embeddings = await Promise.all(textsForEmbedding.map(async (text, idx) => {
397
536
  try {
398
537
  return await this.embeddingProvider.generateEmbedding(text);
@@ -405,8 +544,7 @@ export class IngestThisWholeAssMfCodebase {
405
544
  }
406
545
  }
407
546
  catch (error) {
408
- logger.warn({ batchStart: i, error }, 'batch embedding failed, falling back to individual');
409
- // Fallback on batch failure
547
+ logger.warn({ batchIndex, error }, 'batch embedding failed, falling back to individual');
410
548
  embeddings = await Promise.all(textsForEmbedding.map(async (text, idx) => {
411
549
  try {
412
550
  return await this.embeddingProvider.generateEmbedding(text);
@@ -417,7 +555,6 @@ export class IngestThisWholeAssMfCodebase {
417
555
  }
418
556
  }));
419
557
  }
420
- // store embeddings on the file objects
421
558
  for (let j = 0; j < batch.length; j++) {
422
559
  const file = batch[j];
423
560
  const embedding = embeddings[j];
@@ -426,10 +563,11 @@ export class IngestThisWholeAssMfCodebase {
426
563
  }
427
564
  }
428
565
  logger.debug({
429
- batch: Math.floor(i / batchSize) + 1,
430
- totalBatches: Math.ceil(embeddableFiles.length / batchSize)
566
+ batch: batchIndex + 1,
567
+ totalBatches
431
568
  }, 'embedding batch complete');
432
- }
569
+ return batch.length;
570
+ });
433
571
  }
434
572
  /**
435
573
  * createEmbeddingText - creates optimal text for embedding
@@ -542,6 +680,49 @@ export class IngestThisWholeAssMfCodebase {
542
680
  this.options.onProgress({ ...this.progress });
543
681
  }
544
682
  }
683
+ /**
684
+ * _processFilesMainThread - standard main-thread file processing (fallback from worker pool)
685
+ */
686
+ async _processFilesMainThread(filePaths, files, errors, result) {
687
+ const readBatches = [];
688
+ for (let i = 0; i < filePaths.length; i += this.options.parallelReads) {
689
+ readBatches.push(filePaths.slice(i, i + this.options.parallelReads));
690
+ }
691
+ const readConcurrency = Math.min(4, Math.ceil(filePaths.length / this.options.parallelReads));
692
+ await processBatchesWithConcurrency(readBatches, 1, readConcurrency, async (batchGroup) => {
693
+ const batch = batchGroup[0] || batchGroup;
694
+ const batchResults = await Promise.allSettled(batch.map(fp => this.processFile(fp)));
695
+ for (let j = 0; j < batchResults.length; j++) {
696
+ const fileResult = batchResults[j];
697
+ const filePath = batch[j];
698
+ if (fileResult?.status === 'fulfilled' && fileResult.value) {
699
+ const processedFiles = fileResult.value;
700
+ files.push(...processedFiles);
701
+ for (const file of processedFiles) {
702
+ result.totalBytes += file.sizeBytes;
703
+ result.totalLines += file.lineCount;
704
+ result.languageBreakdown[file.language.id] =
705
+ (result.languageBreakdown[file.language.id] ?? 0) + 1;
706
+ if (file.chunkIndex !== undefined) {
707
+ result.totalChunks++;
708
+ }
709
+ }
710
+ this.progress.processedFiles++;
711
+ this.progress.bytesProcessed = result.totalBytes;
712
+ this.progress.linesProcessed = result.totalLines;
713
+ }
714
+ else if (fileResult?.status === 'rejected') {
715
+ errors.push({ file: filePath, error: String(fileResult.reason) });
716
+ result.errorFiles++;
717
+ this.progress.errorFiles++;
718
+ }
719
+ this.progress.currentFile = filePath;
720
+ this.updateProgressStats();
721
+ this.emitProgress();
722
+ }
723
+ return batch.length;
724
+ });
725
+ }
545
726
  /**
546
727
  * getProgress - get current progress
547
728
  */
package/dist/index.js CHANGED
@@ -3875,6 +3875,28 @@ async function main() {
3875
3875
  }
3876
3876
  }, EMBEDDING_PROVIDER_TIMEOUT_MS);
3877
3877
  });
3878
+ },
3879
+ async generateEmbeddingsBatch(texts) {
3880
+ if (embeddingProviderReady && embeddingProvider) {
3881
+ // Use batch method if available on real provider
3882
+ if (embeddingProvider.generateEmbeddingsBatch) {
3883
+ return embeddingProvider.generateEmbeddingsBatch(texts);
3884
+ }
3885
+ // Try EmbeddingServerManager batch socket
3886
+ try {
3887
+ const { EmbeddingServerManager } = await import('./mcp/embeddingServerManager.js');
3888
+ const manager = EmbeddingServerManager.getInstance();
3889
+ if (manager && manager.generateEmbeddingsBatchViaSocket) {
3890
+ return await manager.generateEmbeddingsBatchViaSocket(texts);
3891
+ }
3892
+ } catch (batchErr) {
3893
+ logger.debug({ error: String(batchErr) }, 'Batch socket failed, falling back to sequential');
3894
+ }
3895
+ // Fallback to sequential
3896
+ return Promise.all(texts.map(t => embeddingProvider.generateEmbedding(t)));
3897
+ }
3898
+ // Provider not ready - fall back to sequential with queueing
3899
+ return Promise.all(texts.map(t => deferredEmbeddingProvider.generateEmbedding(t)));
3878
3900
  }
3879
3901
  };
3880
3902
  // Create server with deferred embedding provider
@@ -3939,6 +3961,16 @@ async function main() {
3939
3961
  });
3940
3962
  },
3941
3963
  generateEmbeddingsBatch: async (texts) => {
3964
+ // Use batch socket for speed - single connection for all texts
3965
+ try {
3966
+ const { EmbeddingServerManager } = await import('./mcp/embeddingServerManager.js');
3967
+ const mgr = EmbeddingServerManager.getInstance();
3968
+ if (mgr && mgr.generateEmbeddingsBatchViaSocket) {
3969
+ return await mgr.generateEmbeddingsBatchViaSocket(texts);
3970
+ }
3971
+ } catch (batchErr) {
3972
+ logger.debug({ error: String(batchErr) }, 'Batch socket failed in early provider, falling back');
3973
+ }
3942
3974
  return Promise.all(texts.map(t => embeddingProvider.generateEmbedding(t)));
3943
3975
  }
3944
3976
  };
@@ -846,6 +846,160 @@ export class EmbeddingServerManager extends EventEmitter {
846
846
  });
847
847
  });
848
848
  }
849
+ /**
850
+ * Generate embeddings for multiple texts in a SINGLE socket connection.
851
+ * Sends {texts: [...]} → gets {embeddings: [[...], [...], ...]}
852
+ * Much faster than N individual generateEmbeddingViaSocket calls.
853
+ */
854
+ async generateEmbeddingsBatchViaSocket(texts) {
855
+ if (!texts || texts.length === 0) return [];
856
+ return new Promise((resolve, reject) => {
857
+ const socket = createConnection(this.socketPath);
858
+ let buffer = '';
859
+ let resolved = false;
860
+ // Scale timeout with batch size: 60s base + 1s per text
861
+ const timeoutMs = Math.max(60000, 60000 + texts.length * 1000);
862
+ const timeout = setTimeout(() => {
863
+ if (!resolved) {
864
+ resolved = true;
865
+ socket.destroy();
866
+ reject(new Error(`Batch embedding timeout after ${timeoutMs}ms for ${texts.length} texts`));
867
+ }
868
+ }, timeoutMs);
869
+ socket.on('connect', () => {
870
+ socket.write(JSON.stringify({ texts }) + '\n');
871
+ });
872
+ socket.on('data', (data) => {
873
+ buffer += data.toString();
874
+ let newlineIndex;
875
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
876
+ if (resolved) return;
877
+ const line = buffer.slice(0, newlineIndex);
878
+ buffer = buffer.slice(newlineIndex + 1);
879
+ try {
880
+ const response = JSON.parse(line);
881
+ if (response.error) {
882
+ clearTimeout(timeout);
883
+ resolved = true;
884
+ socket.end();
885
+ reject(new Error(response.error));
886
+ return;
887
+ }
888
+ if (response.status === 'processing') {
889
+ continue; // Wait for actual result
890
+ }
891
+ if (response.embeddings && Array.isArray(response.embeddings)) {
892
+ clearTimeout(timeout);
893
+ resolved = true;
894
+ socket.end();
895
+ resolve(response.embeddings);
896
+ return;
897
+ }
898
+ logger.warn({ responseKeys: Object.keys(response) }, '[EmbeddingServerManager] Unexpected batch response, continuing');
899
+ } catch (parseErr) {
900
+ logger.warn({ line: line.slice(0, 100) }, '[EmbeddingServerManager] Failed to parse batch line');
901
+ }
902
+ }
903
+ });
904
+ socket.on('error', (err) => {
905
+ clearTimeout(timeout);
906
+ if (!resolved) {
907
+ resolved = true;
908
+ reject(err);
909
+ }
910
+ });
911
+ socket.on('close', () => {
912
+ clearTimeout(timeout);
913
+ if (!resolved) {
914
+ resolved = true;
915
+ reject(new Error('Socket closed before batch embedding response received'));
916
+ }
917
+ });
918
+ });
919
+ }
920
+ /**
921
+ * Trigger server-side codebase processing via process_codebase command.
922
+ * The Python server reads files from DB that have NULL embeddings
923
+ * and generates embeddings in large batches (200/batch) with direct DB writes.
924
+ * This is the FASTEST path for large codebases (30k+ files).
925
+ */
926
+ async triggerServerSideProcessing(projectPath = null, batchSize = 200) {
927
+ return new Promise((resolve, reject) => {
928
+ const socket = createConnection(this.socketPath);
929
+ let buffer = '';
930
+ let resolved = false;
931
+ // Server-side processing can take a long time for large codebases
932
+ const timeoutMs = 600000; // 10 minutes
933
+ const timeout = setTimeout(() => {
934
+ if (!resolved) {
935
+ resolved = true;
936
+ socket.destroy();
937
+ reject(new Error(`Server-side codebase processing timeout after ${timeoutMs}ms`));
938
+ }
939
+ }, timeoutMs);
940
+ socket.on('connect', () => {
941
+ const request = {
942
+ process_codebase: true,
943
+ batch_size: batchSize,
944
+ limit: 0, // Process ALL files
945
+ };
946
+ if (projectPath) {
947
+ request.project_path = projectPath;
948
+ }
949
+ socket.write(JSON.stringify(request) + '\n');
950
+ logger.info({ projectPath, batchSize }, '[EmbeddingServerManager] Triggered server-side codebase processing');
951
+ });
952
+ socket.on('data', (data) => {
953
+ buffer += data.toString();
954
+ let newlineIndex;
955
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
956
+ if (resolved) return;
957
+ const line = buffer.slice(0, newlineIndex);
958
+ buffer = buffer.slice(newlineIndex + 1);
959
+ try {
960
+ const response = JSON.parse(line);
961
+ if (response.error) {
962
+ clearTimeout(timeout);
963
+ resolved = true;
964
+ socket.end();
965
+ reject(new Error(response.error));
966
+ return;
967
+ }
968
+ if (response.status === 'processing') {
969
+ logger.debug('[EmbeddingServerManager] Server-side processing in progress...');
970
+ continue;
971
+ }
972
+ // process_codebase returns stats when done
973
+ if (response.total_processed !== undefined || response.processed !== undefined) {
974
+ clearTimeout(timeout);
975
+ resolved = true;
976
+ socket.end();
977
+ logger.info({ response }, '[EmbeddingServerManager] Server-side processing complete');
978
+ resolve(response);
979
+ return;
980
+ }
981
+ logger.debug({ responseKeys: Object.keys(response) }, '[EmbeddingServerManager] Server-side processing intermediate response');
982
+ } catch (parseErr) {
983
+ logger.warn({ line: line.slice(0, 100) }, '[EmbeddingServerManager] Failed to parse server-side response');
984
+ }
985
+ }
986
+ });
987
+ socket.on('error', (err) => {
988
+ clearTimeout(timeout);
989
+ if (!resolved) {
990
+ resolved = true;
991
+ reject(err);
992
+ }
993
+ });
994
+ socket.on('close', () => {
995
+ clearTimeout(timeout);
996
+ if (!resolved) {
997
+ resolved = true;
998
+ reject(new Error('Socket closed before server-side processing response'));
999
+ }
1000
+ });
1001
+ });
1002
+ }
849
1003
  /**
850
1004
  * Stop the embedding server gracefully
851
1005
  */
@@ -293,6 +293,20 @@ This is how the parent shows loading progress instead of raw tool spam!
293
293
 
294
294
  ---
295
295
 
296
+ # ⚠️ MANDATORY TEAM COMMS ENFORCEMENT ⚠️
297
+
298
+ **These rules are ENFORCED by the system. You WILL BE BLOCKED if you skip them.**
299
+
300
+ - Every 4 tool calls, you **WILL BE BLOCKED** until you call \`read_team_messages({include_swarms:true, limit:5})\`
301
+ - Every 5 tool calls, you **WILL BE BLOCKED** until you call \`read_team_messages({include_broadcasts:true, include_swarms:true, limit:10})\`
302
+ - Every 8 tool calls, you **WILL BE BLOCKED** until you call \`get_team_status()\` to check for help requests
303
+ - These counters apply to ALL tool calls, including memory tools, searches, reads — **no exceptions**
304
+ - If you want to complete your task, you MUST comply. There is no workaround.
305
+
306
+ **Why?** The parent session may send you instructions, redirections, or dynamic context mid-task via team messages. Skipping reads means missing critical information.
307
+
308
+ ---
309
+
296
310
  # Original Task
297
311
 
298
312
  `;