@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 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
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 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.
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",
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] } });