@danielblomma/cortex-mcp 2.2.4 → 2.2.5
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
|
@@ -418,14 +418,15 @@ all derived from the available CPU cores, RAM, and repository size at run time
|
|
|
418
418
|
(container memory limits included). No configuration is needed — on a laptop or
|
|
419
419
|
a CI runner, cortex picks safe settings by itself.
|
|
420
420
|
|
|
421
|
-
The embedding token budget defaults to `auto`, which
|
|
422
|
-
model's own maximum context
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
The embedding token budget defaults to `auto`, which is quality-first but
|
|
422
|
+
memory-aware. Cortex starts from the embedding model's own maximum context and
|
|
423
|
+
only lowers the cap when the local memory headroom is unlikely to fit that
|
|
424
|
+
model/context combination. Degraded auto runs are explicit in logs, for example
|
|
425
|
+
`token_budget=auto_degraded reason=memory_headroom cap=2048`. To force a
|
|
425
426
|
specific capped run, set `CORTEX_EMBED_MAX_TOKENS` to a number such as `2048`;
|
|
426
|
-
set it to `model` or `full` to
|
|
427
|
-
several cortex instances share one machine, set
|
|
428
|
-
each its fair share of cores.
|
|
427
|
+
set it to `model` or `full` to force the full-model baseline even on tight
|
|
428
|
+
machines. When several cortex instances share one machine, set
|
|
429
|
+
`CORTEX_EMBED_THREADS` to give each its fair share of cores.
|
|
429
430
|
|
|
430
431
|
## Limitations
|
|
431
432
|
|
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.5",
|
|
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",
|
|
@@ -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,
|
|
@@ -453,11 +454,7 @@ async function main(): Promise<void> {
|
|
|
453
454
|
uniqueSignatures.add(entity.signature);
|
|
454
455
|
}
|
|
455
456
|
const uniqueTextCount = uniqueSignatures.size;
|
|
456
|
-
const tokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
457
457
|
uniqueSignatures.clear();
|
|
458
|
-
const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
|
|
459
|
-
|
|
460
|
-
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
461
458
|
|
|
462
459
|
env.cacheDir = MODEL_CACHE_DIR;
|
|
463
460
|
// Total thread budget for embedding. CORTEX_EMBED_THREADS caps it so
|
|
@@ -466,6 +463,29 @@ async function main(): Promise<void> {
|
|
|
466
463
|
const threadsRaw = Number(process.env.CORTEX_EMBED_THREADS);
|
|
467
464
|
const threadBudget =
|
|
468
465
|
Number.isFinite(threadsRaw) && threadsRaw >= 1 ? Math.floor(threadsRaw) : os.cpus().length;
|
|
466
|
+
const readHeadroom = () =>
|
|
467
|
+
resolveMemoryHeadroom({
|
|
468
|
+
freeMemory: os.freemem(),
|
|
469
|
+
totalMemory: os.totalmem(),
|
|
470
|
+
constrainedMemory: process.constrainedMemory?.() ?? null,
|
|
471
|
+
availableMemory: process.availableMemory?.() ?? null
|
|
472
|
+
});
|
|
473
|
+
const memoryHeadroom = readHeadroom();
|
|
474
|
+
const previewPoolConfig = resolvePoolConfig({
|
|
475
|
+
threadBudget,
|
|
476
|
+
uniqueCount: uniqueTextCount,
|
|
477
|
+
memoryBytes: memoryHeadroom
|
|
478
|
+
});
|
|
479
|
+
const requestedTokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
480
|
+
const tokenBudget = resolveEffectiveTokenBudget({
|
|
481
|
+
choice: requestedTokenBudget,
|
|
482
|
+
modelMaxTokens: resolveModelMaxTokens(undefined, requestedTokenBudget.cap ?? undefined),
|
|
483
|
+
memoryBytes: memoryHeadroom,
|
|
484
|
+
sessions: previewPoolConfig.sessions
|
|
485
|
+
});
|
|
486
|
+
const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
|
|
487
|
+
|
|
488
|
+
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
469
489
|
|
|
470
490
|
let reused = 0;
|
|
471
491
|
// Slot per entity keeps output in entity order; failed slots stay null.
|
|
@@ -505,14 +525,6 @@ async function main(): Promise<void> {
|
|
|
505
525
|
// adapts to the machine so no tuning is expected from users. Container
|
|
506
526
|
// limits (cgroups) and platforms that under-report free memory are both
|
|
507
527
|
// 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
528
|
const poolConfig = resolvePoolConfig({
|
|
517
529
|
threadBudget,
|
|
518
530
|
poolOverride: Number(process.env.CORTEX_EMBED_POOL) || null,
|
|
@@ -663,7 +675,7 @@ async function main(): Promise<void> {
|
|
|
663
675
|
fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
664
676
|
|
|
665
677
|
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}`
|
|
678
|
+
`[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}`
|
|
667
679
|
);
|
|
668
680
|
console.log(
|
|
669
681
|
`[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
|
|
@@ -490,6 +490,58 @@ test("resolveTokenBudgetChoice: explicit caps and full-model opt-out win over au
|
|
|
490
490
|
});
|
|
491
491
|
});
|
|
492
492
|
|
|
493
|
+
test("resolveEffectiveTokenBudget: auto degrades when model max is too expensive for headroom", async () => {
|
|
494
|
+
const { resolveEffectiveTokenBudget, resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
495
|
+
const choice = resolveTokenBudgetChoice(undefined, 2237);
|
|
496
|
+
const degraded = resolveEffectiveTokenBudget({
|
|
497
|
+
choice,
|
|
498
|
+
modelMaxTokens: 8192,
|
|
499
|
+
memoryBytes: 7e9,
|
|
500
|
+
sessions: 2
|
|
501
|
+
});
|
|
502
|
+
assert.equal(degraded.mode, "auto_degraded");
|
|
503
|
+
assert.equal(degraded.cap, 2048);
|
|
504
|
+
assert.match(degraded.reason, /memory_headroom/);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("resolveEffectiveTokenBudget: auto keeps model max when headroom is sufficient", async () => {
|
|
508
|
+
const { resolveEffectiveTokenBudget, resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
509
|
+
const choice = resolveTokenBudgetChoice("auto", 2237);
|
|
510
|
+
assert.deepEqual(
|
|
511
|
+
resolveEffectiveTokenBudget({
|
|
512
|
+
choice,
|
|
513
|
+
modelMaxTokens: 8192,
|
|
514
|
+
memoryBytes: 64e9,
|
|
515
|
+
sessions: 2
|
|
516
|
+
}),
|
|
517
|
+
choice
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("resolveEffectiveTokenBudget: explicit caps and full-model opt-out are never degraded", async () => {
|
|
522
|
+
const { resolveEffectiveTokenBudget, resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
523
|
+
const explicit = resolveTokenBudgetChoice("4096", 2237);
|
|
524
|
+
assert.deepEqual(
|
|
525
|
+
resolveEffectiveTokenBudget({
|
|
526
|
+
choice: explicit,
|
|
527
|
+
modelMaxTokens: 8192,
|
|
528
|
+
memoryBytes: 1e9,
|
|
529
|
+
sessions: 4
|
|
530
|
+
}),
|
|
531
|
+
explicit
|
|
532
|
+
);
|
|
533
|
+
const full = resolveTokenBudgetChoice("full", 2237);
|
|
534
|
+
assert.deepEqual(
|
|
535
|
+
resolveEffectiveTokenBudget({
|
|
536
|
+
choice: full,
|
|
537
|
+
modelMaxTokens: 8192,
|
|
538
|
+
memoryBytes: 1e9,
|
|
539
|
+
sessions: 4
|
|
540
|
+
}),
|
|
541
|
+
full
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
493
545
|
test("createTokenCounter: uses the tokenizer, clamps at model max, survives failures", async () => {
|
|
494
546
|
const { createTokenCounter } = await import("../dist/embedScheduler.js");
|
|
495
547
|
const tokenizer = (text) => ({ input_ids: { dims: [1, text.length * 2] } });
|