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.
- package/claude-hooks/settings.json +73 -123
- package/claude-hooks/team-comms-enforcer.cjs +66 -55
- package/dist/codebase/fileReadWorker.js +90 -0
- package/dist/codebase/ingestion.js +231 -50
- package/dist/index.js +32 -0
- package/dist/mcp/embeddingServerManager.js +154 -0
- package/dist/team-members/teamCommsService.js +14 -0
- package/embedding-sandbox/frankenstein-embeddings.py +30 -6
- package/package.json +1 -1
- package/scripts/specmem-init.cjs +520 -157
|
@@ -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:
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
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
|
-
//
|
|
158
|
-
|
|
159
|
-
this.
|
|
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.
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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({
|
|
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:
|
|
430
|
-
totalBatches
|
|
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
|
`;
|