@danielblomma/cortex-mcp 2.2.4 → 2.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.
- package/README.md +62 -7
- package/bin/cortex.mjs +31 -167
- package/package.json +2 -2
- package/scaffold/mcp/package-lock.json +24 -21
- package/scaffold/mcp/src/cli/query.ts +36 -2
- package/scaffold/mcp/src/embed.ts +234 -21
- package/scaffold/mcp/src/embedScheduler.ts +69 -1
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
- package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/embed-scheduler.test.mjs +52 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
- package/scaffold/mcp/tests/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
- package/scaffold/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- package/scaffold/scripts/status.sh +13 -6
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_SCHEDULER_OPTIONS,
|
|
12
12
|
groupDuplicates,
|
|
13
13
|
packWorkUnits,
|
|
14
|
+
resolveEffectiveTokenBudget,
|
|
14
15
|
resolveInFlightTokens,
|
|
15
16
|
resolveMemoryHeadroom,
|
|
16
17
|
resolveModelMaxTokens,
|
|
@@ -30,6 +31,31 @@ const MODEL_CACHE_DIR = PATHS.embeddingsModelCache;
|
|
|
30
31
|
const EMBEDDINGS_DIR = path.dirname(EMBEDDINGS_PATH);
|
|
31
32
|
|
|
32
33
|
export const DEFAULT_MODEL_ID = "jinaai/jina-embeddings-v2-base-code";
|
|
34
|
+
export const COMPACT_FILE_TEXT_STRATEGY = "compact_files_v1";
|
|
35
|
+
export const COMPACT_FILE_TEXT_THRESHOLD_CHARS = 32768;
|
|
36
|
+
export const COMPACT_FILE_TEXT_TARGET_CHARS = 16000;
|
|
37
|
+
|
|
38
|
+
const COMPACT_FILE_SIGNAL_BUDGET_CHARS = 4096;
|
|
39
|
+
const COMPACT_FILE_SIGNAL_MAX_LINE_CHARS = 512;
|
|
40
|
+
const COMPACT_FILE_MIN_HEAD_CHARS = 4096;
|
|
41
|
+
const COMPACT_FILE_MIN_TAIL_CHARS = 2048;
|
|
42
|
+
|
|
43
|
+
export type EmbedTextProfile = "full" | "compact-files";
|
|
44
|
+
|
|
45
|
+
type FileEmbeddingTextResult = {
|
|
46
|
+
text: string;
|
|
47
|
+
profile: EmbedTextProfile;
|
|
48
|
+
compacted: boolean;
|
|
49
|
+
original_chars: number;
|
|
50
|
+
text_chars: number;
|
|
51
|
+
omitted_chars: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ParseFileEntitiesOptions = {
|
|
55
|
+
textProfile?: EmbedTextProfile;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type SignatureEntityType = "File" | "Rule" | "ADR" | "Module" | "Project" | "Chunk";
|
|
33
59
|
|
|
34
60
|
export function resolveModelId(): string {
|
|
35
61
|
return (process.env.CORTEX_EMBED_MODEL ?? DEFAULT_MODEL_ID).trim() || DEFAULT_MODEL_ID;
|
|
@@ -47,6 +73,11 @@ type FileEntity = {
|
|
|
47
73
|
updated_at: string;
|
|
48
74
|
text: string;
|
|
49
75
|
signature: string;
|
|
76
|
+
text_profile: EmbedTextProfile;
|
|
77
|
+
text_compacted: boolean;
|
|
78
|
+
text_original_chars: number;
|
|
79
|
+
text_chars: number;
|
|
80
|
+
text_omitted_chars: number;
|
|
50
81
|
};
|
|
51
82
|
|
|
52
83
|
type RuleEntity = {
|
|
@@ -154,6 +185,129 @@ function normalizeText(value: string): string {
|
|
|
154
185
|
return value.replace(/\s+/g, " ").trim();
|
|
155
186
|
}
|
|
156
187
|
|
|
188
|
+
export function resolveEmbedTextProfile(raw = process.env.CORTEX_EMBED_TEXT_PROFILE): EmbedTextProfile {
|
|
189
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
190
|
+
if (!value || value === "full") {
|
|
191
|
+
return "full";
|
|
192
|
+
}
|
|
193
|
+
if (value === "compact-files") {
|
|
194
|
+
return "compact-files";
|
|
195
|
+
}
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Unsupported CORTEX_EMBED_TEXT_PROFILE=${JSON.stringify(raw)}; expected "full" or "compact-files"`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isSignalLine(line: string): boolean {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (!trimmed) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return (
|
|
207
|
+
/^(import|export)\b/.test(trimmed) ||
|
|
208
|
+
/^(abstract\s+|async\s+|public\s+|private\s+|protected\s+|static\s+|readonly\s+|override\s+)*(class|interface|type|enum|function)\b/.test(trimmed) ||
|
|
209
|
+
/^(const|let|var)\s+[$A-Z_a-z][$\w]*\s*=/.test(trimmed) ||
|
|
210
|
+
/^(describe|it|test)\s*\(/.test(trimmed) ||
|
|
211
|
+
/^(@[A-Z_a-z][$\w]*|#[#\s])/.test(trimmed) ||
|
|
212
|
+
/^```[A-Za-z0-9_-]+/.test(trimmed) ||
|
|
213
|
+
/\b(route|router|endpoint|controller|handler|middleware|permission|auth|token|secret|security|todo|fixme)\b/i.test(trimmed)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectSignalLines(content: string, budgetChars: number): string {
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
const seen = new Set<string>();
|
|
220
|
+
let used = 0;
|
|
221
|
+
|
|
222
|
+
for (const line of content.split(/\r?\n/)) {
|
|
223
|
+
if (!isSignalLine(line)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const normalized = line.trimEnd();
|
|
227
|
+
const signalLine = normalized.length > COMPACT_FILE_SIGNAL_MAX_LINE_CHARS
|
|
228
|
+
? `${normalized.slice(0, COMPACT_FILE_SIGNAL_MAX_LINE_CHARS)} [cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_line_truncated_chars=${normalized.length - COMPACT_FILE_SIGNAL_MAX_LINE_CHARS}]`
|
|
229
|
+
: normalized;
|
|
230
|
+
if (seen.has(signalLine)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const next = used + signalLine.length + 1;
|
|
234
|
+
if (next > budgetChars) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
lines.push(signalLine);
|
|
238
|
+
seen.add(signalLine);
|
|
239
|
+
used = next;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function buildFileEmbeddingText(
|
|
246
|
+
filePath: string,
|
|
247
|
+
excerpt: string,
|
|
248
|
+
content: string,
|
|
249
|
+
profile: EmbedTextProfile = "full"
|
|
250
|
+
): FileEmbeddingTextResult {
|
|
251
|
+
const fullText = `${filePath}\n${excerpt}\n${content}`;
|
|
252
|
+
if (profile === "full" || fullText.length <= COMPACT_FILE_TEXT_THRESHOLD_CHARS) {
|
|
253
|
+
return {
|
|
254
|
+
text: fullText,
|
|
255
|
+
profile,
|
|
256
|
+
compacted: false,
|
|
257
|
+
original_chars: fullText.length,
|
|
258
|
+
text_chars: fullText.length,
|
|
259
|
+
omitted_chars: 0
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const signalText = collectSignalLines(content, COMPACT_FILE_SIGNAL_BUDGET_CHARS);
|
|
264
|
+
const markerPrefix = `[cortex ${COMPACT_FILE_TEXT_STRATEGY} omitted_chars=`;
|
|
265
|
+
const staticChars =
|
|
266
|
+
filePath.length +
|
|
267
|
+
excerpt.length +
|
|
268
|
+
signalText.length +
|
|
269
|
+
markerPrefix.length +
|
|
270
|
+
64;
|
|
271
|
+
const available = Math.max(
|
|
272
|
+
COMPACT_FILE_MIN_HEAD_CHARS + COMPACT_FILE_MIN_TAIL_CHARS,
|
|
273
|
+
COMPACT_FILE_TEXT_TARGET_CHARS - staticChars
|
|
274
|
+
);
|
|
275
|
+
const headChars = Math.max(COMPACT_FILE_MIN_HEAD_CHARS, Math.floor(available * 0.62));
|
|
276
|
+
const tailChars = Math.max(COMPACT_FILE_MIN_TAIL_CHARS, available - headChars);
|
|
277
|
+
const head = content.slice(0, headChars);
|
|
278
|
+
const tail = content.slice(-tailChars);
|
|
279
|
+
const omittedChars = Math.max(0, content.length - head.length - tail.length);
|
|
280
|
+
const marker = `${markerPrefix}${omittedChars}]`;
|
|
281
|
+
const compactText = [
|
|
282
|
+
filePath,
|
|
283
|
+
excerpt,
|
|
284
|
+
head,
|
|
285
|
+
marker,
|
|
286
|
+
signalText ? `[cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_lines]\n${signalText}` : "",
|
|
287
|
+
tail
|
|
288
|
+
].filter((part) => part.length > 0).join("\n");
|
|
289
|
+
|
|
290
|
+
if (compactText.length >= fullText.length) {
|
|
291
|
+
return {
|
|
292
|
+
text: fullText,
|
|
293
|
+
profile,
|
|
294
|
+
compacted: false,
|
|
295
|
+
original_chars: fullText.length,
|
|
296
|
+
text_chars: fullText.length,
|
|
297
|
+
omitted_chars: 0
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
text: compactText,
|
|
303
|
+
profile,
|
|
304
|
+
compacted: true,
|
|
305
|
+
original_chars: fullText.length,
|
|
306
|
+
text_chars: compactText.length,
|
|
307
|
+
omitted_chars: fullText.length - compactText.length
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
157
311
|
function ensureRequiredFiles(): void {
|
|
158
312
|
const required = [
|
|
159
313
|
path.join(CACHE_DIR, "documents.jsonl"),
|
|
@@ -168,7 +322,8 @@ function ensureRequiredFiles(): void {
|
|
|
168
322
|
}
|
|
169
323
|
}
|
|
170
324
|
|
|
171
|
-
export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
|
|
325
|
+
export function parseFileEntities(raw: JsonObject[], options: ParseFileEntitiesOptions = {}): FileEntity[] {
|
|
326
|
+
const textProfile = options.textProfile ?? "full";
|
|
172
327
|
return raw
|
|
173
328
|
.map((item) => {
|
|
174
329
|
const id = asString(item.id);
|
|
@@ -181,7 +336,7 @@ export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
|
|
|
181
336
|
const excerpt = asString(item.excerpt);
|
|
182
337
|
const updatedAt = asString(item.updated_at);
|
|
183
338
|
const checksum = asString(item.checksum, hashText(content));
|
|
184
|
-
const
|
|
339
|
+
const embeddingText = buildFileEmbeddingText(filePath, excerpt, content, textProfile);
|
|
185
340
|
|
|
186
341
|
return {
|
|
187
342
|
id,
|
|
@@ -193,8 +348,13 @@ export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
|
|
|
193
348
|
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
194
349
|
trust_level: asNumber(item.trust_level, 50),
|
|
195
350
|
updated_at: updatedAt,
|
|
196
|
-
text,
|
|
197
|
-
signature: hashText(`file|${checksum}|${updatedAt}|${hashText(text)}`)
|
|
351
|
+
text: embeddingText.text,
|
|
352
|
+
signature: hashText(`file|${checksum}|${updatedAt}|${hashText(embeddingText.text)}`),
|
|
353
|
+
text_profile: embeddingText.profile,
|
|
354
|
+
text_compacted: embeddingText.compacted,
|
|
355
|
+
text_original_chars: embeddingText.original_chars,
|
|
356
|
+
text_chars: embeddingText.text_chars,
|
|
357
|
+
text_omitted_chars: embeddingText.omitted_chars
|
|
198
358
|
};
|
|
199
359
|
})
|
|
200
360
|
.filter((value): value is FileEntity => value !== null);
|
|
@@ -407,8 +567,24 @@ function roundVector(values: number[]): number[] {
|
|
|
407
567
|
return values.map((value) => Number(value.toFixed(6)));
|
|
408
568
|
}
|
|
409
569
|
|
|
410
|
-
function resolveSignatureProfile(
|
|
411
|
-
|
|
570
|
+
export function resolveSignatureProfile(
|
|
571
|
+
maxTokenCap: number | null,
|
|
572
|
+
textProfile: EmbedTextProfile = "full",
|
|
573
|
+
entityType?: SignatureEntityType
|
|
574
|
+
): string {
|
|
575
|
+
const parts: string[] = [];
|
|
576
|
+
if (maxTokenCap) {
|
|
577
|
+
parts.push(`max_tokens=${maxTokenCap}`);
|
|
578
|
+
}
|
|
579
|
+
if (textProfile === "compact-files" && (entityType === undefined || entityType === "File")) {
|
|
580
|
+
parts.push(
|
|
581
|
+
"text_profile=compact-files",
|
|
582
|
+
COMPACT_FILE_TEXT_STRATEGY,
|
|
583
|
+
`threshold_chars=${COMPACT_FILE_TEXT_THRESHOLD_CHARS}`,
|
|
584
|
+
`target_chars=${COMPACT_FILE_TEXT_TARGET_CHARS}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return parts.length ? `embed|${parts.join("|")}` : "";
|
|
412
588
|
}
|
|
413
589
|
|
|
414
590
|
function embeddingSignature(entitySignature: string, profile: string): string {
|
|
@@ -433,8 +609,19 @@ async function main(): Promise<void> {
|
|
|
433
609
|
fs.mkdirSync(MODEL_CACHE_DIR, { recursive: true });
|
|
434
610
|
|
|
435
611
|
const modelId = resolveModelId();
|
|
436
|
-
|
|
437
|
-
|
|
612
|
+
const textProfile = resolveEmbedTextProfile();
|
|
613
|
+
|
|
614
|
+
const documents = parseFileEntities(readJsonl(path.join(CACHE_DIR, "documents.jsonl")), { textProfile });
|
|
615
|
+
const textProfileStats = {
|
|
616
|
+
strategy: textProfile === "compact-files" ? COMPACT_FILE_TEXT_STRATEGY : null,
|
|
617
|
+
threshold_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_THRESHOLD_CHARS : null,
|
|
618
|
+
target_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_TARGET_CHARS : null,
|
|
619
|
+
file_entities: documents.length,
|
|
620
|
+
compacted_files: documents.filter((doc) => doc.text_compacted).length,
|
|
621
|
+
original_chars: documents.reduce((total, doc) => total + doc.text_original_chars, 0),
|
|
622
|
+
text_chars: documents.reduce((total, doc) => total + doc.text_chars, 0),
|
|
623
|
+
saved_chars: documents.reduce((total, doc) => total + doc.text_omitted_chars, 0)
|
|
624
|
+
};
|
|
438
625
|
const rules = parseRuleEntities(readJsonl(path.join(CACHE_DIR, "entities.rule.jsonl")));
|
|
439
626
|
const adrs = parseAdrEntities(readJsonl(path.join(CACHE_DIR, "entities.adr.jsonl")));
|
|
440
627
|
const modules = parseModuleEntities(readJsonl(path.join(CACHE_DIR, "entities.module.jsonl")));
|
|
@@ -453,11 +640,7 @@ async function main(): Promise<void> {
|
|
|
453
640
|
uniqueSignatures.add(entity.signature);
|
|
454
641
|
}
|
|
455
642
|
const uniqueTextCount = uniqueSignatures.size;
|
|
456
|
-
const tokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
457
643
|
uniqueSignatures.clear();
|
|
458
|
-
const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
|
|
459
|
-
|
|
460
|
-
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
461
644
|
|
|
462
645
|
env.cacheDir = MODEL_CACHE_DIR;
|
|
463
646
|
// Total thread budget for embedding. CORTEX_EMBED_THREADS caps it so
|
|
@@ -466,6 +649,32 @@ async function main(): Promise<void> {
|
|
|
466
649
|
const threadsRaw = Number(process.env.CORTEX_EMBED_THREADS);
|
|
467
650
|
const threadBudget =
|
|
468
651
|
Number.isFinite(threadsRaw) && threadsRaw >= 1 ? Math.floor(threadsRaw) : os.cpus().length;
|
|
652
|
+
const readHeadroom = () =>
|
|
653
|
+
resolveMemoryHeadroom({
|
|
654
|
+
freeMemory: os.freemem(),
|
|
655
|
+
totalMemory: os.totalmem(),
|
|
656
|
+
constrainedMemory: process.constrainedMemory?.() ?? null,
|
|
657
|
+
availableMemory: process.availableMemory?.() ?? null
|
|
658
|
+
});
|
|
659
|
+
const memoryHeadroom = readHeadroom();
|
|
660
|
+
const previewPoolConfig = resolvePoolConfig({
|
|
661
|
+
threadBudget,
|
|
662
|
+
uniqueCount: uniqueTextCount,
|
|
663
|
+
memoryBytes: memoryHeadroom
|
|
664
|
+
});
|
|
665
|
+
const requestedTokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
666
|
+
const tokenBudget = resolveEffectiveTokenBudget({
|
|
667
|
+
choice: requestedTokenBudget,
|
|
668
|
+
modelMaxTokens: resolveModelMaxTokens(undefined, requestedTokenBudget.cap ?? undefined),
|
|
669
|
+
memoryBytes: memoryHeadroom,
|
|
670
|
+
sessions: previewPoolConfig.sessions
|
|
671
|
+
});
|
|
672
|
+
const defaultSignatureProfile = resolveSignatureProfile(tokenBudget.cap, "full");
|
|
673
|
+
const fileSignatureProfile = resolveSignatureProfile(tokenBudget.cap, textProfile, "File");
|
|
674
|
+
const signatureProfileForEntity = (entity: SearchEntity) =>
|
|
675
|
+
entity.type === "File" ? fileSignatureProfile : defaultSignatureProfile;
|
|
676
|
+
|
|
677
|
+
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
469
678
|
|
|
470
679
|
let reused = 0;
|
|
471
680
|
// Slot per entity keeps output in entity order; failed slots stay null.
|
|
@@ -474,6 +683,7 @@ async function main(): Promise<void> {
|
|
|
474
683
|
let dimensions = 0;
|
|
475
684
|
|
|
476
685
|
entities.forEach((entity, index) => {
|
|
686
|
+
const signatureProfile = signatureProfileForEntity(entity);
|
|
477
687
|
const signature = embeddingSignature(entity.signature, signatureProfile);
|
|
478
688
|
const previous = existing.get(entity.id);
|
|
479
689
|
if (previous && previous.signature === signature && previous.vector.length > 0) {
|
|
@@ -505,14 +715,6 @@ async function main(): Promise<void> {
|
|
|
505
715
|
// adapts to the machine so no tuning is expected from users. Container
|
|
506
716
|
// limits (cgroups) and platforms that under-report free memory are both
|
|
507
717
|
// handled inside resolveMemoryHeadroom.
|
|
508
|
-
const readHeadroom = () =>
|
|
509
|
-
resolveMemoryHeadroom({
|
|
510
|
-
freeMemory: os.freemem(),
|
|
511
|
-
totalMemory: os.totalmem(),
|
|
512
|
-
constrainedMemory: process.constrainedMemory?.() ?? null,
|
|
513
|
-
availableMemory: process.availableMemory?.() ?? null
|
|
514
|
-
});
|
|
515
|
-
const memoryHeadroom = readHeadroom();
|
|
516
718
|
const poolConfig = resolvePoolConfig({
|
|
517
719
|
threadBudget,
|
|
518
720
|
poolOverride: Number(process.env.CORTEX_EMBED_POOL) || null,
|
|
@@ -616,6 +818,7 @@ async function main(): Promise<void> {
|
|
|
616
818
|
maxInFlightTokens,
|
|
617
819
|
onVector(index, rawVector) {
|
|
618
820
|
const entity = entities[index];
|
|
821
|
+
const signatureProfile = signatureProfileForEntity(entity);
|
|
619
822
|
const vector = roundVector(rawVector);
|
|
620
823
|
embedded += 1;
|
|
621
824
|
dimensions = dimensions || vector.length;
|
|
@@ -650,6 +853,13 @@ async function main(): Promise<void> {
|
|
|
650
853
|
mode,
|
|
651
854
|
model: modelId,
|
|
652
855
|
dimensions,
|
|
856
|
+
text_profile: textProfile,
|
|
857
|
+
signature_profile: fileSignatureProfile === defaultSignatureProfile ? defaultSignatureProfile : "per_entity",
|
|
858
|
+
signature_profiles: {
|
|
859
|
+
default: defaultSignatureProfile,
|
|
860
|
+
file: fileSignatureProfile
|
|
861
|
+
},
|
|
862
|
+
text_profile_stats: textProfileStats,
|
|
653
863
|
counts: {
|
|
654
864
|
entities: entities.length,
|
|
655
865
|
output: outputCount,
|
|
@@ -663,7 +873,10 @@ async function main(): Promise<void> {
|
|
|
663
873
|
fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
664
874
|
|
|
665
875
|
console.log(
|
|
666
|
-
`[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems} max_tokens<=${modelMaxTokensUsed || tokenBudget.cap || "model"} token_budget=${tokenBudget.mode}`
|
|
876
|
+
`[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems} max_tokens<=${modelMaxTokensUsed || tokenBudget.cap || "model"} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason} text_profile=${textProfile}`
|
|
877
|
+
);
|
|
878
|
+
console.log(
|
|
879
|
+
`[embed] text_profile=${textProfile} compacted_files=${textProfileStats.compacted_files}/${textProfileStats.file_entities} saved_chars=${textProfileStats.saved_chars}`
|
|
667
880
|
);
|
|
668
881
|
console.log(
|
|
669
882
|
`[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`
|
|
@@ -195,7 +195,7 @@ export function resolveModelMaxTokens(raw: unknown, overrideRaw?: unknown): numb
|
|
|
195
195
|
|
|
196
196
|
export type TokenBudgetChoice = {
|
|
197
197
|
cap: number | null;
|
|
198
|
-
mode: "auto" | "explicit" | "model";
|
|
198
|
+
mode: "auto" | "auto_degraded" | "explicit" | "model";
|
|
199
199
|
reason: string;
|
|
200
200
|
};
|
|
201
201
|
|
|
@@ -225,6 +225,74 @@ export function resolveTokenBudgetChoice(overrideRaw: unknown, uniqueCount: numb
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
const AUTO_TOKEN_BUDGET_CANDIDATES = [8192, 4096, 2048];
|
|
229
|
+
const AUTO_TOKEN_BUDGET_MIN = 2048;
|
|
230
|
+
const AUTO_TOKEN_BUDGET_SESSION_BYTES = 2.4e9;
|
|
231
|
+
const AUTO_TOKEN_BUDGET_ACTIVATION_BYTES_AT_4096 = 3e9;
|
|
232
|
+
const AUTO_TOKEN_BUDGET_MEMORY_MARGIN = 0.95;
|
|
233
|
+
|
|
234
|
+
function estimateTokenBudgetMemoryBytes(maxTokens: number, sessions: number): number {
|
|
235
|
+
const sessionCount = Number.isFinite(sessions) && sessions >= 1 ? Math.floor(sessions) : 1;
|
|
236
|
+
const tokenCount = Number.isFinite(maxTokens) && maxTokens >= 1 ? Math.floor(maxTokens) : 8192;
|
|
237
|
+
const activationBytes =
|
|
238
|
+
AUTO_TOKEN_BUDGET_ACTIVATION_BYTES_AT_4096 * Math.pow(tokenCount / 4096, 2);
|
|
239
|
+
return sessionCount * AUTO_TOKEN_BUDGET_SESSION_BYTES + activationBytes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Keeps auto quality-first but avoids choosing a token budget that is likely
|
|
244
|
+
* to OOM this process. Explicit numeric caps and explicit full-model modes are
|
|
245
|
+
* operator choices and are never degraded here.
|
|
246
|
+
*/
|
|
247
|
+
export function resolveEffectiveTokenBudget({
|
|
248
|
+
choice,
|
|
249
|
+
modelMaxTokens,
|
|
250
|
+
memoryBytes,
|
|
251
|
+
sessions
|
|
252
|
+
}: {
|
|
253
|
+
choice: TokenBudgetChoice;
|
|
254
|
+
modelMaxTokens: number;
|
|
255
|
+
memoryBytes?: number;
|
|
256
|
+
sessions?: number;
|
|
257
|
+
}): TokenBudgetChoice {
|
|
258
|
+
if (choice.mode !== "auto" || choice.cap !== null) {
|
|
259
|
+
return choice;
|
|
260
|
+
}
|
|
261
|
+
const modelMax =
|
|
262
|
+
Number.isFinite(modelMaxTokens) && modelMaxTokens >= 1 ? Math.floor(modelMaxTokens) : 8192;
|
|
263
|
+
if (modelMax <= AUTO_TOKEN_BUDGET_MIN) {
|
|
264
|
+
return choice;
|
|
265
|
+
}
|
|
266
|
+
if (!Number.isFinite(memoryBytes) || (memoryBytes as number) <= 0) {
|
|
267
|
+
return choice;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const available = (memoryBytes as number) * AUTO_TOKEN_BUDGET_MEMORY_MARGIN;
|
|
271
|
+
const candidates = AUTO_TOKEN_BUDGET_CANDIDATES
|
|
272
|
+
.filter((candidate) => candidate <= modelMax)
|
|
273
|
+
.sort((a, b) => b - a);
|
|
274
|
+
if (!candidates.includes(AUTO_TOKEN_BUDGET_MIN)) {
|
|
275
|
+
candidates.push(AUTO_TOKEN_BUDGET_MIN);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const selected =
|
|
279
|
+
candidates.find((candidate) => estimateTokenBudgetMemoryBytes(candidate, sessions ?? 1) <= available) ??
|
|
280
|
+
AUTO_TOKEN_BUDGET_MIN;
|
|
281
|
+
|
|
282
|
+
if (selected >= modelMax) {
|
|
283
|
+
return choice;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
cap: selected,
|
|
288
|
+
mode: "auto_degraded",
|
|
289
|
+
reason: `memory_headroom cap=${selected} model_max=${modelMax} sessions=${Math.max(
|
|
290
|
+
1,
|
|
291
|
+
Math.floor(sessions ?? 1)
|
|
292
|
+
)} headroom_mb=${Math.round((memoryBytes as number) / 1024 / 1024)}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
228
296
|
/**
|
|
229
297
|
* Builds a token counter from a tokenizer callable. Falls back to a
|
|
230
298
|
* chars/4 estimate when the tokenizer is unavailable, misbehaves, or
|
|
@@ -21,7 +21,7 @@ import { pushWorkflowSnapshot, setWorkflowPushContext } from "./workflow/push.js
|
|
|
21
21
|
import { hasWorkflowState, loadWorkflowState } from "./workflow/state.js";
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
|
-
const pkg = require("
|
|
24
|
+
const pkg = require("../../package.json") as { version: string };
|
|
25
25
|
|
|
26
26
|
export const name = "cortex-enterprise";
|
|
27
27
|
export const version: string = pkg.version;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function gitLines(projectRoot: string, args: string[]): string[] {
|
|
4
|
+
const output = execFileSync("git", args, {
|
|
5
|
+
cwd: projectRoot,
|
|
6
|
+
encoding: "utf8",
|
|
7
|
+
timeout: 5000,
|
|
8
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9
|
+
});
|
|
10
|
+
return output.split("\n").map((value) => value.trim()).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function trackedChangedFiles(projectRoot: string): string[] {
|
|
14
|
+
try {
|
|
15
|
+
return gitLines(projectRoot, ["diff", "--name-only", "HEAD"]);
|
|
16
|
+
} catch {
|
|
17
|
+
// No HEAD yet (repo without commits): every tracked file is new.
|
|
18
|
+
return [
|
|
19
|
+
...gitLines(projectRoot, ["diff", "--name-only"]),
|
|
20
|
+
...gitLines(projectRoot, ["ls-files"]),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveChangedReviewFiles(projectRoot: string): string[] | null {
|
|
26
|
+
try {
|
|
27
|
+
gitLines(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
|
|
28
|
+
const tracked = trackedChangedFiles(projectRoot);
|
|
29
|
+
const untracked = gitLines(projectRoot, ["ls-files", "--others", "--exclude-standard"]);
|
|
30
|
+
return [...new Set([...tracked, ...untracked])].sort();
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|