@functional-systems/srcx 0.1.0-dev.19 → 0.1.0-dev.21

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/runtime/qwen.py CHANGED
@@ -4,6 +4,7 @@ import json
4
4
  import math
5
5
  import os
6
6
  import sys
7
+ from pathlib import Path
7
8
 
8
9
  MODEL = "Qwen/Qwen3-Reranker-0.6B"
9
10
  REVISION = "e61197ed45024b0ed8a2d74b80b4d909f1255473"
@@ -14,6 +15,19 @@ PREFIX = (
14
15
  )
15
16
  SUFFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
16
17
  INSTRUCTION = "Given a code search query, retrieve code passages that answer the query."
18
+ CACHE_FILES = ["config.json", "generation_config.json", "model.safetensors",
19
+ "tokenizer.json", "tokenizer_config.json", "vocab.json", "merges.txt"]
20
+
21
+
22
+ class WorkerFailure(ValueError):
23
+ """Exit codes: 20 imports, 21 cache, 22 device, 23 pair tokens, 24 request bytes.
24
+
25
+ The Node caller maps codes to messages; exception text never crosses the boundary.
26
+ """
27
+
28
+ def __init__(self, code):
29
+ super().__init__("Qwen worker failure")
30
+ self.code = code
17
31
 
18
32
 
19
33
  def require(condition, message):
@@ -25,7 +39,8 @@ def encode(tokenizer, query, text):
25
39
  body = f"<Instruct>: {INSTRUCTION}\n<Query>: {query}\n<Document>: {text}"
26
40
  ids = sum((tokenizer.encode(p, add_special_tokens=False)
27
41
  for p in (PREFIX, body, SUFFIX)), [])
28
- require(len(ids) <= 8192, "Pair exceeds 8192 tokens; no truncation")
42
+ if len(ids) > 8192:
43
+ raise WorkerFailure(23)
29
44
  return ids
30
45
 
31
46
 
@@ -45,22 +60,33 @@ def validate(body):
45
60
 
46
61
  def score(body):
47
62
  os.environ["HF_HUB_OFFLINE"] = "1"
48
- from huggingface_hub import snapshot_download
49
- from transformers import AutoTokenizer, AutoModelForCausalLM
50
- import torch
51
-
52
- path = snapshot_download(MODEL, revision=REVISION, local_files_only=True,
53
- allow_patterns=["config.json", "generation_config.json", "model.safetensors",
54
- "tokenizer.json", "tokenizer_config.json", "vocab.json", "merges.txt"])
63
+ device = os.environ.get("SRCX_RERANK_DEVICE", "auto")
64
+ if device not in ("auto", "cpu", "mps", "cuda"):
65
+ raise WorkerFailure(22)
66
+ try:
67
+ from huggingface_hub import snapshot_download
68
+ from huggingface_hub.errors import LocalEntryNotFoundError
69
+ from transformers import AutoTokenizer, AutoModelForCausalLM
70
+ import torch
71
+ except ImportError:
72
+ raise WorkerFailure(20) from None
73
+ if device == "auto":
74
+ device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
75
+ elif ((device == "cuda" and not torch.cuda.is_available()) or
76
+ (device == "mps" and not torch.backends.mps.is_available())):
77
+ raise WorkerFailure(22)
78
+ try:
79
+ path = snapshot_download(MODEL, revision=REVISION, local_files_only=True,
80
+ allow_patterns=CACHE_FILES)
81
+ except LocalEntryNotFoundError:
82
+ raise WorkerFailure(21) from None
83
+ if not all((Path(path) / name).is_file() for name in CACHE_FILES):
84
+ raise WorkerFailure(21)
55
85
  tokenizer = AutoTokenizer.from_pretrained(path, local_files_only=True, trust_remote_code=False)
56
86
  # Reuse exact duplicate code within this query, retaining every candidate ID.
57
87
  encoded = {c["text"]: None for c in body["candidates"]}
58
88
  for text in encoded:
59
89
  encoded[text] = encode(tokenizer, body["query"], text)
