@danielblomma/cortex-mcp 2.2.3 → 2.2.4
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
CHANGED
|
@@ -413,12 +413,19 @@ Required npm configuration:
|
|
|
413
413
|
## Embedding performance
|
|
414
414
|
|
|
415
415
|
Embedding generation tunes itself to the machine: the number of parallel
|
|
416
|
-
workers, memory limits for long files, and skip-work caching are
|
|
417
|
-
from the available CPU cores and
|
|
418
|
-
included). No configuration is needed — on a laptop or
|
|
419
|
-
picks safe
|
|
420
|
-
|
|
421
|
-
|
|
416
|
+
workers, memory limits for long files, token budget, and skip-work caching are
|
|
417
|
+
all derived from the available CPU cores, RAM, and repository size at run time
|
|
418
|
+
(container memory limits included). No configuration is needed — on a laptop or
|
|
419
|
+
a CI runner, cortex picks safe settings by itself.
|
|
420
|
+
|
|
421
|
+
The embedding token budget defaults to `auto`, which preserves the embedding
|
|
422
|
+
model's own maximum context. Cortex does not lower the default token budget
|
|
423
|
+
just because a repository is large; lower caps can discard semantically useful
|
|
424
|
+
text and are reserved for explicit benchmark or quality experiments. To force a
|
|
425
|
+
specific capped run, set `CORTEX_EMBED_MAX_TOKENS` to a number such as `2048`;
|
|
426
|
+
set it to `model` or `full` to make the full-model baseline explicit. When
|
|
427
|
+
several cortex instances share one machine, set `CORTEX_EMBED_THREADS` to give
|
|
428
|
+
each its fair share of cores.
|
|
422
429
|
|
|
423
430
|
## Limitations
|
|
424
431
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielblomma/cortex-mcp",
|
|
3
3
|
"mcpName": "io.github.DanielBlomma/cortex",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.4",
|
|
5
5
|
"description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Daniel Blomma",
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
resolveMemoryHeadroom,
|
|
16
16
|
resolveModelMaxTokens,
|
|
17
17
|
resolvePoolConfig,
|
|
18
|
+
resolveTokenBudgetChoice,
|
|
18
19
|
runWorkUnits,
|
|
20
|
+
truncateTextToTokenBudget,
|
|
19
21
|
type EmbedExtractor,
|
|
20
22
|
type MeasuredText,
|
|
21
23
|
type PendingText
|
|
@@ -405,6 +407,14 @@ function roundVector(values: number[]): number[] {
|
|
|
405
407
|
return values.map((value) => Number(value.toFixed(6)));
|
|
406
408
|
}
|
|
407
409
|
|
|
410
|
+
function resolveSignatureProfile(maxTokenCap: number | null): string {
|
|
411
|
+
return maxTokenCap ? `embed|max_tokens=${maxTokenCap}` : "";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function embeddingSignature(entitySignature: string, profile: string): string {
|
|
415
|
+
return profile ? hashText(`${profile}|${entitySignature}`) : entitySignature;
|
|
416
|
+
}
|
|
417
|
+
|
|
408
418
|
function* presentEmbeddingRecords(
|
|
409
419
|
slots: Array<EmbeddingRecord | null>
|
|
410
420
|
): Generator<EmbeddingRecord> {
|
|
@@ -438,6 +448,14 @@ async function main(): Promise<void> {
|
|
|
438
448
|
const chunks = parseChunkEntities(readJsonl(path.join(CACHE_DIR, "entities.chunk.jsonl")), filePathById);
|
|
439
449
|
|
|
440
450
|
const entities: SearchEntity[] = [...documents, ...rules, ...adrs, ...modules, ...projects, ...chunks].sort((a, b) => a.id.localeCompare(b.id));
|
|
451
|
+
const uniqueSignatures = new Set<string>();
|
|
452
|
+
for (const entity of entities) {
|
|
453
|
+
uniqueSignatures.add(entity.signature);
|
|
454
|
+
}
|
|
455
|
+
const uniqueTextCount = uniqueSignatures.size;
|
|
456
|
+
const tokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
457
|
+
uniqueSignatures.clear();
|
|
458
|
+
const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
|
|
441
459
|
|
|
442
460
|
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
443
461
|
|
|
@@ -456,8 +474,9 @@ async function main(): Promise<void> {
|
|
|
456
474
|
let dimensions = 0;
|
|
457
475
|
|
|
458
476
|
entities.forEach((entity, index) => {
|
|
477
|
+
const signature = embeddingSignature(entity.signature, signatureProfile);
|
|
459
478
|
const previous = existing.get(entity.id);
|
|
460
|
-
if (previous && previous.signature ===
|
|
479
|
+
if (previous && previous.signature === signature && previous.vector.length > 0) {
|
|
461
480
|
reused += 1;
|
|
462
481
|
dimensions = dimensions || previous.vector.length;
|
|
463
482
|
slots[index] = {
|
|
@@ -470,7 +489,7 @@ async function main(): Promise<void> {
|
|
|
470
489
|
source_of_truth: entity.source_of_truth,
|
|
471
490
|
trust_level: entity.trust_level,
|
|
472
491
|
updated_at: entity.updated_at,
|
|
473
|
-
signature
|
|
492
|
+
signature,
|
|
474
493
|
model: modelId,
|
|
475
494
|
dimensions: previous.vector.length
|
|
476
495
|
};
|
|
@@ -522,6 +541,7 @@ async function main(): Promise<void> {
|
|
|
522
541
|
// Fully warm cache: nothing to embed, so skip model loading entirely —
|
|
523
542
|
// this is the common repeat-bootstrap / small-update path.
|
|
524
543
|
let embedded = 0;
|
|
544
|
+
let modelMaxTokensUsed = 0;
|
|
525
545
|
let result: { vectors: Map<number, number[]>; failures: Array<{ index: number; message: string }> } = {
|
|
526
546
|
vectors: new Map(),
|
|
527
547
|
failures: []
|
|
@@ -569,11 +589,22 @@ async function main(): Promise<void> {
|
|
|
569
589
|
|
|
570
590
|
// Inference truncates at the model max; token counts must too, or one
|
|
571
591
|
// giant file inflates scheduling cost and gate mass far beyond reality.
|
|
572
|
-
const modelMaxTokens = resolveModelMaxTokens(
|
|
592
|
+
const modelMaxTokens = resolveModelMaxTokens(
|
|
593
|
+
first.tokenizer?.model_max_length,
|
|
594
|
+
tokenBudget.cap ?? undefined
|
|
595
|
+
);
|
|
596
|
+
modelMaxTokensUsed = modelMaxTokens;
|
|
573
597
|
const countTokens = createTokenCounter(first.tokenizer, modelMaxTokens);
|
|
598
|
+
const countRawTokens = createTokenCounter(first.tokenizer, 131072);
|
|
574
599
|
|
|
575
|
-
const measured: MeasuredText[] = unique.map((item) =>
|
|
600
|
+
const measured: MeasuredText[] = unique.map((item) => {
|
|
601
|
+
const text = truncateTextToTokenBudget(item.text, countRawTokens, modelMaxTokens);
|
|
602
|
+
return { ...item, text, tokens: countTokens(text) };
|
|
603
|
+
});
|
|
576
604
|
const units = packWorkUnits(measured, schedulerOptions);
|
|
605
|
+
console.log(
|
|
606
|
+
`[embed] scheduler unique=${unique.length} units=${units.length} max_tokens<=${modelMaxTokens} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason}`
|
|
607
|
+
);
|
|
577
608
|
// Recompute headroom after the model copies are resident so the gate
|
|
578
609
|
// reflects what is actually left for inference activations.
|
|
579
610
|
const inFlightRaw = Number(process.env.CORTEX_EMBED_INFLIGHT_TOKENS);
|
|
@@ -598,7 +629,7 @@ async function main(): Promise<void> {
|
|
|
598
629
|
source_of_truth: entity.source_of_truth,
|
|
599
630
|
trust_level: entity.trust_level,
|
|
600
631
|
updated_at: entity.updated_at,
|
|
601
|
-
signature: entity.signature,
|
|
632
|
+
signature: embeddingSignature(entity.signature, signatureProfile),
|
|
602
633
|
model: modelId,
|
|
603
634
|
dimensions: vector.length,
|
|
604
635
|
vector
|
|
@@ -632,7 +663,7 @@ async function main(): Promise<void> {
|
|
|
632
663
|
fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
633
664
|
|
|
634
665
|
console.log(
|
|
635
|
-
`[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems}`
|
|
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}`
|
|
636
667
|
);
|
|
637
668
|
console.log(
|
|
638
669
|
`[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`
|
|
@@ -183,9 +183,46 @@ export function resolveMemoryHeadroom({
|
|
|
183
183
|
/** Sanitizes a tokenizer-reported maximum sequence length. Sentinel and
|
|
184
184
|
* absurd values (some configs report 1e30) fall back to 8192; anything
|
|
185
185
|
* beyond 128k tokens is treated as absurd. */
|
|
186
|
-
export function resolveModelMaxTokens(raw: unknown): number {
|
|
186
|
+
export function resolveModelMaxTokens(raw: unknown, overrideRaw?: unknown): number {
|
|
187
187
|
const value = Number(raw);
|
|
188
|
-
|
|
188
|
+
const modelMax = Number.isFinite(value) && value >= 1 && value <= 131072 ? Math.floor(value) : 8192;
|
|
189
|
+
const override = Number(overrideRaw);
|
|
190
|
+
if (Number.isFinite(override) && override >= 16 && override <= 131072) {
|
|
191
|
+
return Math.min(modelMax, Math.floor(override));
|
|
192
|
+
}
|
|
193
|
+
return modelMax;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type TokenBudgetChoice = {
|
|
197
|
+
cap: number | null;
|
|
198
|
+
mode: "auto" | "explicit" | "model";
|
|
199
|
+
reason: string;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Chooses the requested embedding token budget before model load, so cache
|
|
204
|
+
* signatures can include the same cap that inference will later enforce.
|
|
205
|
+
* Auto mode is quality-preserving: it uses the model's own maximum by default
|
|
206
|
+
* and only truncates when the user explicitly requests a numeric cap.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveTokenBudgetChoice(overrideRaw: unknown, uniqueCount: number): TokenBudgetChoice {
|
|
209
|
+
const raw = String(overrideRaw ?? "").trim().toLowerCase();
|
|
210
|
+
const override = Number(raw);
|
|
211
|
+
if (Number.isFinite(override) && override >= 16 && override <= 131072) {
|
|
212
|
+
const cap = Math.floor(override);
|
|
213
|
+
return { cap, mode: "explicit", reason: "env_override" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (raw === "model" || raw === "none" || raw === "off" || raw === "full") {
|
|
217
|
+
return { cap: null, mode: "model", reason: "env_full_model" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const count = Number.isFinite(uniqueCount) && uniqueCount > 0 ? Math.floor(uniqueCount) : 0;
|
|
221
|
+
return {
|
|
222
|
+
cap: null,
|
|
223
|
+
mode: "auto",
|
|
224
|
+
reason: `quality_preserving_model_max:unique=${count}`
|
|
225
|
+
};
|
|
189
226
|
}
|
|
190
227
|
|
|
191
228
|
/**
|
|
@@ -216,6 +253,29 @@ export function createTokenCounter(
|
|
|
216
253
|
};
|
|
217
254
|
}
|
|
218
255
|
|
|
256
|
+
export function truncateTextToTokenBudget(
|
|
257
|
+
text: string,
|
|
258
|
+
countTokens: (text: string) => number,
|
|
259
|
+
maxTokens: number
|
|
260
|
+
): string {
|
|
261
|
+
if (!Number.isFinite(maxTokens) || maxTokens < 1 || countTokens(text) <= maxTokens) {
|
|
262
|
+
return text;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let low = 0;
|
|
266
|
+
let high = text.length;
|
|
267
|
+
while (low < high) {
|
|
268
|
+
const mid = Math.ceil((low + high) / 2);
|
|
269
|
+
if (countTokens(text.slice(0, mid)) <= maxTokens) {
|
|
270
|
+
low = mid;
|
|
271
|
+
} else {
|
|
272
|
+
high = mid - 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return text.slice(0, low).trimEnd();
|
|
277
|
+
}
|
|
278
|
+
|
|
219
279
|
/** Estimated working memory one full pool session needs (model copy plus
|
|
220
280
|
* inference activations for a base-size model). Used only as a derating
|
|
221
281
|
* heuristic; deliberately conservative. */
|
|
@@ -448,6 +448,48 @@ test("resolveModelMaxTokens: sane values pass, sentinels and absurdities fall ba
|
|
|
448
448
|
assert.equal(resolveModelMaxTokens(0), 8192);
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
+
test("resolveModelMaxTokens: explicit override caps but never raises model max", async () => {
|
|
452
|
+
const { resolveModelMaxTokens } = await import("../dist/embedScheduler.js");
|
|
453
|
+
assert.equal(resolveModelMaxTokens(8192, 2048), 2048);
|
|
454
|
+
assert.equal(resolveModelMaxTokens(512, 2048), 512);
|
|
455
|
+
assert.equal(resolveModelMaxTokens(8192, "1024"), 1024);
|
|
456
|
+
assert.equal(resolveModelMaxTokens(8192, 4), 8192);
|
|
457
|
+
assert.equal(resolveModelMaxTokens(8192, "nope"), 8192);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("resolveTokenBudgetChoice: auto preserves model max regardless of repo size", async () => {
|
|
461
|
+
const { resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
462
|
+
assert.deepEqual(resolveTokenBudgetChoice(undefined, 180), {
|
|
463
|
+
cap: null,
|
|
464
|
+
mode: "auto",
|
|
465
|
+
reason: "quality_preserving_model_max:unique=180"
|
|
466
|
+
});
|
|
467
|
+
assert.deepEqual(resolveTokenBudgetChoice("auto", 20000), {
|
|
468
|
+
cap: null,
|
|
469
|
+
mode: "auto",
|
|
470
|
+
reason: "quality_preserving_model_max:unique=20000"
|
|
471
|
+
});
|
|
472
|
+
assert.deepEqual(resolveTokenBudgetChoice(undefined, 48533), {
|
|
473
|
+
cap: null,
|
|
474
|
+
mode: "auto",
|
|
475
|
+
reason: "quality_preserving_model_max:unique=48533"
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("resolveTokenBudgetChoice: explicit caps and full-model opt-out win over auto", async () => {
|
|
480
|
+
const { resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
481
|
+
assert.deepEqual(resolveTokenBudgetChoice("4096", 48533), {
|
|
482
|
+
cap: 4096,
|
|
483
|
+
mode: "explicit",
|
|
484
|
+
reason: "env_override"
|
|
485
|
+
});
|
|
486
|
+
assert.deepEqual(resolveTokenBudgetChoice("model", 48533), {
|
|
487
|
+
cap: null,
|
|
488
|
+
mode: "model",
|
|
489
|
+
reason: "env_full_model"
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
451
493
|
test("createTokenCounter: uses the tokenizer, clamps at model max, survives failures", async () => {
|
|
452
494
|
const { createTokenCounter } = await import("../dist/embedScheduler.js");
|
|
453
495
|
const tokenizer = (text) => ({ input_ids: { dims: [1, text.length * 2] } });
|
|
@@ -462,6 +504,13 @@ test("createTokenCounter: uses the tokenizer, clamps at model max, survives fail
|
|
|
462
504
|
assert.equal(missing("x".repeat(40)), 10);
|
|
463
505
|
});
|
|
464
506
|
|
|
507
|
+
test("truncateTextToTokenBudget: trims a prefix to the requested token cap", async () => {
|
|
508
|
+
const { truncateTextToTokenBudget } = await import("../dist/embedScheduler.js");
|
|
509
|
+
const countTokens = (text) => text.length;
|
|
510
|
+
assert.equal(truncateTextToTokenBudget("abcdef", countTokens, 3), "abc");
|
|
511
|
+
assert.equal(truncateTextToTokenBudget("abc", countTokens, 3), "abc");
|
|
512
|
+
});
|
|
513
|
+
|
|
465
514
|
test("resolveMemoryHeadroom: constrainedMemory 0 means unconstrained (Node sentinel)", async () => {
|
|
466
515
|
const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
|
|
467
516
|
const unconstrained = resolveMemoryHeadroom({ freeMemory: 50e9, totalMemory: 64e9 });
|