openclaw-sc 5.38.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 (48) hide show
  1. package/.env.example +13 -0
  2. package/INSTALL.md +13 -0
  3. package/LICENSE +233 -0
  4. package/README.md +123 -0
  5. package/SECURITY.md +27 -0
  6. package/index.js +3479 -0
  7. package/lib/code-review-shared.js +164 -0
  8. package/lib/config.js +164 -0
  9. package/lib/constants.js +438 -0
  10. package/lib/decomposer.js +896 -0
  11. package/lib/dialog-recall.js +389 -0
  12. package/lib/env.js +59 -0
  13. package/lib/level-rules.js +72 -0
  14. package/lib/log-manager.js +258 -0
  15. package/lib/preemption.js +278 -0
  16. package/lib/priority-calc.js +134 -0
  17. package/lib/prompt-injection.js +748 -0
  18. package/lib/route-evidence.js +701 -0
  19. package/lib/shared-fs.js +244 -0
  20. package/lib/steward-rules.js +648 -0
  21. package/lib/task-center.js +602 -0
  22. package/lib/task-chain.js +713 -0
  23. package/lib/task-checker.js +317 -0
  24. package/lib/task-profiles.js +255 -0
  25. package/lib/tcell.js +411 -0
  26. package/lib/tool-auto-discover.js +522 -0
  27. package/lib/tool-enrich-subagent.js +375 -0
  28. package/lib/tool-handlers.js +178 -0
  29. package/lib/tool-selector.js +459 -0
  30. package/openclaw.plugin.json +55 -0
  31. package/package.json +63 -0
  32. package/security.js +141 -0
  33. package/tools/bridge.js +3288 -0
  34. package/tools/dashboard/tk-dashboard.py +262 -0
  35. package/tools/hippocampus-multi-search.js +1038 -0
  36. package/tools/hippocampus-sqlite.js +738 -0
  37. package/tools/hippocampus-store.js +262 -0
  38. package/tools/mcp-tools.config.json +653 -0
  39. package/tools/pipeline-engine.js +667 -0
  40. package/tools/sidecar/_check_tools.py +34 -0
  41. package/tools/sidecar/sidecar-server.cjs +1360 -0
  42. package/tools/sidecar/subagent-runner.cjs +1471 -0
  43. package/tools/system-tools.js +619 -0
  44. package/vector/README.md +100 -0
  45. package/vector/usearch-bridge.js +639 -0
  46. package/vector/usearch-http.js +74 -0
  47. package/vector/usearch-serve.js +156 -0
  48. package/workers/worker.js +1419 -0