60
- device = os.environ.get("SRCX_RERANK_DEVICE", "auto")
61
- require(device in ("auto", "cpu", "mps", "cuda"), "Invalid device")
62
- if device == "auto":
63
- device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
64
90
  model = AutoModelForCausalLM.from_pretrained(
65
91
  path, local_files_only=True, trust_remote_code=False,
66
92
  dtype=torch.float32, attn_implementation="sdpa",
@@ -83,15 +109,24 @@ def score(body):
83
109
 
84
110
  def main():
85
111
  raw = sys.stdin.buffer.read(4 * 1024 * 1024 + 1)
86
- require(len(raw) <= 4 * 1024 * 1024, "Request exceeds 4 MiB")
112
+ if len(raw) > 4 * 1024 * 1024:
113
+ raise WorkerFailure(24)
87
114
  body = validate(json.loads(raw))
88
115
  print(json.dumps(score(body), allow_nan=False))
89
116
 
90
117
 
91
- if __name__ == "__main__":
118
+ def run():
92
119
  try:
93
120
  main()
121
+ except WorkerFailure as error:
122
+ # The Node caller maps these codes to fixed, actionable messages.
123
+ return error.code
94
124
  except Exception:
95
125
  # Libraries may include source text or environment values in exceptions.
96
126
  print("Qwen worker failed; verify setup, device and input size.", file=sys.stderr)
97
- sys.exit(1)
127
+ return 1
128
+ return 0
129
+
130
+
131
+ if __name__ == "__main__":
132
+ sys.exit(run())
package/src/build.ts CHANGED
@@ -46,7 +46,7 @@ export const PRESET: Preset = {
46
46
  maxFileBytes: 1024 * 1024,
47
47
  embedding: null,
48
48
  };
49
- /** Opt-in managed preset; PRESET stays byte-for-byte compatible with lexical corpora. */
49
+ /** Embedding variants share the same chunking behavior. */
50
50
  export const MANAGED_PRESET: Preset = {
51
51
  ...PRESET,
52
52
  embedding: {
package/src/chunk.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { fileURLToPath } from "node:url";
1
2
  import { createRequire } from "node:module";
2
3
  import { Parser, Language, type Node } from "web-tree-sitter";
3
4
  import { getEncoding } from "js-tiktoken";
@@ -8,9 +9,11 @@ export function tokens(text: string): number {
8
9
  return encoding.encode(text, [], []).length;
9
10
  }
10
11
  export const CHUNKER = {
11
- version: 1,
12
+ version: 2,
12
13
  parser: "web-tree-sitter@0.25.10",
13
14
  grammars: "tree-sitter-wasms@0.1.13",
15
+ sqlGrammar:
16
+ "tree-sitter-wasm@2.0.2/sql/b77530893b1dd6d1d4814eacf34d25bcbe4a12ec9a25a8c45b320d447265f42b",
14
17
  tokenizer: "js-tiktoken@1.0.21/cl100k_base",
15
18
  targetTokens: 800,
16
19
  maxTokens: 1500,
@@ -46,17 +49,42 @@ async function language(name: string): Promise<Language> {
46
49
  let l = languages.get(name);
47
50
  if (!l) {
48
51
  l = await Language.load(
49
- require.resolve(`tree-sitter-wasms/out/tree-sitter-${name}.wasm`),
52
+ name === "sql"
53
+ ? fileURLToPath(
54
+ new URL(
55
+ "../runtime/grammars/tree-sitter-sql.wasm",
56
+ import.meta.url,
57
+ ),
58
+ )
59
+ : require.resolve(
60
+ `tree-sitter-wasms/out/tree-sitter-${name === "shell" ? "bash" : name}.wasm`,
61
+ ),
50
62
  );
51
63
  languages.set(name, l);
52
64
  }
53
65
  return l;
54
66
  }
55
67
  export function detectLanguage(path: string): string {
68
+ if (path.endsWith(".C")) return "cpp";
56
69
  const ext = path.split(".").at(-1)?.toLowerCase();
57
70
  return (
58
71
  (
59
72
  {
73
+ py: "python",
74
+ pyi: "python",
75
+ go: "go",
76
+ rs: "rust",
77
+ c: "c",
78
+ h: "c",
79
+ cc: "cpp",
80
+ cpp: "cpp",
81
+ cxx: "cpp",
82
+ hh: "cpp",
83
+ hpp: "cpp",
84
+ hxx: "cpp",
85
+ sh: "shell",
86
+ bash: "shell",
87
+ sql: "sql",
60
88
  java: "java",
61
89
  ts: "typescript",
62
90
  tsx: "tsx",
@@ -102,6 +130,278 @@ function scopeName(n: Node): string | undefined {
102
130
  }
103
131
  return names.length ? names.join(".") : undefined;
104
132
  }
133
+
134
+ /** Keep complete functions (including decorators) before token-bound splitting. */
135
+ function pythonUnits(root: Node): Unit[] {
136
+ const units: Unit[] = [];
137
+ function visit(n: Node, scope?: string): void {
138
+ const definition = n.childForFieldName("definition") ?? n;
139
+ const body = definition.childForFieldName("body");
140
+ const symbol = definition.childForFieldName("name")?.text.slice(0, 200);
141
+ const unit: Unit = {
142
+ start: n.startIndex,
143
+ end: n.endIndex,
144
+ kind: classify(definition.type),
145
+ scope,
146
+ symbol,
147
+ };
148
+ if (definition.type === "class_definition" && body) {
149
+ units.push({ ...unit, end: body.startIndex });
150
+ const nested = [scope, symbol].filter(Boolean).join(".");
151
+ for (const child of body.namedChildren) if (child) visit(child, nested);
152
+ } else {
153
+ if (definition.type === "function_definition" && body)
154
+ unit.signature = definition.text
155
+ .slice(0, body.startIndex - definition.startIndex)
156
+ .trim()
157
+ .slice(0, 240);
158
+ if (
159
+ definition.type === "expression_statement" &&
160
+ definition.firstNamedChild?.type === "string"
161
+ )
162
+ unit.kind = "documentation";
163
+ units.push(unit);
164
+ }
165
+ }
166
+ for (const n of root.namedChildren) if (n) visit(n);
167
+ return units;
168
+ }
169
+
170
+ function goUnits(root: Node): Unit[] {
171
+ const units: Unit[] = [];
172
+ for (const n of root.namedChildren) {
173
+ if (!n) continue;
174
+ if (n.type === "type_declaration") {
175
+ // Include the type keyword and grouped declaration delimiters in coverage.
176
+ const specs = n.namedChildren.filter(
177
+ (node): node is Node => node !== null,
178
+ );
179
+ for (const [i, spec] of specs.entries())
180
+ units.push({
181
+ start: i === 0 ? n.startIndex : spec.startIndex,
182
+ end: i === specs.length - 1 ? n.endIndex : spec.endIndex,
183
+ kind: classify(spec.type),
184
+ symbol: spec.childForFieldName("name")?.text.slice(0, 200),
185
+ });
186
+ continue;
187
+ }
188
+ const unit: Unit = {
189
+ start: n.startIndex,
190
+ end: n.endIndex,
191
+ kind: classify(n.type),
192
+ symbol: n.childForFieldName("name")?.text.slice(0, 200),
193
+ };
194
+ let receiver = n
195
+ .childForFieldName("receiver")
196
+ ?.firstNamedChild?.childForFieldName("type");
197
+ if (receiver?.type === "pointer_type") receiver = receiver.firstNamedChild;
198
+ if (receiver?.type === "generic_type")
199
+ receiver = receiver.childForFieldName("type");
200
+ if (receiver) unit.scope = receiver.text.slice(0, 160);
201
+ const body = n.childForFieldName("body");
202
+ if (unit.kind === "function")
203
+ unit.signature = n.text
204
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
205
+ .trim()
206
+ .slice(0, 240);
207
+ units.push(unit);
208
+ }
209
+ return units;
210
+ }
211
+
212
+ /** Walk declarators rather than parameter/type names (including pointer returns). */
213
+ function declaratorName(n: Node | null): string | undefined {
214
+ if (!n) return undefined;
215
+ if (
216
+ /^(identifier|field_identifier|type_identifier|qualified_identifier|destructor_name|operator_name)$/.test(
217
+ n.type,
218
+ )
219
+ )
220
+ return n.text;
221
+ return declaratorName(
222
+ n.childForFieldName("declarator") ??
223
+ (n.type === "parenthesized_declarator" ? n.firstNamedChild : null),
224
+ );
225
+ }
226
+
227
+ function cUnits(root: Node): Unit[] {
228
+ const units: Unit[] = [];
229
+ function visit(n: Node, scope?: string, start = n.startIndex): void {
230
+ if (n.type === "template_declaration") {
231
+ const declaration = n.namedChildren
232
+ .filter((c): c is Node => !!c && c.type !== "template_parameter_list")
233
+ .at(-1);
234
+ if (declaration) {
235
+ visit(declaration, scope, start);
236
+ return;
237
+ }
238
+ }
239
+ const body = n.childForFieldName("body");
240
+ const symbol =
241
+ n.childForFieldName("name")?.text ??
242
+ declaratorName(n.childForFieldName("declarator"));
243
+ const unit: Unit = {
244
+ start,
245
+ end: n.endIndex,
246
+ kind: n.type === "preproc_include" ? "imports" : classify(n.type),
247
+ scope,
248
+ symbol: symbol?.slice(0, 200),
249
+ };
250
+ if (body && /^(declaration_list|field_declaration_list)$/.test(body.type)) {
251
+ const nested = [scope, symbol].filter(Boolean).join("::") || undefined;
252
+ units.push({ ...unit, end: body.startIndex + 1 });
253
+ for (const child of body.namedChildren) if (child) visit(child, nested);
254
+ units.push({
255
+ start: body.endIndex - 1,
256
+ end: n.endIndex,
257
+ kind: "structural",
258
+ scope: nested,
259
+ });
260
+ } else if (/^preproc_(if|ifdef|elif|else|elifdef)$/.test(n.type)) {
261
+ // Preserve directives and both branches without evaluating the build configuration.
262
+ const condition =
263
+ n.childForFieldName("condition") ?? n.childForFieldName("name");
264
+ const children = n.namedChildren.filter(
265
+ (c): c is Node => !!c && c.id !== condition?.id,
266
+ );
267
+ if (!children.length) {
268
+ units.push(unit);
269
+ return;
270
+ }
271
+ units.push({ ...unit, end: children[0]!.startIndex });
272
+ for (const child of children) visit(child, scope);
273
+ const end = children.at(-1)!.endIndex;
274
+ if (end < n.endIndex)
275
+ units.push({ start: end, end: n.endIndex, kind: "structural", scope });
276
+ } else {
277
+ if (n.type === "function_definition")
278
+ unit.signature = n.text
279
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
280
+ .trim()
281
+ .slice(0, 240);
282
+ units.push(unit);
283
+ }
284
+ }
285
+ for (const n of root.namedChildren) if (n) visit(n);
286
+ return units;
287
+ }
288
+
289
+ function shellUnits(root: Node): Unit[] {
290
+ return root.namedChildren
291
+ .filter((n): n is Node => !!n)
292
+ .map((n) => {
293
+ const body = n.childForFieldName("body");
294
+ return {
295
+ start: n.startIndex,
296
+ end: n.endIndex,
297
+ kind: classify(n.type),
298
+ symbol:
299
+ n.type === "function_definition"
300
+ ? n.childForFieldName("name")?.text.slice(0, 200)
301
+ : undefined,
302
+ signature:
303
+ n.type === "function_definition"
304
+ ? n.text
305
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
306
+ .trim()
307
+ .slice(0, 240)
308
+ : undefined,
309
+ };
310
+ });
311
+ }
312
+
313
+ function sqlUnits(root: Node): Unit[] {
314
+ return root.namedChildren
315
+ .filter((n): n is Node => !!n)
316
+ .map((n) => {
317
+ const declaration = n.firstNamedChild;
318
+ // Object names describe DDL declarations, not every table referenced by a query.
319
+ const object = declaration?.type.startsWith("create_")
320
+ ? declaration.namedChildren.find((c) => c?.type === "object_reference")
321
+ : undefined;
322
+ return {
323
+ start: n.startIndex,
324
+ end: n.nextSibling?.type === ";" ? n.nextSibling.endIndex : n.endIndex,
325
+ kind: n.type === "comment" ? "documentation" : "statement",
326
+ symbol: object?.childForFieldName("name")?.text.slice(0, 200),
327
+ scope: object?.childForFieldName("schema")?.text.slice(0, 160),
328
+ };
329
+ });
330
+ }
331
+
332
+ function rustUnits(root: Node): Unit[] {
333
+ const units: Unit[] = [];
334
+ function visit(container: Node, scope?: string): void {
335
+ let prefix: number | undefined;
336
+ for (const n of container.namedChildren) {
337
+ if (!n) continue;
338
+ // Outer attributes/doc comments belong to the following item, not a chunk
339
+ // containing only cfg/derive metadata. Inner module docs remain separate.
340
+ if (
341
+ n.type === "attribute_item" ||
342
+ /^(\/\/\/(?!\/)|\/\*\*(?!\*))/.test(n.text)
343
+ ) {
344
+ prefix ??= n.startIndex;
345
+ continue;
346
+ }
347
+ if (prefix !== undefined && n.type.endsWith("comment")) continue;
348
+ const body = n.childForFieldName("body");
349
+ const name = n.childForFieldName("name")?.text;
350
+ const type = n.childForFieldName("type")?.text;
351
+ const trait = n.childForFieldName("trait")?.text;
352
+ const unit: Unit = {
353
+ start: prefix ?? n.startIndex,
354
+ end: n.endIndex,
355
+ kind:
356
+ n.type === "use_declaration" || n.type === "extern_crate_declaration"
357
+ ? "imports"
358
+ : classify(n.type),
359
+ scope,
360
+ symbol: (
361
+ name ??
362
+ (n.type === "impl_item" ? type : n.childForFieldName("macro")?.text)
363
+ )?.slice(0, 200),
364
+ };
365
+ prefix = undefined;
366
+ if (body?.type === "declaration_list") {
367
+ const owner =
368
+ n.type === "impl_item"
369
+ ? [type, trait].filter(Boolean).join(" as ")
370
+ : name;
371
+ const nested =
372
+ [scope, owner].filter(Boolean).join("::").slice(0, 160) || undefined;
373
+ units.push({ ...unit, end: body.startIndex + 1 });
374
+ visit(body, nested);
375
+ // Keep closing delimiters with their container, including empty bodies.
376
+ units.push({
377
+ start: body.endIndex - 1,
378
+ end: n.endIndex,
379
+ kind: "structural",
380
+ scope: nested,
381
+ });
382
+ } else {
383
+ if (unit.kind === "function")
384
+ unit.signature = n.text
385
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
386
+ .trim()
387
+ .slice(0, 240);
388
+ units.push(unit);
389
+ }
390
+ }
391
+ if (prefix !== undefined)
392
+ units.push({
393
+ start: prefix,
394
+ end:
395
+ container.type === "declaration_list"
396
+ ? container.endIndex - 1
397
+ : container.endIndex,
398
+ kind: "documentation",
399
+ scope,
400
+ });
401
+ }
402
+ visit(root);
403
+ return units;
404
+ }
105
405
  /** Bound strings without slicing a surrogate pair; prefer line ends where possible. */
106
406
  function splitUnit(
107
407
  source: string,
@@ -198,7 +498,21 @@ export async function chunk(
198
498
  if (mode === "window") {
199
499
  status = "window-baseline";
200
500
  units = [{ start: 0, end: source.length, kind: "fallback" }];
201
- } else if (["java", "typescript", "tsx", "javascript"].includes(lang)) {
501
+ } else if (
502
+ [
503
+ "java",
504
+ "typescript",
505
+ "tsx",
506
+ "javascript",
507
+ "python",
508
+ "go",
509
+ "rust",
510
+ "c",
511
+ "cpp",
512
+ "shell",
513
+ "sql",
514
+ ].includes(lang)
515
+ ) {
202
516
  const grammar = await language(lang);
203
517
  const parser = new Parser();
204
518
  let tree: ReturnType<Parser["parse"]> = null;
@@ -233,7 +547,13 @@ export async function chunk(
233
547
  for (const child of n.namedChildren) if (child) visit(child);
234
548
  } else if (n.endIndex > n.startIndex) units.push(u);
235
549
  }
236
- visit(tree.rootNode);
550
+ if (lang === "python") units = pythonUnits(tree.rootNode);
551
+ else if (lang === "go") units = goUnits(tree.rootNode);
552
+ else if (lang === "rust") units = rustUnits(tree.rootNode);
553
+ else if (lang === "c" || lang === "cpp") units = cUnits(tree.rootNode);
554
+ else if (lang === "shell") units = shellUnits(tree.rootNode);
555
+ else if (lang === "sql") units = sqlUnits(tree.rootNode);
556
+ else visit(tree.rootNode);
237
557
  // Attach every gap (punctuation, BOM, comments, whitespace) without altering source bytes.
238
558
  let cursor = 0;
239
559
  const covered: Unit[] = [];
@@ -272,6 +592,9 @@ export async function chunk(
272
592
  const prev = merged.at(-1);
273
593
  if (
274
594
  prev &&
595
+ lang !== "sql" &&
596
+ (!["python", "go", "rust", "c", "cpp", "shell"].includes(lang) ||
597
+ (!prev.symbol && !u.symbol)) &&
275
598
  prev.kind !== "function" &&
276
599
  u.kind === prev.kind &&
277
600
  u.scope === prev.scope &&
package/src/rerank.ts CHANGED
@@ -4,6 +4,14 @@ import { invariant } from "./common.js";
4
4
 
5
5
  export const QWEN_MODEL = "Qwen/Qwen3-Reranker-0.6B";
6
6
  export const QWEN_REVISION = "e61197ed45024b0ed8a2d74b80b4d909f1255473";
7
+ // Reserved worker exit codes; never interpret or forward library stderr.
8
+ const WORKER_FAILURES: Record<number, string> = {
9
+ 20: "Qwen Python dependencies are missing or incompatible. Install runtime/requirements.txt with the Python selected by SRCX_RERANK_PYTHON.",
10
+ 21: "Qwen model cache is missing or incomplete. Download the pinned model revision from README and check HF_HUB_CACHE.",
11
+ 22: "Qwen device is invalid or unavailable. Set SRCX_RERANK_DEVICE to auto or cpu, or a supported mps/cuda device.",
12
+ 23: "Qwen query/chunk pair exceeds 8,192 tokens. Shorten the query or use search without --rerank; source is not truncated.",
13
+ 24: "Qwen request exceeds 4 MiB. Reduce --candidates or shorten the query.",
14
+ };
7
15
  export type Candidate = { id: string; text: string };
8
16
  export type RerankOptions = {
9
17
  rerank?: "qwen";
@@ -49,7 +57,7 @@ export async function qwenScores(query: string, candidates: Candidate[]) {
49
57
  const input = JSON.stringify({ query, candidates });
50
58
  invariant(
51
59
  Buffer.byteLength(input) <= 4 * 1024 * 1024,
52
- "Reranker input exceeds 4 MiB.",
60
+ "Reranker input exceeds 4 MiB. Reduce --candidates or shorten the query; no fallback was applied.",
53
61
  );
54
62
  const python = process.env.SRCX_RERANK_PYTHON;
55
63
  invariant(
@@ -74,13 +82,21 @@ export async function qwenScores(query: string, candidates: Candidate[]) {
74
82
  },
75
83
  },
76
84
  (error, output) => {
77
- if (error)
78
- reject(
79
- new Error(
80
- "Qwen reranking failed or exceeded 120 seconds. Check Python dependencies, cached model and SRCX_RERANK_DEVICE; no fallback was applied.",
81
- ),
82
- );
83
- else resolve(output);
85
+ if (!error) return resolve(output);
86
+ let message =
87
+ "Qwen reranking failed. Check Python dependencies, cached model and SRCX_RERANK_DEVICE.";
88
+ if (error.code === "ENOENT")
89
+ message =
90
+ "Qwen Python executable was not found. Set SRCX_RERANK_PYTHON to an existing Python executable.";
91
+ else if (error.code === "EACCES")
92
+ message =
93
+ "Qwen Python executable cannot be started. Check execute permissions for SRCX_RERANK_PYTHON.";
94
+ else if (error.killed && error.signal === "SIGKILL" && !error.code)
95
+ message =
96
+ "Qwen reranking exceeded 120 seconds. Reduce --candidates or use an available faster device via SRCX_RERANK_DEVICE.";
97
+ else if (typeof error.code === "number")
98
+ message = WORKER_FAILURES[error.code] ?? message;
99
+ reject(new Error(`${message} No fallback was applied.`));
84
100
  },
85
101
  );
86
102
  // An early worker exit can close stdin before Node finishes writing.