@danielblomma/cortex-mcp 2.1.1 → 2.2.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.
Files changed (150) hide show
  1. package/README.md +72 -24
  2. package/bin/cortex.mjs +50 -21
  3. package/mcp-registry-submission.json +63 -0
  4. package/package.json +19 -5
  5. package/scaffold/AGENTS.md +13 -9
  6. package/scaffold/CLAUDE.md +13 -9
  7. package/scaffold/docs/architecture.md +1 -1
  8. package/scaffold/mcp/build.mjs +92 -0
  9. package/scaffold/mcp/package.json +4 -4
  10. package/scaffold/mcp/src/cli/query.ts +455 -0
  11. package/scaffold/mcp/src/embed.ts +181 -77
  12. package/scaffold/mcp/src/embedScheduler.ts +509 -0
  13. package/scaffold/mcp/src/embeddings.ts +31 -30
  14. package/scaffold/mcp/src/graphCsv.ts +65 -0
  15. package/scaffold/mcp/src/jsonl.ts +69 -14
  16. package/scaffold/mcp/src/loadGraph.ts +276 -40
  17. package/scaffold/mcp/src/lruCache.ts +41 -0
  18. package/scaffold/mcp/src/searchCore.ts +1 -1
  19. package/scaffold/mcp/src/searchResults.ts +3 -3
  20. package/scaffold/mcp/src/types.ts +6 -1
  21. package/scaffold/mcp/tests/embed-scheduler.test.mjs +474 -0
  22. package/scaffold/mcp/tests/embedding-jsonl.test.mjs +39 -0
  23. package/scaffold/mcp/tests/graph-bulk-load.test.mjs +258 -0
  24. package/scaffold/mcp/tests/graph-csv.test.mjs +118 -0
  25. package/scaffold/mcp/tests/lru-cache.test.mjs +37 -0
  26. package/scaffold/mcp/tests/query-cli.test.mjs +115 -0
  27. package/scaffold/mcp/tests/vector-index.test.mjs +109 -0
  28. package/scaffold/mcp/tsconfig.json +3 -1
  29. package/scaffold/scripts/bootstrap.sh +37 -4
  30. package/scaffold/scripts/dashboard.mjs +1 -1
  31. package/scaffold/scripts/doctor.sh +9 -8
  32. package/scaffold/scripts/embed.sh +3 -2
  33. package/scaffold/scripts/ingest-parsers.mjs +387 -0
  34. package/scaffold/scripts/ingest-worker.mjs +52 -0
  35. package/scaffold/scripts/ingest.mjs +550 -378
  36. package/scaffold/scripts/load-ryu.sh +3 -2
  37. package/scaffold/scripts/memory-compile.mjs +5 -2
  38. package/scaffold/scripts/memory-lint.mjs +5 -2
  39. package/scaffold/scripts/parsers/csharp.mjs +2 -1
  40. package/scaffold/scripts/parsers/javascript/ast.mjs +160 -8
  41. package/scaffold/scripts/parsers/javascript/chunks.mjs +129 -22
  42. package/scaffold/scripts/parsers/javascript/imports.mjs +38 -4
  43. package/scaffold/scripts/parsers/vbnet.mjs +2 -1
  44. package/scaffold/scripts/status.sh +50 -14
  45. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -56
  46. package/scaffold/scripts/parsers/node_modules/acorn/CHANGELOG.md +0 -972
  47. package/scaffold/scripts/parsers/node_modules/acorn/LICENSE +0 -21
  48. package/scaffold/scripts/parsers/node_modules/acorn/README.md +0 -301
  49. package/scaffold/scripts/parsers/node_modules/acorn/bin/acorn +0 -4
  50. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.mts +0 -883
  51. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.ts +0 -883
  52. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.js +0 -6295
  53. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.mjs +0 -6266
  54. package/scaffold/scripts/parsers/node_modules/acorn/dist/bin.js +0 -90
  55. package/scaffold/scripts/parsers/node_modules/acorn/package.json +0 -50
  56. package/scaffold/scripts/parsers/node_modules/acorn-typescript/CHANGELOG.md +0 -421
  57. package/scaffold/scripts/parsers/node_modules/acorn-typescript/LICENSE +0 -21
  58. package/scaffold/scripts/parsers/node_modules/acorn-typescript/README.md +0 -81
  59. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.d.ts +0 -103
  60. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js +0 -78
  61. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js.map +0 -1
  62. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.d.ts +0 -167
  63. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js +0 -75
  64. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js.map +0 -1
  65. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.d.ts +0 -177
  66. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js +0 -56
  67. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js.map +0 -1
  68. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.d.ts +0 -198
  69. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js +0 -327
  70. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js.map +0 -1
  71. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.d.ts +0 -256
  72. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js +0 -256
  73. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js.map +0 -1
  74. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.d.ts +0 -472
  75. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js +0 -1
  76. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js.map +0 -1
  77. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.mjs +0 -1
  78. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.d.ts +0 -159
  79. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js +0 -2
  80. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js.map +0 -1
  81. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.d.ts +0 -10
  82. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js +0 -38
  83. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js.map +0 -1
  84. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.d.ts +0 -12
  85. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js +0 -29
  86. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js.map +0 -1
  87. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.d.ts +0 -2
  88. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js +0 -118
  89. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js.map +0 -1
  90. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.d.ts +0 -60
  91. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js +0 -2
  92. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js.map +0 -1
  93. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.d.ts +0 -2
  94. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js +0 -19
  95. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js.map +0 -1
  96. package/scaffold/scripts/parsers/node_modules/acorn-typescript/package.json +0 -53
  97. package/scaffold/scripts/parsers/node_modules/acorn-typescript/tsconfig.json +0 -19
  98. package/scaffold/scripts/parsers/node_modules/acorn-walk/CHANGELOG.md +0 -209
  99. package/scaffold/scripts/parsers/node_modules/acorn-walk/LICENSE +0 -21
  100. package/scaffold/scripts/parsers/node_modules/acorn-walk/README.md +0 -124
  101. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.mts +0 -152
  102. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.ts +0 -152
  103. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.js +0 -485
  104. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.mjs +0 -467
  105. package/scaffold/scripts/parsers/node_modules/acorn-walk/package.json +0 -50
  106. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/LICENSE +0 -24
  107. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/README.md +0 -23
  108. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-bash.wasm +0 -0
  109. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c.wasm +0 -0
  110. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
  111. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-cpp.wasm +0 -0
  112. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-css.wasm +0 -0
  113. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-dart.wasm +0 -0
  114. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elisp.wasm +0 -0
  115. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elixir.wasm +0 -0
  116. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elm.wasm +0 -0
  117. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-embedded_template.wasm +0 -0
  118. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
  119. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-html.wasm +0 -0
  120. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
  121. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
  122. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-json.wasm +0 -0
  123. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
  124. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-lua.wasm +0 -0
  125. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-objc.wasm +0 -0
  126. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ocaml.wasm +0 -0
  127. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
  128. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
  129. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ql.wasm +0 -0
  130. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rescript.wasm +0 -0
  131. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
  132. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
  133. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
  134. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-solidity.wasm +0 -0
  135. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-swift.wasm +0 -0
  136. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-systemrdl.wasm +0 -0
  137. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tlaplus.wasm +0 -0
  138. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-toml.wasm +0 -0
  139. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
  140. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
  141. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-vue.wasm +0 -0
  142. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-yaml.wasm +0 -0
  143. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-zig.wasm +0 -0
  144. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/package.json +0 -64
  145. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/LICENSE +0 -21
  146. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/README.md +0 -198
  147. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/package.json +0 -37
  148. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter-web.d.ts +0 -242
  149. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.js +0 -1
  150. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
@@ -1,10 +1,25 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import { env, pipeline } from "@huggingface/transformers";
6
- import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
7
+ import { readJsonl, readJsonlRecords, writeJsonlRecords, asString, asNumber, asBoolean } from "./jsonl.js";
7
8
  import { CACHE_DIR, PATHS } from "./paths.js";
9
+ import {
10
+ createTokenCounter,
11
+ DEFAULT_SCHEDULER_OPTIONS,
12
+ groupDuplicates,
13
+ packWorkUnits,
14
+ resolveInFlightTokens,
15
+ resolveMemoryHeadroom,
16
+ resolveModelMaxTokens,
17
+ resolvePoolConfig,
18
+ runWorkUnits,
19
+ type EmbedExtractor,
20
+ type MeasuredText,
21
+ type PendingText
22
+ } from "./embedScheduler.js";
8
23
  import type { JsonObject, JsonValue } from "./types.js";
9
24
 
10
25
  const EMBEDDINGS_PATH = PATHS.embeddingsEntities;
@@ -137,11 +152,6 @@ function normalizeText(value: string): string {
137
152
  return value.replace(/\s+/g, " ").trim();
138
153
  }
139
154
 
140
- function writeJsonl(filePath: string, records: EmbeddingRecord[]): void {
141
- const body = records.map((record) => JSON.stringify(record)).join("\n");
142
- fs.writeFileSync(filePath, body ? `${body}\n` : "", "utf8");
143
- }
144
-
145
155
  function ensureRequiredFiles(): void {
146
156
  const required = [
147
157
  path.join(CACHE_DIR, "documents.jsonl"),
@@ -350,7 +360,7 @@ function parseProjectEntities(raw: JsonObject[]): ProjectEntity[] {
350
360
  .filter((value): value is ProjectEntity => value !== null);
351
361
  }
352
362
 
353
- function parseExistingEmbeddings(raw: JsonObject[], modelId: string): Map<string, EmbeddingRecord> {
363
+ function parseExistingEmbeddings(raw: Iterable<JsonObject>, modelId: string): Map<string, EmbeddingRecord> {
354
364
  const index = new Map<string, EmbeddingRecord>();
355
365
 
356
366
  for (const item of raw) {
@@ -360,9 +370,12 @@ function parseExistingEmbeddings(raw: JsonObject[], modelId: string): Map<string
360
370
  const vectorRaw = item.vector;
361
371
  if (!Array.isArray(vectorRaw)) continue;
362
372
 
363
- const vector = vectorRaw
364
- .map((value) => (typeof value === "number" && Number.isFinite(value) ? value : null))
365
- .filter((value): value is number => value !== null);
373
+ const vector: number[] = [];
374
+ for (const value of vectorRaw) {
375
+ if (typeof value === "number" && Number.isFinite(value)) {
376
+ vector.push(value);
377
+ }
378
+ }
366
379
 
367
380
  if (vector.length === 0) continue;
368
381
  const model = asString(item.model);
@@ -388,23 +401,20 @@ function parseExistingEmbeddings(raw: JsonObject[], modelId: string): Map<string
388
401
  return index;
389
402
  }
390
403
 
391
- function toEmbeddingVector(output: unknown): number[] {
392
- if (!output || typeof output !== "object") {
393
- throw new Error("Invalid embedding output type");
394
- }
395
-
396
- const data = (output as { data?: unknown }).data;
397
- if (!data || typeof (data as ArrayLike<number>).length !== "number") {
398
- throw new Error("Missing embedding data");
399
- }
400
-
401
- return Array.from(data as ArrayLike<number>).map((value) => Number(value));
402
- }
403
-
404
404
  function roundVector(values: number[]): number[] {
405
405
  return values.map((value) => Number(value.toFixed(6)));
406
406
  }
407
407
 
408
+ function* presentEmbeddingRecords(
409
+ slots: Array<EmbeddingRecord | null>
410
+ ): Generator<EmbeddingRecord> {
411
+ for (const record of slots) {
412
+ if (record) {
413
+ yield record;
414
+ }
415
+ }
416
+ }
417
+
408
418
  async function main(): Promise<void> {
409
419
  const { mode } = parseArgs(process.argv);
410
420
  ensureRequiredFiles();
@@ -429,36 +439,28 @@ async function main(): Promise<void> {
429
439
 
430
440
  const entities: SearchEntity[] = [...documents, ...rules, ...adrs, ...modules, ...projects, ...chunks].sort((a, b) => a.id.localeCompare(b.id));
431
441
 
432
- const existing = parseExistingEmbeddings(readJsonl(EMBEDDINGS_PATH), modelId);
442
+ const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
433
443
 
434
444
  env.cacheDir = MODEL_CACHE_DIR;
435
- // Optional intra-op thread cap so co-located embedders (e.g. parallel CI
436
- // jobs or eval containers) do not oversubscribe shared cores. Unset =
437
- // library default (all cores), which is correct for a single instance.
445
+ // Total thread budget for embedding. CORTEX_EMBED_THREADS caps it so
446
+ // co-located embedders (parallel CI jobs, eval containers) do not
447
+ // oversubscribe shared cores; unset = all cores.
438
448
  const threadsRaw = Number(process.env.CORTEX_EMBED_THREADS);
439
- const sessionOptions =
440
- Number.isFinite(threadsRaw) && threadsRaw >= 1
441
- ? { session_options: { intraOpNumThreads: Math.floor(threadsRaw), interOpNumThreads: 1 } }
442
- : undefined;
443
- const extractor = await pipeline(
444
- "feature-extraction",
445
- modelId,
446
- sessionOptions as Parameters<typeof pipeline>[2]
447
- );
449
+ const threadBudget =
450
+ Number.isFinite(threadsRaw) && threadsRaw >= 1 ? Math.floor(threadsRaw) : os.cpus().length;
448
451
 
449
452
  let reused = 0;
450
- let embedded = 0;
451
- let failed = 0;
452
- const failures: string[] = [];
453
- const output: EmbeddingRecord[] = [];
453
+ // Slot per entity keeps output in entity order; failed slots stay null.
454
+ const slots: Array<EmbeddingRecord | null> = entities.map(() => null);
455
+ const pending: PendingText[] = [];
454
456
  let dimensions = 0;
455
457
 
456
- for (const entity of entities) {
458
+ entities.forEach((entity, index) => {
457
459
  const previous = existing.get(entity.id);
458
460
  if (previous && previous.signature === entity.signature && previous.vector.length > 0) {
459
461
  reused += 1;
460
462
  dimensions = dimensions || previous.vector.length;
461
- output.push({
463
+ slots[index] = {
462
464
  ...previous,
463
465
  entity_type: entity.type,
464
466
  kind: entity.kind,
@@ -471,46 +473,146 @@ async function main(): Promise<void> {
471
473
  signature: entity.signature,
472
474
  model: modelId,
473
475
  dimensions: previous.vector.length
474
- });
475
- continue;
476
+ };
477
+ return;
476
478
  }
479
+ pending.push({ index, text: normalizeText(entity.text) });
480
+ });
477
481
 
478
- try {
479
- const embeddingOutput = await extractor(normalizeText(entity.text), {
480
- pooling: "mean",
481
- normalize: true
482
- });
483
- const vector = roundVector(toEmbeddingVector(embeddingOutput));
484
- if (vector.length === 0) {
485
- throw new Error("Empty embedding vector");
486
- }
482
+ // Deduplicate identical texts (lossless: identical input -> identical
483
+ // vector), then measure token lengths for routing and batch packing.
484
+ const unique = groupDuplicates(pending);
485
+ // Memory headroom drives pool size and the concurrency gate; everything
486
+ // adapts to the machine so no tuning is expected from users. Container
487
+ // limits (cgroups) and platforms that under-report free memory are both
488
+ // handled inside resolveMemoryHeadroom.
489
+ const readHeadroom = () =>
490
+ resolveMemoryHeadroom({
491
+ freeMemory: os.freemem(),
492
+ totalMemory: os.totalmem(),
493
+ constrainedMemory: process.constrainedMemory?.() ?? null,
494
+ availableMemory: process.availableMemory?.() ?? null
495
+ });
496
+ const memoryHeadroom = readHeadroom();
497
+ const poolConfig = resolvePoolConfig({
498
+ threadBudget,
499
+ poolOverride: Number(process.env.CORTEX_EMBED_POOL) || null,
500
+ uniqueCount: unique.length,
501
+ memoryBytes: memoryHeadroom
502
+ });
487
503
 
488
- embedded += 1;
489
- dimensions = dimensions || vector.length;
490
- output.push({
491
- id: entity.id,
492
- entity_type: entity.type,
493
- kind: entity.kind,
494
- label: entity.label,
495
- path: entity.path,
496
- status: entity.status,
497
- source_of_truth: entity.source_of_truth,
498
- trust_level: entity.trust_level,
499
- updated_at: entity.updated_at,
500
- signature: entity.signature,
501
- model: modelId,
502
- dimensions: vector.length,
503
- vector
504
- });
505
- } catch (error) {
506
- failed += 1;
507
- failures.push(
508
- `${entity.id}: ${error instanceof Error ? error.message : "embedding generation failed"}`
504
+ const batchSizeRaw = Number(process.env.CORTEX_EMBED_BATCH_SIZE);
505
+ const batchTokensRaw = Number(process.env.CORTEX_EMBED_BATCH_TOKENS);
506
+ const shortTokensRaw = Number(process.env.CORTEX_EMBED_SHORT_TOKENS);
507
+ const schedulerOptions = {
508
+ ...DEFAULT_SCHEDULER_OPTIONS,
509
+ // 0 (or any value below 1) disables micro-batching rather than silently
510
+ // meaning "use the default".
511
+ ...(Number.isFinite(batchSizeRaw) && batchSizeRaw >= 0
512
+ ? { batchMaxItems: Math.max(1, Math.floor(batchSizeRaw)) }
513
+ : {}),
514
+ ...(Number.isFinite(batchTokensRaw) && batchTokensRaw >= 16
515
+ ? { batchTokenBudget: Math.floor(batchTokensRaw) }
516
+ : {}),
517
+ ...(Number.isFinite(shortTokensRaw) && shortTokensRaw >= 1
518
+ ? { shortMaxTokens: Math.floor(shortTokensRaw) }
519
+ : {})
520
+ };
521
+
522
+ // Fully warm cache: nothing to embed, so skip model loading entirely —
523
+ // this is the common repeat-bootstrap / small-update path.
524
+ let embedded = 0;
525
+ let result: { vectors: Map<number, number[]>; failures: Array<{ index: number; message: string }> } = {
526
+ vectors: new Map(),
527
+ failures: []
528
+ };
529
+
530
+ if (unique.length > 0) {
531
+ const makeExtractor = async (threads: number) =>
532
+ (await pipeline("feature-extraction", modelId, {
533
+ session_options: { intraOpNumThreads: threads, interOpNumThreads: 1 }
534
+ } as Parameters<typeof pipeline>[2])) as unknown as EmbedExtractor & {
535
+ tokenizer?: ((text: string) => { input_ids?: { dims?: number[] } }) & {
536
+ model_max_length?: number;
537
+ };
538
+ };
539
+
540
+ // First session loads (and caches) the model; the rest load in parallel.
541
+ // Extra sessions failing (e.g. memory pressure) degrades the pool instead
542
+ // of aborting the run — one session can always finish the work.
543
+ const first = await makeExtractor(poolConfig.threadsPerSession);
544
+ const extraSessions = await Promise.allSettled(
545
+ Array.from({ length: poolConfig.sessions - 1 }, () =>
546
+ makeExtractor(poolConfig.threadsPerSession)
547
+ )
548
+ );
549
+ const extractors: EmbedExtractor[] = [
550
+ first,
551
+ ...extraSessions
552
+ .filter(
553
+ (settled): settled is PromiseFulfilledResult<Awaited<ReturnType<typeof makeExtractor>>> =>
554
+ settled.status === "fulfilled"
555
+ )
556
+ .map((settled) => settled.value)
557
+ ];
558
+ const failedSessions = extraSessions.length + 1 - extractors.length;
559
+ if (failedSessions > 0) {
560
+ const firstFailure = extraSessions.find(
561
+ (settled): settled is PromiseRejectedResult => settled.status === "rejected"
562
+ );
563
+ const reason =
564
+ firstFailure?.reason instanceof Error ? firstFailure.reason.message : String(firstFailure?.reason ?? "unknown");
565
+ console.warn(
566
+ `[embed] ${failedSessions} pool session(s) failed to load (${reason}); continuing with ${extractors.length}`
509
567
  );
510
568
  }
569
+
570
+ // Inference truncates at the model max; token counts must too, or one
571
+ // giant file inflates scheduling cost and gate mass far beyond reality.
572
+ const modelMaxTokens = resolveModelMaxTokens(first.tokenizer?.model_max_length);
573
+ const countTokens = createTokenCounter(first.tokenizer, modelMaxTokens);
574
+
575
+ const measured: MeasuredText[] = unique.map((item) => ({ ...item, tokens: countTokens(item.text) }));
576
+ const units = packWorkUnits(measured, schedulerOptions);
577
+ // Recompute headroom after the model copies are resident so the gate
578
+ // reflects what is actually left for inference activations.
579
+ const inFlightRaw = Number(process.env.CORTEX_EMBED_INFLIGHT_TOKENS);
580
+ const maxInFlightTokens =
581
+ Number.isFinite(inFlightRaw) && inFlightRaw >= 1024
582
+ ? Math.floor(inFlightRaw)
583
+ : resolveInFlightTokens({ memoryBytes: readHeadroom(), modelMaxTokens });
584
+ result = await runWorkUnits(units, extractors, {
585
+ maxInFlightTokens,
586
+ onVector(index, rawVector) {
587
+ const entity = entities[index];
588
+ const vector = roundVector(rawVector);
589
+ embedded += 1;
590
+ dimensions = dimensions || vector.length;
591
+ slots[index] = {
592
+ id: entity.id,
593
+ entity_type: entity.type,
594
+ kind: entity.kind,
595
+ label: entity.label,
596
+ path: entity.path,
597
+ status: entity.status,
598
+ source_of_truth: entity.source_of_truth,
599
+ trust_level: entity.trust_level,
600
+ updated_at: entity.updated_at,
601
+ signature: entity.signature,
602
+ model: modelId,
603
+ dimensions: vector.length,
604
+ vector
605
+ };
606
+ }
607
+ });
511
608
  }
512
609
 
513
- writeJsonl(EMBEDDINGS_PATH, output);
610
+ const failures = result.failures.map(
611
+ (failure) => `${entities[failure.index].id}: ${failure.message}`
612
+ );
613
+ const failed = result.failures.length;
614
+
615
+ const outputCount = writeJsonlRecords(EMBEDDINGS_PATH, presentEmbeddingRecords(slots));
514
616
 
515
617
  const manifest = {
516
618
  generated_at: new Date().toISOString(),
@@ -519,7 +621,7 @@ async function main(): Promise<void> {
519
621
  dimensions,
520
622
  counts: {
521
623
  entities: entities.length,
522
- output: output.length,
624
+ output: outputCount,
523
625
  embedded,
524
626
  reused,
525
627
  failed
@@ -529,7 +631,9 @@ async function main(): Promise<void> {
529
631
 
530
632
  fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
531
633
 
532
- console.log(`[embed] mode=${mode} model=${modelId} dim=${dimensions}`);
634
+ console.log(
635
+ `[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems}`
636
+ );
533
637
  console.log(
534
638
  `[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`
535
639
  );