@@ -0,0 +1,639 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * usearch-bridge.js - single-service, multi-backend USearch bridge.
4
+ *
5
+ * Compatibility:
6
+ * - qdrantQuery(query, topK, timeoutMs) still works and defaults to OpenClaw.
7
+ * - qdrantBuild(files, timeoutMs) still works and defaults to OpenClaw.
8
+ */
9
+
10
+ import { Index } from 'usearch';
11
+ import { spawn } from 'child_process';
12
+ import { copyFileSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, appendFileSync, renameSync, rmSync, statSync, writeFileSync } from 'fs';
13
+ import { dirname, join } from 'path';
14
+ import { homedir } from 'os';
15
+ import { createInterface } from 'readline';
16
+
17
+ const USER_HOME = process.env.OPENCLAW_USER_HOME
18
+ || process.env.CODEX_USER_HOME
19
+ || process.env.USERPROFILE
20
+ || homedir();
21
+ const ws = process.env.OPENCLAW_WORKSPACE_ROOT || join(USER_HOME, '.openclaw', 'workspace');
22
+ const HIP_DIR = join(ws, 'memory', 'hippocampus');
23
+ const CODEX_QA_USEARCH_DIR = process.env.CODEX_QA_USEARCH_INDEX_ROOT
24
+ || join(USER_HOME, '.codex', 'memory', 'indexes', 'codex-qa-diary-usearch-bge-m3');
25
+ const EMBED_SCRIPT = process.env.BGE_M3_EMBED_SCRIPT || join(ws, 'scripts', 'bge-m3-embed-serve.py');
26
+
27
+ const PYTHON_312 = join(
28
+ USER_HOME,
29
+ 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'python.exe'
30
+ );
31
+
32
+ export const MODEL_ID = process.env.BGE_M3_MODEL_ID || 'BAAI/bge-m3';
33
+ export const MODEL_VARIANT = process.env.BGE_M3_MODEL_VARIANT || 'dynamic-int8-onnx';
34
+ export const MODEL_SOURCE_REPO = process.env.BGE_M3_MODEL_SOURCE_REPO || 'er6y/bge-m3_dynamic_int8_onnx';
35
+ export const DIM = 1024;
36
+ export const TOP_K = 30;
37
+ export const DEFAULT_BACKEND_ID = 'openclaw_usearch';
38
+ const BATCH_SIZE = 16;
39
+
40
+ const BACKENDS = Object.freeze({
41
+ openclaw_usearch: {
42
+ id: 'openclaw_usearch',
43
+ aliases: ['openclaw', 'default'],
44
+ label: 'OpenClaw memory USearch',
45
+ dimension: DIM,
46
+ model: MODEL_ID,
47
+ modelVariant: MODEL_VARIANT,
48
+ modelSourceRepo: MODEL_SOURCE_REPO,
49
+ indexPath: join(HIP_DIR, 'usearch_bge_m3_vectors.index'),
50
+ metaPath: join(HIP_DIR, 'usearch_bge_m3_meta.jsonl'),
51
+ manifestPath: join(HIP_DIR, 'usearch_bge_m3_manifest.json'),
52
+ sourceKind: 'openclaw_qa_logger',
53
+ authorityTier: 'raw_search_evidence',
54
+ },
55
+ codex_qa_usearch: {
56
+ id: 'codex_qa_usearch',
57
+ aliases: ['codex_qa', 'codex'],
58
+ label: 'Codex QA diary USearch',
59
+ dimension: DIM,
60
+ model: MODEL_ID,
61
+ modelVariant: MODEL_VARIANT,
62
+ modelSourceRepo: MODEL_SOURCE_REPO,
63
+ indexPath: join(CODEX_QA_USEARCH_DIR, 'usearch_vectors.index'),
64
+ metaPath: join(CODEX_QA_USEARCH_DIR, 'usearch_meta.jsonl'),
65
+ manifestPath: join(CODEX_QA_USEARCH_DIR, 'manifest.json'),
66
+ sourceKind: 'codex_qa_diary',
67
+ authorityTier: 'codex_qa_archive',
68
+ },
69
+ });
70
+
71
+ const BACKEND_ALIASES = new Map();
72
+ for (const backend of Object.values(BACKENDS)) {
73
+ BACKEND_ALIASES.set(backend.id, backend.id);
74
+ for (const alias of backend.aliases || []) BACKEND_ALIASES.set(alias, backend.id);
75
+ }
76
+
77
+ const _states = new Map();
78
+
79
+ function intEnv(name, fallback) {
80
+ const value = Number(process.env[name]);
81
+ return Number.isFinite(value) && value > 0 ? value : fallback;
82
+ }
83
+
84
+ function stateFor(backend) {
85
+ if (!_states.has(backend.id)) {
86
+ _states.set(backend.id, {
87
+ idx: null,
88
+ metaMap: new Map(),
89
+ metaLoaded: false,
90
+ sourceSet: new Set(),
91
+ total: 0,
92
+ maxKey: 0,
93
+ keyCounter: Date.now(),
94
+ indexSignature: null,
95
+ metaSignature: null,
96
+ });
97
+ }
98
+ return _states.get(backend.id);
99
+ }
100
+
101
+ function normalizeOptions(value) {
102
+ if (typeof value === 'number') return { timeoutMs: value };
103
+ if (value && typeof value === 'object') return value;
104
+ return {};
105
+ }
106
+
107
+ function resolveBackend(input) {
108
+ const raw = (typeof input === 'string' ? input : input?.backend || input?.backendId || DEFAULT_BACKEND_ID).toString();
109
+ const backendId = BACKEND_ALIASES.get(raw) || raw;
110
+ const backend = BACKENDS[backendId];
111
+ if (!backend) throw new Error(`unknown USearch backend: ${raw}`);
112
+ return backend;
113
+ }
114
+
115
+ function ensureParent(filePath) {
116
+ mkdirSync(dirname(filePath), { recursive: true });
117
+ }
118
+
119
+ function compactText(text, limit = 2500) {
120
+ return String(text || '').replace(/\s+/g, ' ').trim().slice(0, limit);
121
+ }
122
+
123
+ function fileSignature(filePath) {
124
+ if (!existsSync(filePath)) return 'missing';
125
+ const info = statSync(filePath);
126
+ return `${info.size}:${Math.round(info.mtimeMs)}`;
127
+ }
128
+
129
+ function loadIndex(backend) {
130
+ const state = stateFor(backend);
131
+ const signature = fileSignature(backend.indexPath);
132
+ if (state.idx && state.indexSignature === signature) return state;
133
+ ensureParent(backend.indexPath);
134
+ state.idx = new Index({ metric: 'cos', dimensions: backend.dimension, connectivity: 16, expansionSearch: 64 });
135
+ if (existsSync(backend.indexPath) && statSync(backend.indexPath).size > 0) {
136
+ try {
137
+ state.idx.load(backend.indexPath);
138
+ } catch (error) {
139
+ console.error(`[usearch-bridge] ${backend.id} index load failed, using empty index: ${error.message}`);
140
+ }
141
+ }
142
+ state.total = Number(state.idx.size()) || 0;
143
+ state.indexSignature = signature;
144
+ return state;
145
+ }
146
+
147
+ async function loadMeta(backend) {
148
+ const state = stateFor(backend);
149
+ const signature = fileSignature(backend.metaPath);
150
+ if (state.metaLoaded && state.metaSignature === signature) return state;
151
+ state.metaMap = new Map();
152
+ state.sourceSet = new Set();
153
+ state.maxKey = 0;
154
+ if (!existsSync(backend.metaPath)) {
155
+ state.metaLoaded = true;
156
+ state.metaSignature = signature;
157
+ return state;
158
+ }
159
+ await new Promise((resolve, reject) => {
160
+ const rl = createInterface({ input: createReadStream(backend.metaPath, 'utf-8'), crlfDelay: Infinity });
161
+ rl.on('line', (line) => {
162
+ if (!line.trim()) return;
163
+ try {
164
+ const m = JSON.parse(line);
165
+ if (m.k === undefined) return;
166
+ const key = Number(m.k);
167
+ state.metaMap.set(key, {
168
+ text: m.t || m.text || '',
169
+ source: m.s || m.source || '',
170
+ sourceKind: m.source_kind || backend.sourceKind,
171
+ authorityTier: m.authority_tier || backend.authorityTier,
172
+ });
173
+ if (m.s || m.source) state.sourceSet.add(String(m.s || m.source));
174
+ if (Number.isFinite(key)) state.maxKey = Math.max(state.maxKey, key);
175
+ } catch (_) {
176
+ // Ignore malformed metadata lines.
177
+ }
178
+ });
179
+ rl.on('close', resolve);
180
+ rl.on('error', reject);
181
+ });
182
+ state.keyCounter = Math.max(state.keyCounter || 0, state.maxKey || 0, Date.now());
183
+ state.metaLoaded = true;
184
+ state.metaSignature = signature;
185
+ return state;
186
+ }
187
+
188
+ // Embedding process ----------------------------------------------------------
189
+ let _proc = null;
190
+ let _starting = null;
191
+ let _qid = 0;
192
+ let _pending = new Map();
193
+ let _buf = '';
194
+ let _ready = false;
195
+
196
+ function failPending(error) {
197
+ for (const pending of _pending.values()) {
198
+ clearTimeout(pending.timer);
199
+ pending.reject(error);
200
+ }
201
+ _pending.clear();
202
+ }
203
+
204
+ function ensureEmbedProc() {
205
+ if (_proc && _proc.exitCode === null && _ready) return Promise.resolve();
206
+ if (_starting) return _starting;
207
+ if (_proc) {
208
+ try { _proc.kill(); } catch (_) {}
209
+ _proc = null;
210
+ }
211
+ _ready = false;
212
+ _buf = '';
213
+ _proc = spawn(PYTHON_312, [EMBED_SCRIPT], {
214
+ stdio: ['pipe', 'pipe', 'pipe'],
215
+ windowsHide: true,
216
+ env: { ...process.env, HOME: USER_HOME, USERPROFILE: USER_HOME },
217
+ });
218
+
219
+ _proc.stdout.on('data', (chunk) => {
220
+ _buf += chunk.toString();
221
+ const lines = _buf.split('\n');
222
+ _buf = lines.pop() || '';
223
+ for (const line of lines) {
224
+ if (!line.trim()) continue;
225
+ let data;
226
+ try {
227
+ data = JSON.parse(line);
228
+ } catch (_) {
229
+ continue;
230
+ }
231
+ const id = String(data._queryId ?? data.id ?? '');
232
+ const pending = _pending.get(id);
233
+ if (!pending) continue;
234
+ clearTimeout(pending.timer);
235
+ _pending.delete(id);
236
+ if (data.error) pending.reject(new Error(data.error));
237
+ else pending.resolve(data.vectors || (data.vector ? [data.vector] : []));
238
+ }
239
+ });
240
+
241
+ _starting = new Promise((resolve, reject) => {
242
+ let settled = false;
243
+ const warmupTimeoutMs = intEnv('BGE_M3_EMBED_WARMUP_TIMEOUT_MS', 90000);
244
+ const timer = setTimeout(() => settle(new Error(`BGE-M3 embedding process warmup timeout after ${warmupTimeoutMs}ms`)), warmupTimeoutMs);
245
+ const settle = (error = null) => {
246
+ if (settled) return;
247
+ settled = true;
248
+ clearTimeout(timer);
249
+ _starting = null;
250
+ if (error) reject(error);
251
+ else resolve();
252
+ };
253
+
254
+ _proc.stderr.on('data', (chunk) => {
255
+ const text = chunk.toString();
256
+ if (text.includes('就绪') || text.includes('Ready')) {
257
+ _ready = true;
258
+ settle();
259
+ }
260
+ const trimmed = text.trim();
261
+ if (trimmed && !trimmed.includes('就绪')) console.error(`[bge-m3-embed] ${trimmed}`);
262
+ });
263
+ _proc.on('exit', (code) => {
264
+ _ready = false;
265
+ failPending(new Error(`BGE-M3 embedding process exited(code=${code})`));
266
+ if (!settled) settle(new Error(`BGE-M3 embedding process exited before ready(code=${code})`));
267
+ });
268
+ _proc.on('error', (error) => {
269
+ _ready = false;
270
+ failPending(error);
271
+ if (!settled) settle(error);
272
+ });
273
+ });
274
+ return _starting;
275
+ }
276
+
277
+ async function embedTexts(texts, timeoutMs = 300000) {
278
+ await ensureEmbedProc();
279
+ return new Promise((resolve, reject) => {
280
+ const id = String(++_qid);
281
+ const timer = setTimeout(() => {
282
+ _pending.delete(id);
283
+ reject(new Error('BGE-M3 embedding timeout'));
284
+ }, timeoutMs);
285
+ _pending.set(id, { resolve, reject, timer });
286
+ try {
287
+ _proc.stdin.write(JSON.stringify({ _queryId: id, texts }, null, 0) + '\n');
288
+ } catch (error) {
289
+ clearTimeout(timer);
290
+ _pending.delete(id);
291
+ reject(error);
292
+ }
293
+ }).then((vectors) => {
294
+ for (const vector of vectors) {
295
+ if (!Array.isArray(vector) || vector.length !== DIM) {
296
+ throw new Error(`BGE-M3 vector dimension mismatch: expected ${DIM}, got ${Array.isArray(vector) ? vector.length : 'non-array'}`);
297
+ }
298
+ }
299
+ return vectors;
300
+ });
301
+ }
302
+
303
+ // Query ----------------------------------------------------------------------
304
+ export async function qdrantQuery(query, topK = TOP_K, optionsOrTimeout = {}) {
305
+ if (!query) throw new Error('query is required');
306
+ const options = normalizeOptions(optionsOrTimeout);
307
+ const backend = resolveBackend(options);
308
+ const timeoutMs = Number(options.timeoutMs || options.timeout || 30000);
309
+ const limit = Math.max(1, Math.min(Number(topK || TOP_K), 100));
310
+ const t0 = performance.now();
311
+ const state = loadIndex(backend);
312
+ await loadMeta(backend);
313
+ const tMeta = performance.now();
314
+
315
+ if (state.total <= 0) {
316
+ return {
317
+ results: [],
318
+ total_in_index: 0,
319
+ elapsed_ms: Math.round(performance.now() - t0),
320
+ backend: backend.id,
321
+ dimension: backend.dimension,
322
+ model: backend.model,
323
+ model_variant: backend.modelVariant,
324
+ model_source_repo: backend.modelSourceRepo,
325
+ timing: { meta_ms: Math.round(tMeta - t0), embed_ms: 0, search_ms: 0 },
326
+ };
327
+ }
328
+
329
+ const [vector] = await embedTexts([query], timeoutMs);
330
+ const tEmbed = performance.now();
331
+ const matches = state.idx.search(new Float32Array(vector), limit);
332
+ const tSearch = performance.now();
333
+ const items = [];
334
+ for (let i = 0; i < matches.keys.length; i += 1) {
335
+ const key = Number(matches.keys[i]);
336
+ const distance = Number(matches.distances[i]);
337
+ const score = 1 - distance;
338
+ const meta = state.metaMap.get(key) || { text: '', source: '', sourceKind: backend.sourceKind, authorityTier: backend.authorityTier };
339
+ items.push({
340
+ score: Math.round(score * 10000) / 10000,
341
+ text: compactText(meta.text, 2500),
342
+ text_full_length: String(meta.text || '').length,
343
+ source: String(meta.source || '').slice(0, 500),
344
+ id: key,
345
+ backend: backend.id,
346
+ source_kind: meta.sourceKind || backend.sourceKind,
347
+ authority_tier: meta.authorityTier || backend.authorityTier,
348
+ needs_main_review: true,
349
+ });
350
+ }
351
+
352
+ return {
353
+ results: items,
354
+ total_in_index: state.total,
355
+ elapsed_ms: Math.round(performance.now() - t0),
356
+ backend: backend.id,
357
+ dimension: backend.dimension,
358
+ model: backend.model,
359
+ model_variant: backend.modelVariant,
360
+ model_source_repo: backend.modelSourceRepo,
361
+ timing: {
362
+ meta_ms: Math.round(tMeta - t0),
363
+ embed_ms: Math.round(tEmbed - tMeta),
364
+ search_ms: Math.round(tSearch - tEmbed),
365
+ },
366
+ };
367
+ }
368
+
369
+ function nextKey(state) {
370
+ state.keyCounter = Math.max(state.keyCounter || 0, state.maxKey || 0, Date.now()) + 1;
371
+ state.maxKey = Math.max(state.maxKey || 0, state.keyCounter);
372
+ return state.keyCounter;
373
+ }
374
+
375
+ function chunksFromFile(filePath) {
376
+ const raw = readFileSync(filePath, 'utf-8');
377
+ const records = [];
378
+ if (filePath.endsWith('.jsonl') || filePath.endsWith('.json')) {
379
+ for (const line of raw.split(/\r?\n/)) {
380
+ if (!line.trim()) continue;
381
+ try {
382
+ const obj = JSON.parse(line);
383
+ const text = obj.text || obj.content || obj.summary || line;
384
+ if (String(text).trim().length >= 10) records.push({ text: String(text), source: filePath });
385
+ } catch (_) {
386
+ if (line.trim().length >= 20) records.push({ text: line, source: filePath });
387
+ }
388
+ }
389
+ return records;
390
+ }
391
+
392
+ for (const para of raw.split(/\n{2,}/)) {
393
+ const trimmed = para.trim();
394
+ if (trimmed.length < 20) continue;
395
+ if (trimmed.length <= 1800) {
396
+ records.push({ text: trimmed, source: filePath });
397
+ continue;
398
+ }
399
+ for (let offset = 0; offset < trimmed.length; offset += 1500) {
400
+ const chunk = trimmed.slice(offset, offset + 1800).trim();
401
+ if (chunk.length >= 20) records.push({ text: chunk, source: filePath });
402
+ }
403
+ }
404
+ return records;
405
+ }
406
+
407
+ export async function qdrantBuild(files, optionsOrTimeout = {}) {
408
+ const options = normalizeOptions(optionsOrTimeout);
409
+ const backend = resolveBackend(options);
410
+ const timeoutMs = Number(options.timeoutMs || options.timeout || 900000);
411
+ if (!files || files.length === 0) {
412
+ return { status: 'ok', backend: backend.id, added: 0, total: stateFor(backend).total || 0, elapsed_ms: 0, note: 'no files' };
413
+ }
414
+
415
+ const t0 = performance.now();
416
+ const state = loadIndex(backend);
417
+ await loadMeta(backend);
418
+ ensureParent(backend.metaPath);
419
+
420
+ const newRecords = [];
421
+ for (const rawFile of files) {
422
+ const filePath = String(rawFile || '').trim();
423
+ if (!filePath || !existsSync(filePath) || state.sourceSet.has(filePath)) continue;
424
+ try {
425
+ newRecords.push(...chunksFromFile(filePath));
426
+ } catch (error) {
427
+ console.error(`[usearch-bridge] ${backend.id} read failed: ${filePath}: ${error.message}`);
428
+ }
429
+ }
430
+
431
+ let added = 0;
432
+ for (let offset = 0; offset < newRecords.length; offset += BATCH_SIZE) {
433
+ const batch = newRecords.slice(offset, offset + BATCH_SIZE);
434
+ const vectors = await embedTexts(batch.map((record) => record.text), timeoutMs);
435
+ for (let i = 0; i < batch.length; i += 1) {
436
+ const key = nextKey(state);
437
+ const text = compactText(batch[i].text, 2500);
438
+ const source = String(batch[i].source || '').slice(0, 500);
439
+ state.idx.add(BigInt(key), new Float32Array(vectors[i]));
440
+ state.metaMap.set(key, { text, source, sourceKind: backend.sourceKind, authorityTier: backend.authorityTier });
441
+ state.sourceSet.add(source);
442
+ appendFileSync(backend.metaPath, JSON.stringify({
443
+ k: key,
444
+ t: text,
445
+ s: source,
446
+ source_kind: backend.sourceKind,
447
+ authority_tier: backend.authorityTier,
448
+ }, null, 0) + '\n', 'utf-8');
449
+ added += 1;
450
+ }
451
+ }
452
+
453
+ if (added > 0) {
454
+ state.idx.save(backend.indexPath);
455
+ state.total = Number(state.idx.size()) || 0;
456
+ writeManifest(backend, { mode: 'incremental', added, total: state.total, source_files: files.length });
457
+ }
458
+
459
+ return {
460
+ status: 'ok',
461
+ backend: backend.id,
462
+ added,
463
+ total: state.total,
464
+ dimension: backend.dimension,
465
+ model: backend.model,
466
+ model_variant: backend.modelVariant,
467
+ model_source_repo: backend.modelSourceRepo,
468
+ elapsed_ms: Math.round(performance.now() - t0),
469
+ };
470
+ }
471
+
472
+ function writeManifest(backend, extra) {
473
+ ensureParent(backend.manifestPath);
474
+ writeFileSync(backend.manifestPath, JSON.stringify({
475
+ schema_version: 1,
476
+ generated_at: new Date().toISOString(),
477
+ backend: backend.id,
478
+ model: backend.model,
479
+ model_variant: backend.modelVariant,
480
+ model_source_repo: backend.modelSourceRepo,
481
+ dimension: backend.dimension,
482
+ index_path: backend.indexPath,
483
+ meta_path: backend.metaPath,
484
+ ...extra,
485
+ }, null, 2), 'utf-8');
486
+ }
487
+
488
+ function validateTmpIndex(filePath, expectedSize, dimension) {
489
+ if (expectedSize <= 0) return;
490
+ const probe = new Index({ metric: 'cos', dimensions: dimension, connectivity: 16, expansionSearch: 64 });
491
+ probe.load(filePath);
492
+ const actualSize = Number(probe.size()) || 0;
493
+ if (actualSize !== expectedSize) {
494
+ throw new Error(`rebuilt index size mismatch: expected ${expectedSize}, got ${actualSize}`);
495
+ }
496
+ }
497
+
498
+ function replaceFileWithBackup(tmpPath, targetPath, backupPath) {
499
+ const hadTarget = existsSync(targetPath);
500
+ if (hadTarget) copyFileSync(targetPath, backupPath);
501
+ try {
502
+ rmSync(targetPath, { force: true });
503
+ renameSync(tmpPath, targetPath);
504
+ } catch (error) {
505
+ try {
506
+ if (hadTarget && existsSync(backupPath) && !existsSync(targetPath)) {
507
+ copyFileSync(backupPath, targetPath);
508
+ }
509
+ } catch (_) {
510
+ // Preserve the original error; restoration is best-effort.
511
+ }
512
+ throw error;
513
+ }
514
+ }
515
+
516
+ export async function rebuildBackendFromRecords(backendInput, records, options = {}) {
517
+ const backend = resolveBackend(backendInput);
518
+ const timeoutMs = Number(options.timeoutMs || options.timeout || 900000);
519
+ const t0 = performance.now();
520
+ ensureParent(backend.indexPath);
521
+ ensureParent(backend.metaPath);
522
+
523
+ const tmpIndex = `${backend.indexPath}.tmp-${process.pid}`;
524
+ const tmpMeta = `${backend.metaPath}.tmp-${process.pid}`;
525
+ const backupStamp = new Date().toISOString().replace(/[:.]/g, '-');
526
+ const backupIndex = `${backend.indexPath}.bak-${backupStamp}`;
527
+ const backupMeta = `${backend.metaPath}.bak-${backupStamp}`;
528
+ rmSync(tmpIndex, { force: true });
529
+ rmSync(tmpMeta, { force: true });
530
+
531
+ const idx = new Index({ metric: 'cos', dimensions: backend.dimension, connectivity: 16, expansionSearch: 64 });
532
+ const metaStream = createWriteStream(tmpMeta, { encoding: 'utf-8' });
533
+ let added = 0;
534
+
535
+ for (let offset = 0; offset < records.length; offset += BATCH_SIZE) {
536
+ const batch = records.slice(offset, offset + BATCH_SIZE)
537
+ .map((record) => ({
538
+ text: String(record.text || record.content || '').trim(),
539
+ source: String(record.source || record.path || '').slice(0, 500),
540
+ chunk_index: record.chunk_index,
541
+ }))
542
+ .filter((record) => record.text.length >= 10);
543
+ if (batch.length === 0) continue;
544
+
545
+ const vectors = await embedTexts(batch.map((record) => record.text), timeoutMs);
546
+ for (let i = 0; i < batch.length; i += 1) {
547
+ const key = added + 1;
548
+ const text = compactText(batch[i].text, 2500);
549
+ idx.add(BigInt(key), new Float32Array(vectors[i]));
550
+ metaStream.write(JSON.stringify({
551
+ k: key,
552
+ t: text,
553
+ s: batch[i].source,
554
+ chunk_index: batch[i].chunk_index,
555
+ source_kind: backend.sourceKind,
556
+ authority_tier: backend.authorityTier,
557
+ }, null, 0) + '\n');
558
+ added += 1;
559
+ }
560
+ }
561
+
562
+ await new Promise((resolve, reject) => {
563
+ metaStream.on('error', reject);
564
+ metaStream.end(resolve);
565
+ });
566
+
567
+ if (added > 0) idx.save(tmpIndex);
568
+ else writeFileSync(tmpIndex, '', 'utf-8');
569
+ validateTmpIndex(tmpIndex, added, backend.dimension);
570
+ replaceFileWithBackup(tmpMeta, backend.metaPath, backupMeta);
571
+ if (added > 0) replaceFileWithBackup(tmpIndex, backend.indexPath, backupIndex);
572
+ else rmSync(tmpIndex, { force: true });
573
+ writeManifest(backend, {
574
+ mode: options.mode || 'full',
575
+ source_mode: options.sourceMode || 'records',
576
+ source_files: options.sourceFiles || null,
577
+ source_root: options.sourceRoot || null,
578
+ source_fingerprint: options.sourceFingerprint || null,
579
+ chunks: added,
580
+ total: added,
581
+ });
582
+
583
+ _states.delete(backend.id);
584
+ const state = loadIndex(backend);
585
+ await loadMeta(backend);
586
+ return {
587
+ status: 'ok',
588
+ backend: backend.id,
589
+ chunks: added,
590
+ total: state.total,
591
+ dimension: backend.dimension,
592
+ model: backend.model,
593
+ model_variant: backend.modelVariant,
594
+ model_source_repo: backend.modelSourceRepo,
595
+ elapsed_ms: Math.round(performance.now() - t0),
596
+ };
597
+ }
598
+
599
+ function backendStatus(backend) {
600
+ const state = _states.get(backend.id);
601
+ const indexExists = existsSync(backend.indexPath) && statSync(backend.indexPath).size > 0;
602
+ const metaExists = existsSync(backend.metaPath) && statSync(backend.metaPath).size > 0;
603
+ return {
604
+ id: backend.id,
605
+ label: backend.label,
606
+ default: backend.id === DEFAULT_BACKEND_ID,
607
+ dimension: backend.dimension,
608
+ model: backend.model,
609
+ model_variant: backend.modelVariant,
610
+ model_source_repo: backend.modelSourceRepo,
611
+ source_kind: backend.sourceKind,
612
+ authority_tier: backend.authorityTier,
613
+ index_path: backend.indexPath,
614
+ meta_path: backend.metaPath,
615
+ index_exists: indexExists,
616
+ meta_exists: metaExists,
617
+ loaded: Boolean(state?.idx),
618
+ total_vectors: state?.total ?? null,
619
+ status: indexExists && metaExists ? 'ok' : 'missing',
620
+ };
621
+ }
622
+
623
+ export function getBackendRegistry() {
624
+ return Object.values(BACKENDS).map((backend) => ({ ...backend }));
625
+ }
626
+
627
+ export function listBackends() {
628
+ return Object.values(BACKENDS).map(backendStatus);
629
+ }
630
+
631
+ export function stopEmbedProcess() {
632
+ try { if (_proc) _proc.kill(); } catch (_) {}
633
+ _proc = null;
634
+ _ready = false;
635
+ }
636
+
637
+ process.on('exit', () => stopEmbedProcess());
638
+ process.on('SIGTERM', () => { stopEmbedProcess(); process.exit(0); });
639
+ process.on('SIGINT', () => { stopEmbedProcess(); process.exit(0); });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * usearch-http.js - HTTP client for the multi-backend USearch service.
3
+ *
4
+ * Compatibility:
5
+ * - qdrantSearch(query, topK, timeoutMs) defaults to OpenClaw.
6
+ * - qdrantSearch(query, topK, { backend: "codex_qa_usearch" }) is supported.
7
+ */
8
+
9
+ const URL = process.env.USEARCH_SERVICE_URL || 'http://127.0.0.1:18793';
10
+
11
+ function splitTimeoutAndOptions(timeoutOrOptions, maybeOptions) {
12
+ if (timeoutOrOptions && typeof timeoutOrOptions === 'object') {
13
+ return {
14
+ timeoutMs: Number(timeoutOrOptions.timeoutMs || timeoutOrOptions.timeout || 30000),
15
+ options: timeoutOrOptions,
16
+ };
17
+ }
18
+ return {
19
+ timeoutMs: Number(timeoutOrOptions || 30000),
20
+ options: maybeOptions || {},
21
+ };
22
+ }
23
+
24
+ export async function qdrantSearch(query, topK = 10, timeoutOrOptions = 30000, maybeOptions = {}) {
25
+ if (!query) throw new Error('query 必填');
26
+ const { timeoutMs, options } = splitTimeoutAndOptions(timeoutOrOptions, maybeOptions);
27
+ const ctrl = new AbortController();
28
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
29
+ const body = { query, topK };
30
+ if (options.backend) body.backend = options.backend;
31
+
32
+ try {
33
+ const r = await fetch(`${URL}/query`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify(body),
37
+ signal: ctrl.signal,
38
+ });
39
+ clearTimeout(timer);
40
+ if (!r.ok) {
41
+ const err = await r.json().catch(() => ({}));
42
+ throw new Error(err.error || `HTTP ${r.status}`);
43
+ }
44
+ return await r.json();
45
+ } finally {
46
+ clearTimeout(timer);
47
+ }
48
+ }
49
+
50
+ export async function qdrantBuild(files, timeoutOrOptions = 600000, maybeOptions = {}) {
51
+ if (!files || files.length === 0) throw new Error('files 必填');
52
+ const { timeoutMs, options } = splitTimeoutAndOptions(timeoutOrOptions, maybeOptions);
53
+ const ctrl = new AbortController();
54
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
55
+ const body = { files };
56
+ if (options.backend) body.backend = options.backend;
57
+
58
+ try {
59
+ const r = await fetch(`${URL}/incremental`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify(body),
63
+ signal: ctrl.signal,
64
+ });
65
+ clearTimeout(timer);
66
+ if (!r.ok) {
67
+ const err = await r.json().catch(() => ({}));
68
+ throw new Error(err.error || `HTTP ${r.status}`);
69
+ }
70
+ return await r.json();
71
+ } finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }