@danielblomma/cortex-mcp 2.2.2 → 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 all derived
417
- from the available CPU cores and RAM at run time (container memory limits
418
- included). No configuration is needed — on a laptop or a CI runner, cortex
419
- picks safe, fast settings by itself. The one exception worth knowing:
420
- when several cortex instances share one machine, set `CORTEX_EMBED_THREADS`
421
- to give each its fair share of cores.
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.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",
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "scripts": {
66
66
  "pretest": "test -d scaffold/scripts/parsers/node_modules || npm --prefix scaffold/scripts/parsers install --no-fund --no-update-notifier --silent",
67
- "test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/query-cli-shim.test.mjs",
67
+ "test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-run.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/query-cli-shim.test.mjs",
68
68
  "release:sync-version": "node scripts/sync-release-version.mjs",
69
69
  "release:check-version-sync": "node scripts/sync-release-version.mjs --check",
70
70
  "prepublishOnly": "echo 'Ready to publish to npm'"
@@ -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 === entity.signature && previous.vector.length > 0) {
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: entity.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(first.tokenizer?.model_max_length);
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) => ({ ...item, tokens: countTokens(item.text) }));
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
- return Number.isFinite(value) && value >= 1 && value <= 131072 ? Math.floor(value) : 8192;
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. */
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import ryugraph, { type Connection, type PreparedStatement, type QueryResult, type RyuValue } from "ryugraph";
4
- import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
4
+ import { readJsonl, readJsonlRecords, asString, asNumber, asBoolean } from "./jsonl.js";
5
5
  import { CACHE_DIR, CONTEXT_DIR, DB_PATH } from "./paths.js";
6
6
  import { CSV_COPY_OPTIONS, writeCsv, toCopyPathLiteral, type CsvValue } from "./graphCsv.js";
7
7
  import type { JsonObject } from "./types.js";
@@ -127,232 +127,244 @@ function readEntityFile(fileName: string): JsonObject[] {
127
127
  return readJsonl(path.join(CACHE_DIR, fileName));
128
128
  }
129
129
 
130
- function parseFiles(raw: JsonObject[]): FileEntity[] {
131
- return raw
132
- .map((item) => {
133
- const id = asString(item.id);
134
- const filePath = asString(item.path);
135
- if (!id || !filePath) {
136
- return null;
137
- }
130
+ function readEntityRecords(fileName: string): Iterable<JsonObject> {
131
+ return readJsonlRecords(path.join(CACHE_DIR, fileName));
132
+ }
133
+
134
+ function* parseFiles(raw: Iterable<JsonObject>): Generator<FileEntity> {
135
+ for (const item of raw) {
136
+ const id = asString(item.id);
137
+ const filePath = asString(item.path);
138
+ if (!id || !filePath) {
139
+ continue;
140
+ }
138
141
 
139
- return {
140
- id,
141
- path: filePath,
142
- kind: asString(item.kind, "DOC"),
143
- excerpt: asString(item.excerpt),
144
- checksum: asString(item.checksum),
145
- updated_at: asString(item.updated_at),
146
- source_of_truth: asBoolean(item.source_of_truth, false),
147
- trust_level: asNumber(item.trust_level, 50),
148
- status: asString(item.status, "active")
149
- };
150
- })
151
- .filter((value): value is FileEntity => value !== null);
142
+ yield {
143
+ id,
144
+ path: filePath,
145
+ kind: asString(item.kind, "DOC"),
146
+ excerpt: asString(item.excerpt),
147
+ checksum: asString(item.checksum),
148
+ updated_at: asString(item.updated_at),
149
+ source_of_truth: asBoolean(item.source_of_truth, false),
150
+ trust_level: asNumber(item.trust_level, 50),
151
+ status: asString(item.status, "active")
152
+ };
153
+ }
152
154
  }
153
155
 
154
- function parseRules(raw: JsonObject[]): RuleEntity[] {
155
- return raw
156
- .map((item) => {
157
- const id = asString(item.id);
158
- if (!id) {
159
- return null;
160
- }
156
+ function* parseRules(raw: Iterable<JsonObject>): Generator<RuleEntity> {
157
+ for (const item of raw) {
158
+ const id = asString(item.id);
159
+ if (!id) {
160
+ continue;
161
+ }
161
162
 
162
- return {
163
- id,
164
- title: asString(item.title, id),
165
- body: asString(item.body),
166
- scope: asString(item.scope, "global"),
167
- updated_at: asString(item.updated_at),
168
- source_of_truth: asBoolean(item.source_of_truth, true),
169
- trust_level: asNumber(item.trust_level, 95),
170
- status: asString(item.status, "active"),
171
- priority: asNumber(item.priority, 0)
172
- };
173
- })
174
- .filter((value): value is RuleEntity => value !== null);
163
+ yield {
164
+ id,
165
+ title: asString(item.title, id),
166
+ body: asString(item.body),
167
+ scope: asString(item.scope, "global"),
168
+ updated_at: asString(item.updated_at),
169
+ source_of_truth: asBoolean(item.source_of_truth, true),
170
+ trust_level: asNumber(item.trust_level, 95),
171
+ status: asString(item.status, "active"),
172
+ priority: asNumber(item.priority, 0)
173
+ };
174
+ }
175
175
  }
176
176
 
177
- function parseAdrs(raw: JsonObject[]): AdrEntity[] {
178
- return raw
179
- .map((item) => {
180
- const id = asString(item.id);
181
- if (!id) {
182
- return null;
183
- }
177
+ function* parseAdrs(raw: Iterable<JsonObject>): Generator<AdrEntity> {
178
+ for (const item of raw) {
179
+ const id = asString(item.id);
180
+ if (!id) {
181
+ continue;
182
+ }
184
183
 
185
- return {
186
- id,
187
- path: asString(item.path),
188
- title: asString(item.title, id),
189
- body: asString(item.body),
190
- decision_date: asString(item.decision_date),
191
- supersedes_id: asString(item.supersedes_id),
192
- source_of_truth: asBoolean(item.source_of_truth, true),
193
- trust_level: asNumber(item.trust_level, 95),
194
- status: asString(item.status, "active")
195
- };
196
- })
197
- .filter((value): value is AdrEntity => value !== null);
184
+ yield {
185
+ id,
186
+ path: asString(item.path),
187
+ title: asString(item.title, id),
188
+ body: asString(item.body),
189
+ decision_date: asString(item.decision_date),
190
+ supersedes_id: asString(item.supersedes_id),
191
+ source_of_truth: asBoolean(item.source_of_truth, true),
192
+ trust_level: asNumber(item.trust_level, 95),
193
+ status: asString(item.status, "active")
194
+ };
195
+ }
198
196
  }
199
197
 
200
- function parseChunks(raw: JsonObject[]): ChunkEntity[] {
201
- return raw
202
- .map((item) => {
203
- const id = asString(item.id);
204
- const file_id = asString(item.file_id);
205
- const name = asString(item.name);
206
- if (!id || !file_id || !name) {
207
- return null;
208
- }
198
+ function* parseChunks(raw: Iterable<JsonObject>): Generator<ChunkEntity> {
199
+ for (const item of raw) {
200
+ const id = asString(item.id);
201
+ const file_id = asString(item.file_id);
202
+ const name = asString(item.name);
203
+ if (!id || !file_id || !name) {
204
+ continue;
205
+ }
209
206
 
210
- return {
211
- id,
212
- file_id,
213
- name,
214
- kind: asString(item.kind, "function"),
215
- signature: asString(item.signature),
216
- body: asString(item.body),
217
- description: asString(item.description),
218
- start_line: asNumber(item.start_line, 0),
219
- end_line: asNumber(item.end_line, 0),
220
- language: asString(item.language, "javascript"),
221
- exported: asBoolean(item.exported, false),
222
- checksum: asString(item.checksum),
223
- updated_at: asString(item.updated_at),
224
- source_of_truth: asBoolean(item.source_of_truth, true),
225
- trust_level: asNumber(item.trust_level, 80),
226
- status: asString(item.status, "active")
227
- };
228
- })
229
- .filter((value): value is ChunkEntity => value !== null);
207
+ yield {
208
+ id,
209
+ file_id,
210
+ name,
211
+ kind: asString(item.kind, "function"),
212
+ signature: asString(item.signature),
213
+ body: asString(item.body),
214
+ description: asString(item.description),
215
+ start_line: asNumber(item.start_line, 0),
216
+ end_line: asNumber(item.end_line, 0),
217
+ language: asString(item.language, "javascript"),
218
+ exported: asBoolean(item.exported, false),
219
+ checksum: asString(item.checksum),
220
+ updated_at: asString(item.updated_at),
221
+ source_of_truth: asBoolean(item.source_of_truth, true),
222
+ trust_level: asNumber(item.trust_level, 80),
223
+ status: asString(item.status, "active")
224
+ };
225
+ }
230
226
  }
231
227
 
232
- function parseModules(raw: JsonObject[]): ModuleEntity[] {
233
- return raw
234
- .map((item) => {
235
- const id = asString(item.id);
236
- if (!id) {
237
- return null;
238
- }
228
+ function* parseModules(raw: Iterable<JsonObject>): Generator<ModuleEntity> {
229
+ for (const item of raw) {
230
+ const id = asString(item.id);
231
+ if (!id) {
232
+ continue;
233
+ }
239
234
 
240
- return {
241
- id,
242
- path: asString(item.path),
243
- name: asString(item.name),
244
- summary: asString(item.summary),
245
- file_count: asNumber(item.file_count, 0),
246
- exported_symbols: asString(item.exported_symbols),
247
- updated_at: asString(item.updated_at),
248
- source_of_truth: asBoolean(item.source_of_truth, false),
249
- trust_level: asNumber(item.trust_level, 75),
250
- status: asString(item.status, "active")
251
- };
252
- })
253
- .filter((value): value is ModuleEntity => value !== null);
235
+ yield {
236
+ id,
237
+ path: asString(item.path),
238
+ name: asString(item.name),
239
+ summary: asString(item.summary),
240
+ file_count: asNumber(item.file_count, 0),
241
+ exported_symbols: asString(item.exported_symbols),
242
+ updated_at: asString(item.updated_at),
243
+ source_of_truth: asBoolean(item.source_of_truth, false),
244
+ trust_level: asNumber(item.trust_level, 75),
245
+ status: asString(item.status, "active")
246
+ };
247
+ }
254
248
  }
255
249
 
256
- function parseProjects(raw: JsonObject[]): ProjectEntity[] {
257
- return raw
258
- .map((item) => {
259
- const id = asString(item.id);
260
- if (!id) {
261
- return null;
262
- }
250
+ function* parseProjects(raw: Iterable<JsonObject>): Generator<ProjectEntity> {
251
+ for (const item of raw) {
252
+ const id = asString(item.id);
253
+ if (!id) {
254
+ continue;
255
+ }
263
256
 
264
- return {
265
- id,
266
- path: asString(item.path),
267
- name: asString(item.name),
268
- kind: asString(item.kind, "project"),
269
- language: asString(item.language, "dotnet"),
270
- target_framework: asString(item.target_framework),
271
- summary: asString(item.summary),
272
- file_count: asNumber(item.file_count, 0),
273
- updated_at: asString(item.updated_at),
274
- source_of_truth: asBoolean(item.source_of_truth, false),
275
- trust_level: asNumber(item.trust_level, 80),
276
- status: asString(item.status, "active")
277
- };
278
- })
279
- .filter((value): value is ProjectEntity => value !== null);
257
+ yield {
258
+ id,
259
+ path: asString(item.path),
260
+ name: asString(item.name),
261
+ kind: asString(item.kind, "project"),
262
+ language: asString(item.language, "dotnet"),
263
+ target_framework: asString(item.target_framework),
264
+ summary: asString(item.summary),
265
+ file_count: asNumber(item.file_count, 0),
266
+ updated_at: asString(item.updated_at),
267
+ source_of_truth: asBoolean(item.source_of_truth, false),
268
+ trust_level: asNumber(item.trust_level, 80),
269
+ status: asString(item.status, "active")
270
+ };
271
+ }
272
+ }
273
+
274
+ function* parseRelationRecords(raw: Iterable<JsonObject>, noteField: string): Generator<Relation> {
275
+ for (const item of raw) {
276
+ const from = asString(item.from);
277
+ const to = asString(item.to);
278
+ if (!from || !to) {
279
+ continue;
280
+ }
281
+
282
+ yield {
283
+ from,
284
+ to,
285
+ note: asString(item[noteField as keyof JsonObject])
286
+ };
287
+ }
280
288
  }
281
289
 
282
290
  function parseRelations(fileName: string, noteField: string): Relation[] {
283
- const raw = readEntityFile(fileName);
284
- return raw
285
- .map((item) => {
286
- const from = asString(item.from);
287
- const to = asString(item.to);
288
- if (!from || !to) {
289
- return null;
290
- }
291
+ return Array.from(parseRelationRecords(readEntityRecords(fileName), noteField));
292
+ }
293
+
294
+ function* relationRecords(fileName: string, noteField: string): Generator<Relation> {
295
+ yield* parseRelationRecords(readEntityRecords(fileName), noteField);
296
+ }
297
+
298
+ function* parseCallRelationRecords(raw: Iterable<JsonObject>): Generator<CallRelation> {
299
+ for (const item of raw) {
300
+ const from = asString(item.from);
301
+ const to = asString(item.to);
302
+ if (!from || !to) {
303
+ continue;
304
+ }
291
305
 
292
- return {
293
- from,
294
- to,
295
- note: asString(item[noteField as keyof JsonObject])
296
- };
297
- })
298
- .filter((value): value is Relation => value !== null);
306
+ yield {
307
+ from,
308
+ to,
309
+ call_type: asString(item.call_type, "direct")
310
+ };
311
+ }
299
312
  }
300
313
 
301
314
  function parseCallRelations(fileName: string): CallRelation[] {
302
- const raw = readEntityFile(fileName);
303
- return raw
304
- .map((item) => {
305
- const from = asString(item.from);
306
- const to = asString(item.to);
307
- if (!from || !to) {
308
- return null;
309
- }
315
+ return Array.from(parseCallRelationRecords(readEntityRecords(fileName)));
316
+ }
317
+
318
+ function* callRelationRecords(fileName: string): Generator<CallRelation> {
319
+ yield* parseCallRelationRecords(readEntityRecords(fileName));
320
+ }
321
+
322
+ function* parseImportRelationRecords(raw: Iterable<JsonObject>): Generator<ImportRelation> {
323
+ for (const item of raw) {
324
+ const from = asString(item.from);
325
+ const to = asString(item.to);
326
+ if (!from || !to) {
327
+ continue;
328
+ }
310
329
 
311
- return {
312
- from,
313
- to,
314
- call_type: asString(item.call_type, "direct")
315
- };
316
- })
317
- .filter((value): value is CallRelation => value !== null);
330
+ yield {
331
+ from,
332
+ to,
333
+ import_name: asString(item.import_name, "")
334
+ };
335
+ }
318
336
  }
319
337
 
320
338
  function parseImportRelations(fileName: string): ImportRelation[] {
321
- const raw = readEntityFile(fileName);
322
- return raw
323
- .map((item) => {
324
- const from = asString(item.from);
325
- const to = asString(item.to);
326
- if (!from || !to) {
327
- return null;
328
- }
339
+ return Array.from(parseImportRelationRecords(readEntityRecords(fileName)));
340
+ }
341
+
342
+ function* importRelationRecords(fileName: string): Generator<ImportRelation> {
343
+ yield* parseImportRelationRecords(readEntityRecords(fileName));
344
+ }
329
345
 
330
- return {
331
- from,
332
- to,
333
- import_name: asString(item.import_name, "")
334
- };
335
- })
336
- .filter((value): value is ImportRelation => value !== null);
346
+ function* parseSimpleRelationRecords(raw: Iterable<JsonObject>): Generator<Relation> {
347
+ for (const item of raw) {
348
+ const from = asString(item.from);
349
+ const to = asString(item.to);
350
+ if (!from || !to) {
351
+ continue;
352
+ }
353
+
354
+ yield {
355
+ from,
356
+ to,
357
+ note: ""
358
+ };
359
+ }
337
360
  }
338
361
 
339
362
  function parseSimpleRelations(fileName: string): Relation[] {
340
- const raw = readEntityFile(fileName);
341
- return raw
342
- .map((item) => {
343
- const from = asString(item.from);
344
- const to = asString(item.to);
345
- if (!from || !to) {
346
- return null;
347
- }
363
+ return Array.from(parseSimpleRelationRecords(readEntityRecords(fileName)));
364
+ }
348
365
 
349
- return {
350
- from,
351
- to,
352
- note: ""
353
- };
354
- })
355
- .filter((value): value is Relation => value !== null);
366
+ function* simpleRelationRecords(fileName: string): Generator<Relation> {
367
+ yield* parseSimpleRelationRecords(readEntityRecords(fileName));
356
368
  }
357
369
 
358
370
  async function rows(result: QueryResult | QueryResult[]): Promise<Record<string, unknown>[]> {
@@ -460,12 +472,12 @@ type GraphData = {
460
472
 
461
473
  function parseGraphData(): GraphData {
462
474
  return {
463
- fileEntities: parseFiles(readEntityFile("entities.file.jsonl")),
464
- ruleEntities: parseRules(readEntityFile("entities.rule.jsonl")),
465
- adrEntities: parseAdrs(readEntityFile("entities.adr.jsonl")),
466
- chunkEntities: parseChunks(readEntityFile("entities.chunk.jsonl")),
467
- moduleEntities: parseModules(readEntityFile("entities.module.jsonl")),
468
- projectEntities: parseProjects(readEntityFile("entities.project.jsonl")),
475
+ fileEntities: Array.from(parseFiles(readEntityFile("entities.file.jsonl"))),
476
+ ruleEntities: Array.from(parseRules(readEntityFile("entities.rule.jsonl"))),
477
+ adrEntities: Array.from(parseAdrs(readEntityFile("entities.adr.jsonl"))),
478
+ chunkEntities: Array.from(parseChunks(readEntityFile("entities.chunk.jsonl"))),
479
+ moduleEntities: Array.from(parseModules(readEntityFile("entities.module.jsonl"))),
480
+ projectEntities: Array.from(parseProjects(readEntityFile("entities.project.jsonl"))),
469
481
  constrains: parseRelations("relations.constrains.jsonl", "note"),
470
482
  implementsEdges: parseRelations("relations.implements.jsonl", "note"),
471
483
  supersedes: parseRelations("relations.supersedes.jsonl", "reason"),
@@ -506,9 +518,10 @@ function filterEdges<T extends { from: string; to: string }>(
506
518
  })();
507
519
  }
508
520
 
509
- function* ids<T extends { id: string }>(items: Iterable<T>): Iterable<string> {
521
+ function* collectIds<T extends { id: string }>(items: Iterable<T>, collected: Set<string>): Iterable<T> {
510
522
  for (const item of items) {
511
- yield item.id;
523
+ collected.add(item.id);
524
+ yield item;
512
525
  }
513
526
  }
514
527
 
@@ -537,28 +550,28 @@ async function copyTable(
537
550
  await conn.query(`COPY ${table} FROM "${csvForCopy}" ${CSV_COPY_OPTIONS};`);
538
551
  }
539
552
 
540
- // Bulk path: one COPY per table instead of a prepared statement per row.
541
- // Only used on the reset path, where the freshly created tables are empty.
542
- // Produces a byte-identical graph to rowByRowLoad (same parsed arrays, same
543
- // node-then-edge order, same dangling-edge filtering).
544
- async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
553
+ // Bulk path: one COPY per table instead of a prepared statement per row. Only
554
+ // used on the reset path, where the freshly created tables are empty. It reads
555
+ // node and relation JSONL streams directly into CSVs so the hot path retains
556
+ // only endpoint id sets rather than a full parsed GraphData object.
557
+ async function bulkLoad(conn: Connection): Promise<void> {
545
558
  fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
546
559
  fs.mkdirSync(GRAPH_IMPORT_DIR, { recursive: true });
547
560
 
548
561
  try {
549
- const fileIds = new Set(ids(data.fileEntities));
550
- const ruleIds = new Set(ids(data.ruleEntities));
551
- const adrIds = new Set(ids(data.adrEntities));
552
- const chunkIds = new Set(ids(data.chunkEntities));
553
- const moduleIds = new Set(ids(data.moduleEntities));
554
- const projectIds = new Set(ids(data.projectEntities));
562
+ const fileIds = new Set<string>();
563
+ const ruleIds = new Set<string>();
564
+ const adrIds = new Set<string>();
565
+ const chunkIds = new Set<string>();
566
+ const moduleIds = new Set<string>();
567
+ const projectIds = new Set<string>();
555
568
 
556
569
  // Nodes first — edges reference them by primary key.
557
570
  await copyTable(
558
571
  conn,
559
572
  "File",
560
573
  ["id", "path", "kind", "excerpt", "checksum", "updated_at", "source_of_truth", "trust_level", "status"],
561
- csvRows(data.fileEntities, (e) => [
574
+ csvRows(collectIds(parseFiles(readEntityRecords("entities.file.jsonl")), fileIds), (e) => [
562
575
  e.id, e.path, e.kind, e.excerpt, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
563
576
  ])
564
577
  );
@@ -566,7 +579,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
566
579
  conn,
567
580
  "Rule",
568
581
  ["id", "title", "body", "scope", "priority", "updated_at", "source_of_truth", "trust_level", "status"],
569
- csvRows(data.ruleEntities, (e) => [
582
+ csvRows(collectIds(parseRules(readEntityRecords("entities.rule.jsonl")), ruleIds), (e) => [
570
583
  e.id, e.title, e.body, e.scope, e.priority, e.updated_at, e.source_of_truth, e.trust_level, e.status
571
584
  ])
572
585
  );
@@ -574,7 +587,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
574
587
  conn,
575
588
  "ADR",
576
589
  ["id", "path", "title", "body", "decision_date", "supersedes_id", "source_of_truth", "trust_level", "status"],
577
- csvRows(data.adrEntities, (e) => [
590
+ csvRows(collectIds(parseAdrs(readEntityRecords("entities.adr.jsonl")), adrIds), (e) => [
578
591
  e.id, e.path, e.title, e.body, e.decision_date, e.supersedes_id, e.source_of_truth, e.trust_level, e.status
579
592
  ])
580
593
  );
@@ -585,7 +598,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
585
598
  "id", "file_id", "name", "kind", "signature", "body", "description", "start_line", "end_line",
586
599
  "language", "exported", "checksum", "updated_at", "source_of_truth", "trust_level", "status"
587
600
  ],
588
- csvRows(data.chunkEntities, (e) => [
601
+ csvRows(collectIds(parseChunks(readEntityRecords("entities.chunk.jsonl")), chunkIds), (e) => [
589
602
  e.id, e.file_id, e.name, e.kind, e.signature, e.body, e.description, e.start_line, e.end_line,
590
603
  e.language, e.exported, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
591
604
  ])
@@ -594,7 +607,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
594
607
  conn,
595
608
  "Module",
596
609
  ["id", "path", "name", "summary", "file_count", "exported_symbols", "updated_at", "source_of_truth", "trust_level", "status"],
597
- csvRows(data.moduleEntities, (e) => [
610
+ csvRows(collectIds(parseModules(readEntityRecords("entities.module.jsonl")), moduleIds), (e) => [
598
611
  e.id, e.path, e.name, e.summary, e.file_count, e.exported_symbols, e.updated_at, e.source_of_truth, e.trust_level, e.status
599
612
  ])
600
613
  );
@@ -605,7 +618,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
605
618
  "id", "path", "name", "kind", "language", "target_framework", "summary", "file_count",
606
619
  "updated_at", "source_of_truth", "trust_level", "status"
607
620
  ],
608
- csvRows(data.projectEntities, (e) => [
621
+ csvRows(collectIds(parseProjects(readEntityRecords("entities.project.jsonl")), projectIds), (e) => [
609
622
  e.id, e.path, e.name, e.kind, e.language, e.target_framework, e.summary, e.file_count,
610
623
  e.updated_at, e.source_of_truth, e.trust_level, e.status
611
624
  ])
@@ -615,43 +628,43 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
615
628
  // (COPY into a rel table reads src key, dst key, then properties in schema
616
629
  // order), but emitting real names keeps the CSVs self-describing.
617
630
  await copyTable(conn, "CONSTRAINS", ["from", "to", "note"],
618
- csvRows(filterEdges(data.constrains, ruleIds, fileIds), (e) => [e.from, e.to, e.note]));
631
+ csvRows(filterEdges(relationRecords("relations.constrains.jsonl", "note"), ruleIds, fileIds), (e) => [e.from, e.to, e.note]));
619
632
  await copyTable(conn, "IMPLEMENTS", ["from", "to", "note"],
620
- csvRows(filterEdges(data.implementsEdges, fileIds, ruleIds), (e) => [e.from, e.to, e.note]));
633
+ csvRows(filterEdges(relationRecords("relations.implements.jsonl", "note"), fileIds, ruleIds), (e) => [e.from, e.to, e.note]));
621
634
  await copyTable(conn, "SUPERSEDES", ["from", "to", "reason"],
622
- csvRows(filterEdges(data.supersedes, adrIds, adrIds), (e) => [e.from, e.to, e.note]));
635
+ csvRows(filterEdges(relationRecords("relations.supersedes.jsonl", "reason"), adrIds, adrIds), (e) => [e.from, e.to, e.note]));
623
636
  await copyTable(conn, "DEFINES", ["from", "to"],
624
- csvRows(filterEdges(data.defines, fileIds, chunkIds), (e) => [e.from, e.to]));
637
+ csvRows(filterEdges(simpleRelationRecords("relations.defines.jsonl"), fileIds, chunkIds), (e) => [e.from, e.to]));
625
638
  await copyTable(conn, "CALLS", ["from", "to", "call_type"],
626
- csvRows(filterEdges(data.calls, chunkIds, chunkIds), (e) => [e.from, e.to, e.call_type]));
639
+ csvRows(filterEdges(callRelationRecords("relations.calls.jsonl"), chunkIds, chunkIds), (e) => [e.from, e.to, e.call_type]));
627
640
  await copyTable(conn, "IMPORTS", ["from", "to", "import_name"],
628
- csvRows(filterEdges(data.imports, chunkIds, fileIds), (e) => [e.from, e.to, e.import_name]));
641
+ csvRows(filterEdges(importRelationRecords("relations.imports.jsonl"), chunkIds, fileIds), (e) => [e.from, e.to, e.import_name]));
629
642
  await copyTable(conn, "CALLS_SQL", ["from", "to", "note"],
630
- csvRows(filterEdges(data.callsSql, fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
643
+ csvRows(filterEdges(relationRecords("relations.calls_sql.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
631
644
  await copyTable(conn, "USES_CONFIG_KEY", ["from", "to", "note"],
632
- csvRows(filterEdges(data.usesConfigKey, fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
645
+ csvRows(filterEdges(relationRecords("relations.uses_config_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
633
646
  await copyTable(conn, "USES_RESOURCE_KEY", ["from", "to", "note"],
634
- csvRows(filterEdges(data.usesResourceKey, fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
647
+ csvRows(filterEdges(relationRecords("relations.uses_resource_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
635
648
  await copyTable(conn, "USES_SETTING_KEY", ["from", "to", "note"],
636
- csvRows(filterEdges(data.usesSettingKey, fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
649
+ csvRows(filterEdges(relationRecords("relations.uses_setting_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
637
650
  await copyTable(conn, "CONTAINS", ["from", "to"],
638
- csvRows(filterEdges(data.contains, moduleIds, fileIds), (e) => [e.from, e.to]));
651
+ csvRows(filterEdges(simpleRelationRecords("relations.contains.jsonl"), moduleIds, fileIds), (e) => [e.from, e.to]));
639
652
  await copyTable(conn, "CONTAINS_MODULE", ["from", "to"],
640
- csvRows(filterEdges(data.containsModule, moduleIds, moduleIds), (e) => [e.from, e.to]));
653
+ csvRows(filterEdges(simpleRelationRecords("relations.contains_module.jsonl"), moduleIds, moduleIds), (e) => [e.from, e.to]));
641
654
  await copyTable(conn, "EXPORTS", ["from", "to"],
642
- csvRows(filterEdges(data.exports, moduleIds, chunkIds), (e) => [e.from, e.to]));
655
+ csvRows(filterEdges(simpleRelationRecords("relations.exports.jsonl"), moduleIds, chunkIds), (e) => [e.from, e.to]));
643
656
  await copyTable(conn, "INCLUDES_FILE", ["from", "to"],
644
- csvRows(filterEdges(data.includesFile, projectIds, fileIds), (e) => [e.from, e.to]));
657
+ csvRows(filterEdges(simpleRelationRecords("relations.includes_file.jsonl"), projectIds, fileIds), (e) => [e.from, e.to]));
645
658
  await copyTable(conn, "REFERENCES_PROJECT", ["from", "to", "note"],
646
- csvRows(filterEdges(data.referencesProject, projectIds, projectIds), (e) => [e.from, e.to, e.note]));
659
+ csvRows(filterEdges(relationRecords("relations.references_project.jsonl", "note"), projectIds, projectIds), (e) => [e.from, e.to, e.note]));
647
660
  await copyTable(conn, "USES_RESOURCE", ["from", "to", "note"],
648
- csvRows(filterEdges(data.usesResource, fileIds, fileIds), (e) => [e.from, e.to, e.note]));
661
+ csvRows(filterEdges(relationRecords("relations.uses_resource.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
649
662
  await copyTable(conn, "USES_SETTING", ["from", "to", "note"],
650
- csvRows(filterEdges(data.usesSetting, fileIds, fileIds), (e) => [e.from, e.to, e.note]));
663
+ csvRows(filterEdges(relationRecords("relations.uses_setting.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
651
664
  await copyTable(conn, "USES_CONFIG", ["from", "to", "note"],
652
- csvRows(filterEdges(data.usesConfig, fileIds, fileIds), (e) => [e.from, e.to, e.note]));
665
+ csvRows(filterEdges(relationRecords("relations.uses_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
653
666
  await copyTable(conn, "TRANSFORMS_CONFIG", ["from", "to", "note"],
654
- csvRows(filterEdges(data.transformsConfig, fileIds, fileIds), (e) => [e.from, e.to, e.note]));
667
+ csvRows(filterEdges(relationRecords("relations.transforms_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
655
668
  } finally {
656
669
  fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
657
670
  }
@@ -942,13 +955,11 @@ async function main(): Promise<void> {
942
955
  const ontologyStatements = parseOntologyStatements(fs.readFileSync(ONTOLOGY_PATH, "utf8"));
943
956
  await executeStatements(conn, ontologyStatements);
944
957
 
945
- const data = parseGraphData();
946
-
947
958
  const bulkEnabled = reset && process.env.CORTEX_GRAPH_BULK_LOAD !== "never";
948
959
  let usedBulk = false;
949
960
  if (bulkEnabled) {
950
961
  try {
951
- await bulkLoad(conn, data);
962
+ await bulkLoad(conn);
952
963
  usedBulk = true;
953
964
  console.log("[graph-load] loaded via COPY bulk import");
954
965
  } catch (error) {
@@ -959,6 +970,7 @@ async function main(): Promise<void> {
959
970
  }
960
971
  }
961
972
  if (!usedBulk) {
973
+ const data = parseGraphData();
962
974
  await rowByRowLoad(conn, data);
963
975
  }
964
976
 
@@ -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 });