crbro-memory 2.9.1 → 2.11.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 (197) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -456
  3. package/SECURITY.md +135 -8
  4. package/bin/crbro.mjs +555 -299
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +143 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +762 -311
  25. package/dist/engine/doctor.js +676 -0
  26. package/dist/engine/evidence.js +171 -0
  27. package/dist/engine/heat.js +81 -37
  28. package/dist/engine/hippocampus.js +50 -36
  29. package/dist/engine/hookrefresh.js +296 -0
  30. package/dist/engine/hookverify.js +15 -3
  31. package/dist/engine/installer.js +1022 -0
  32. package/dist/engine/installio.js +279 -0
  33. package/dist/engine/keychain.js +735 -144
  34. package/dist/engine/maintenance.js +355 -87
  35. package/dist/engine/modinstall.js +98 -45
  36. package/dist/engine/prefrontal.js +126 -69
  37. package/dist/engine/projects.js +80 -0
  38. package/dist/engine/quarantine.js +204 -0
  39. package/dist/engine/secrets.js +41 -5
  40. package/dist/engine/shelf.js +34 -11
  41. package/dist/engine/synapses.js +183 -80
  42. package/dist/engine/triggers.js +141 -11
  43. package/dist/index.js +37 -3
  44. package/dist/mcp-surface.json +1 -0
  45. package/dist/miner/extractor.js +64 -17
  46. package/dist/miner/scheduler.js +144 -68
  47. package/dist/search/index.js +810 -176
  48. package/dist/search/quality.js +31 -0
  49. package/dist/search/semantic.js +208 -18
  50. package/dist/search/tokenize.js +44 -0
  51. package/dist/server.js +649 -343
  52. package/dist/sync/git.js +203 -72
  53. package/dist/sync/materialize.js +33 -8
  54. package/dist/sync/space.js +286 -101
  55. package/dist/sync/validate.js +204 -0
  56. package/dist/utils/budget.js +15 -2
  57. package/dist/utils/fs.js +40 -6
  58. package/dist/utils/ids.js +128 -14
  59. package/dist/utils/lock.js +219 -52
  60. package/hooks/crbro-codex.mjs +284 -0
  61. package/hooks/crbro-guard.mjs +281 -173
  62. package/hooks/crbro-hooks.shipped.json +20 -0
  63. package/hooks/crbro-lifecycle.mjs +16 -4
  64. package/hooks/crbro-subagent.mjs +180 -180
  65. package/package.json +12 -8
  66. package/dist/daemon/daemon.d.ts +0 -25
  67. package/dist/daemon/daemon.d.ts.map +0 -1
  68. package/dist/daemon/daemon.js.map +0 -1
  69. package/dist/daemon/endpoint.d.ts +0 -66
  70. package/dist/daemon/endpoint.d.ts.map +0 -1
  71. package/dist/daemon/endpoint.js.map +0 -1
  72. package/dist/daemon/lines.d.ts +0 -55
  73. package/dist/daemon/lines.d.ts.map +0 -1
  74. package/dist/daemon/lines.js.map +0 -1
  75. package/dist/daemon/proxy.d.ts +0 -55
  76. package/dist/daemon/proxy.d.ts.map +0 -1
  77. package/dist/daemon/proxy.js.map +0 -1
  78. package/dist/engine/backup.d.ts +0 -68
  79. package/dist/engine/backup.d.ts.map +0 -1
  80. package/dist/engine/backup.js.map +0 -1
  81. package/dist/engine/brain.d.ts +0 -79
  82. package/dist/engine/brain.d.ts.map +0 -1
  83. package/dist/engine/brain.js.map +0 -1
  84. package/dist/engine/cortex.d.ts +0 -670
  85. package/dist/engine/cortex.d.ts.map +0 -1
  86. package/dist/engine/cortex.js.map +0 -1
  87. package/dist/engine/dates.d.ts +0 -28
  88. package/dist/engine/dates.d.ts.map +0 -1
  89. package/dist/engine/dates.js.map +0 -1
  90. package/dist/engine/heat.d.ts +0 -28
  91. package/dist/engine/heat.d.ts.map +0 -1
  92. package/dist/engine/heat.js.map +0 -1
  93. package/dist/engine/hippocampus.d.ts +0 -60
  94. package/dist/engine/hippocampus.d.ts.map +0 -1
  95. package/dist/engine/hippocampus.js.map +0 -1
  96. package/dist/engine/hookverify.d.ts +0 -42
  97. package/dist/engine/hookverify.d.ts.map +0 -1
  98. package/dist/engine/hookverify.js.map +0 -1
  99. package/dist/engine/keychain.d.ts +0 -32
  100. package/dist/engine/keychain.d.ts.map +0 -1
  101. package/dist/engine/keychain.js.map +0 -1
  102. package/dist/engine/maintenance.d.ts +0 -218
  103. package/dist/engine/maintenance.d.ts.map +0 -1
  104. package/dist/engine/maintenance.js.map +0 -1
  105. package/dist/engine/modinstall.d.ts +0 -215
  106. package/dist/engine/modinstall.d.ts.map +0 -1
  107. package/dist/engine/modinstall.js.map +0 -1
  108. package/dist/engine/postmortem.d.ts +0 -81
  109. package/dist/engine/postmortem.d.ts.map +0 -1
  110. package/dist/engine/postmortem.js.map +0 -1
  111. package/dist/engine/prefrontal.d.ts +0 -61
  112. package/dist/engine/prefrontal.d.ts.map +0 -1
  113. package/dist/engine/prefrontal.js.map +0 -1
  114. package/dist/engine/secrets.d.ts +0 -30
  115. package/dist/engine/secrets.d.ts.map +0 -1
  116. package/dist/engine/secrets.js.map +0 -1
  117. package/dist/engine/shelf.d.ts +0 -156
  118. package/dist/engine/shelf.d.ts.map +0 -1
  119. package/dist/engine/shelf.js.map +0 -1
  120. package/dist/engine/source.d.ts +0 -6
  121. package/dist/engine/source.d.ts.map +0 -1
  122. package/dist/engine/source.js.map +0 -1
  123. package/dist/engine/synapses.d.ts +0 -82
  124. package/dist/engine/synapses.d.ts.map +0 -1
  125. package/dist/engine/synapses.js.map +0 -1
  126. package/dist/engine/triggers.d.ts +0 -53
  127. package/dist/engine/triggers.d.ts.map +0 -1
  128. package/dist/engine/triggers.js.map +0 -1
  129. package/dist/engine/usage.d.ts +0 -61
  130. package/dist/engine/usage.d.ts.map +0 -1
  131. package/dist/engine/usage.js.map +0 -1
  132. package/dist/index.d.ts +0 -2
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/miner/extractor.d.ts +0 -12
  136. package/dist/miner/extractor.d.ts.map +0 -1
  137. package/dist/miner/extractor.js.map +0 -1
  138. package/dist/miner/index.d.ts +0 -61
  139. package/dist/miner/index.d.ts.map +0 -1
  140. package/dist/miner/index.js.map +0 -1
  141. package/dist/miner/scheduler.d.ts +0 -30
  142. package/dist/miner/scheduler.d.ts.map +0 -1
  143. package/dist/miner/scheduler.js.map +0 -1
  144. package/dist/miner/types.d.ts +0 -11
  145. package/dist/miner/types.d.ts.map +0 -1
  146. package/dist/miner/types.js.map +0 -1
  147. package/dist/search/index.d.ts +0 -323
  148. package/dist/search/index.d.ts.map +0 -1
  149. package/dist/search/index.js.map +0 -1
  150. package/dist/search/semantic.d.ts +0 -80
  151. package/dist/search/semantic.d.ts.map +0 -1
  152. package/dist/search/semantic.js.map +0 -1
  153. package/dist/search/synonyms.d.ts +0 -2
  154. package/dist/search/synonyms.d.ts.map +0 -1
  155. package/dist/search/synonyms.js.map +0 -1
  156. package/dist/search/tokenize.d.ts +0 -39
  157. package/dist/search/tokenize.d.ts.map +0 -1
  158. package/dist/search/tokenize.js.map +0 -1
  159. package/dist/server.d.ts +0 -41
  160. package/dist/server.d.ts.map +0 -1
  161. package/dist/server.js.map +0 -1
  162. package/dist/sync/git.d.ts +0 -36
  163. package/dist/sync/git.d.ts.map +0 -1
  164. package/dist/sync/git.js.map +0 -1
  165. package/dist/sync/materialize.d.ts +0 -35
  166. package/dist/sync/materialize.d.ts.map +0 -1
  167. package/dist/sync/materialize.js.map +0 -1
  168. package/dist/sync/ops.d.ts +0 -142
  169. package/dist/sync/ops.d.ts.map +0 -1
  170. package/dist/sync/ops.js.map +0 -1
  171. package/dist/sync/space.d.ts +0 -116
  172. package/dist/sync/space.d.ts.map +0 -1
  173. package/dist/sync/space.js.map +0 -1
  174. package/dist/types/index.d.ts +0 -346
  175. package/dist/types/index.d.ts.map +0 -1
  176. package/dist/types/index.js.map +0 -1
  177. package/dist/utils/budget.d.ts +0 -44
  178. package/dist/utils/budget.d.ts.map +0 -1
  179. package/dist/utils/budget.js.map +0 -1
  180. package/dist/utils/fs.d.ts +0 -59
  181. package/dist/utils/fs.d.ts.map +0 -1
  182. package/dist/utils/fs.js.map +0 -1
  183. package/dist/utils/hash.d.ts +0 -20
  184. package/dist/utils/hash.d.ts.map +0 -1
  185. package/dist/utils/hash.js.map +0 -1
  186. package/dist/utils/ids.d.ts +0 -47
  187. package/dist/utils/ids.d.ts.map +0 -1
  188. package/dist/utils/ids.js.map +0 -1
  189. package/dist/utils/lock.d.ts +0 -11
  190. package/dist/utils/lock.d.ts.map +0 -1
  191. package/dist/utils/lock.js.map +0 -1
  192. package/dist/utils/transcripts.d.ts +0 -63
  193. package/dist/utils/transcripts.d.ts.map +0 -1
  194. package/dist/utils/transcripts.js.map +0 -1
  195. package/dist/version.d.ts +0 -26
  196. package/dist/version.d.ts.map +0 -1
  197. package/dist/version.js.map +0 -1
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.entryQuality = entryQuality;
4
+ const shelf_js_1 = require("../engine/shelf.js");
5
+ const evidence_js_1 = require("../engine/evidence.js");
6
+ const hash_js_1 = require("../utils/hash.js");
7
+ /** Etiquetas epistemológicas, no otra puntuación ni una afirmación de verdad. */
8
+ function entryQuality(neuron, id, kind, text, stale) {
9
+ const fact = kind === 'fact' ? neuron?.facts?.find(f => (f.id || (0, hash_js_1.factId)(f.text)) === id || f.text === text) : undefined;
10
+ const check = (0, evidence_js_1.mergeCheck)(fact || {
11
+ verified: id ? neuron?.entry_verified?.[id] : undefined,
12
+ verification: id ? neuron?.entry_verification?.[id] : undefined,
13
+ }, {});
14
+ const verification = check.verification;
15
+ const legacyDate = check.verified;
16
+ const method = verification?.method || (legacyDate ? 'legacy' : 'none');
17
+ const historical = fact?.temporal === 'historical' || (fact?.temporal !== 'current' &&
18
+ ((fact?.source === 'miner' && !verification) || (0, shelf_js_1.isDatedRecord)(text)));
19
+ const reference = (0, evidence_js_1.readReference)(fact?.reference) || verification?.reference;
20
+ return {
21
+ temporal: historical ? 'historical' : 'current_claim',
22
+ verification: verification || { method, ...(legacyDate ? { at: legacyDate } : {}) },
23
+ freshness: {
24
+ state: historical ? 'historical' : stale?.stale ? 'stale' : !legacyDate && !verification ? 'unverified'
25
+ : verification && verification.method !== 'unspecified' ? 'within_window' : 'unattributed_check',
26
+ ...(stale?.age_days !== undefined ? { age_days: stale.age_days } : {}),
27
+ },
28
+ ...(reference ? { reference } : {}),
29
+ };
30
+ }
31
+ //# sourceMappingURL=quality.js.map
@@ -32,7 +32,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
32
32
  return (mod && mod.__esModule) ? mod : { "default": mod };
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.SemanticIndex = exports.SEMANTIC_DTYPE_DEFAULT = exports.SEMANTIC_MODEL_DEFAULT = void 0;
35
+ exports.SemanticIndex = exports.SEMANTIC_MODEL_FILES = exports.SEMANTIC_RUNTIME = exports.SEMANTIC_DTYPE_DEFAULT = exports.SEMANTIC_MODEL_DEFAULT = void 0;
36
+ exports.verifyModelFiles = verifyModelFiles;
36
37
  exports.semanticModel = semanticModel;
37
38
  exports.semanticDtype = semanticDtype;
38
39
  exports.semanticEnabled = semanticEnabled;
@@ -43,6 +44,7 @@ exports.semanticStatus = semanticStatus;
43
44
  const node_module_1 = require("node:module");
44
45
  const node_url_1 = require("node:url");
45
46
  const node_fs_1 = require("node:fs");
47
+ const node_crypto_1 = require("node:crypto");
46
48
  const node_path_1 = __importDefault(require("node:path"));
47
49
  const node_os_1 = __importDefault(require("node:os"));
48
50
  /**
@@ -54,6 +56,81 @@ const node_os_1 = __importDefault(require("node:os"));
54
56
  */
55
57
  exports.SEMANTIC_MODEL_DEFAULT = 'Xenova/multilingual-e5-small';
56
58
  exports.SEMANTIC_DTYPE_DEFAULT = 'q8';
59
+ /**
60
+ * The runtime `semantic install` puts in the semantic home, pinned to an
61
+ * exact version (2.11; it was the floating range `@3`, so every install could
62
+ * pull a different release of a package that runs install scripts).
63
+ */
64
+ exports.SEMANTIC_RUNTIME = '@huggingface/transformers@3.8.1';
65
+ /**
66
+ * The default model, pinned by content (2.11). The Hub revision is a moving
67
+ * `main`; these are the SHA-256 of the files the benchmarks were measured
68
+ * with (downloaded 2026-09-03). A model whose files differ is refused, never
69
+ * used: the layer reports why and recall stays keyword-only. Only checked for
70
+ * the default model and dtype; CRBRO_SEMANTIC_MODEL users pick their own.
71
+ */
72
+ exports.SEMANTIC_MODEL_FILES = {
73
+ 'config.json': { sha256: 'cb99455288675345e1a4f411438d5d0adbba5fbd3a67ea4fb03c015433b996c1', bytes: 658 },
74
+ 'tokenizer.json': { sha256: '0b44a9d7b51c3c62626640cda0e2c2f70fdacdc25bbbd68038369d14ebdf4c39', bytes: 17_082_730 },
75
+ 'tokenizer_config.json': { sha256: 'a1d6bc8734a6f635dc158508bef000f8e2e5a759c7d92f984b2c86e5ff53425b', bytes: 443 },
76
+ 'onnx/model_quantized.onnx': { sha256: 'f80102d3f2a1229f387d3c81909990d8945513e347b0eab049f7de3c6f98c193', bytes: 118_308_185 },
77
+ };
78
+ async function sha256Of(file) {
79
+ const h = (0, node_crypto_1.createHash)('sha256');
80
+ await new Promise((resolve, reject) => {
81
+ (0, node_fs_1.createReadStream)(file).on('data', d => h.update(d)).on('end', () => resolve()).on('error', reject);
82
+ });
83
+ return h.digest('hex');
84
+ }
85
+ /**
86
+ * Check the cached files of the default model against SEMANTIC_MODEL_FILES.
87
+ * Returns null when they match (or the model is not the default one), else
88
+ * what is wrong.
89
+ */
90
+ async function verifyModelFiles(home = semanticHome()) {
91
+ if (semanticModel() !== exports.SEMANTIC_MODEL_DEFAULT || semanticDtype() !== exports.SEMANTIC_DTYPE_DEFAULT)
92
+ return null;
93
+ const dir = node_path_1.default.join(home, 'models', ...exports.SEMANTIC_MODEL_DEFAULT.split('/'));
94
+ // Each file is hashed once per version of it: one whose size and date are
95
+ // those of the last successful check is not read again. Hashing the 135 MB
96
+ // cost ~140 ms at every process start, before the first semantic answer.
97
+ const marca = node_path_1.default.join(dir, VERIFIED_FILE);
98
+ let vistos = {};
99
+ try {
100
+ vistos = JSON.parse(await node_fs_1.promises.readFile(marca, 'utf8')) || {};
101
+ }
102
+ catch { /* first check */ }
103
+ let nuevos = false;
104
+ for (const [rel, want] of Object.entries(exports.SEMANTIC_MODEL_FILES)) {
105
+ const file = node_path_1.default.join(dir, ...rel.split('/'));
106
+ let st;
107
+ try {
108
+ st = await node_fs_1.promises.stat(file);
109
+ }
110
+ catch {
111
+ return `model file missing: ${rel} (run \`npx crbro-memory semantic install\`)`;
112
+ }
113
+ const mismatch = `model file ${rel} does not match the pinned hash; refusing to use it (delete ${dir} and run \`npx crbro-memory semantic install\`)`;
114
+ if (st.size !== want.bytes)
115
+ return mismatch;
116
+ const v = vistos[rel];
117
+ if (v && v.size === st.size && v.mtime === st.mtimeMs && v.sha256 === want.sha256)
118
+ continue;
119
+ if ((await sha256Of(file)) !== want.sha256)
120
+ return mismatch;
121
+ vistos[rel] = { size: st.size, mtime: st.mtimeMs, sha256: want.sha256 };
122
+ nuevos = true;
123
+ }
124
+ if (nuevos) {
125
+ try {
126
+ await node_fs_1.promises.writeFile(marca, JSON.stringify(vistos));
127
+ }
128
+ catch { /* checked again next time */ }
129
+ }
130
+ return null;
131
+ }
132
+ /** Where verifyModelFiles remembers the files it already hashed. */
133
+ const VERIFIED_FILE = '.crbro-verified.json';
57
134
  function semanticModel() { return process.env.CRBRO_SEMANTIC_MODEL || exports.SEMANTIC_MODEL_DEFAULT; }
58
135
  function semanticDtype() { return process.env.CRBRO_SEMANTIC_DTYPE || exports.SEMANTIC_DTYPE_DEFAULT; }
59
136
  /**
@@ -75,6 +152,10 @@ const QUERY_PREFIX = 'query: ';
75
152
  const PASSAGE_PREFIX = 'passage: ';
76
153
  /** Longer passages are truncated by the tokenizer anyway; cap the work. */
77
154
  const MAX_CHARS = 2000;
155
+ /** A background pass is committed in slices of this many passages (2.11). */
156
+ const UPSERT_SLICE = 64;
157
+ /** ...and saved this often while it runs, so a process that leaves keeps what it did. */
158
+ const UPSERT_SAVE_MS = 30_000;
78
159
  /** Off when CRBRO_SEMANTIC says so, forced on when it says so, else on exactly when the runtime is installed. */
79
160
  function semanticEnabled() {
80
161
  const mode = semanticMode();
@@ -118,6 +199,7 @@ function semanticStatus() {
118
199
  }
119
200
  class SemanticIndex {
120
201
  dir;
202
+ options;
121
203
  ids = [];
122
204
  /** Vector width, read from the first embedding (384 for e5-small). */
123
205
  dim = 0;
@@ -130,8 +212,48 @@ class SemanticIndex {
130
212
  broken = null;
131
213
  /** Upserts run one at a time: two overlapping ones would both extend the same array. */
132
214
  queue = Promise.resolve();
133
- constructor(dir) {
215
+ /** Bumped on every change of the vectors, so a write knows whether it caught up. */
216
+ version = 0;
217
+ /**
218
+ * Queries waiting for the model (2.11). A background pass (thousands of
219
+ * passages after an upgrade split long entries into parts) pauses between
220
+ * passages while any is waiting: a recall used to queue behind it, ~2 s
221
+ * each, for the minutes the pass lasted.
222
+ */
223
+ foreground = 0;
224
+ foregroundWaiters = [];
225
+ /** Passages a running pass still has to embed. */
226
+ left = 0;
227
+ /** A large pass is running: more than one slice of passages still to embed. */
228
+ backfilling() { return this.left > UPSERT_SLICE; }
229
+ /**
230
+ * Something in the foreground (a search, a query) is running: background
231
+ * passages wait until every hold is released. Returns the release.
232
+ */
233
+ hold() {
234
+ this.foreground++;
235
+ let released = false;
236
+ return () => {
237
+ if (released)
238
+ return;
239
+ released = true;
240
+ this.foreground--;
241
+ if (this.foreground === 0) {
242
+ const w = this.foregroundWaiters;
243
+ this.foregroundWaiters = [];
244
+ for (const f of w)
245
+ f();
246
+ }
247
+ };
248
+ }
249
+ /**
250
+ * allowDownload: only `semantic install` may fetch the model from the Hub
251
+ * (2.11). A server never downloads during a query: a missing model makes
252
+ * the layer report why and recall stays keyword-only.
253
+ */
254
+ constructor(dir, options = {}) {
134
255
  this.dir = dir;
256
+ this.options = options;
135
257
  }
136
258
  metaPath() { return node_path_1.default.join(this.dir, 'vectors.meta.json'); }
137
259
  dataPath() { return node_path_1.default.join(this.dir, 'vectors.f32'); }
@@ -153,11 +275,14 @@ class SemanticIndex {
153
275
  async load() {
154
276
  try {
155
277
  const meta = JSON.parse(await node_fs_1.promises.readFile(this.metaPath(), 'utf8'));
156
- if (meta.model !== semanticModel() || !(meta.dim > 0))
278
+ // Vectors of another model or another quantisation are not comparable.
279
+ if (meta.model !== semanticModel() || (meta.dtype && meta.dtype !== semanticDtype()) || !(meta.dim > 0))
157
280
  return;
158
281
  const buf = await node_fs_1.promises.readFile(this.dataPath());
159
282
  const expected = meta.ids.length * meta.dim * 4;
160
- if (buf.byteLength !== expected)
283
+ // A torn pair (a crash between the two renames) does not match: start
284
+ // empty and let the index refill what is missing (2.11).
285
+ if (buf.byteLength !== expected || (meta.bytes !== undefined && meta.bytes !== buf.byteLength))
161
286
  return;
162
287
  this.vectors = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
163
288
  this.ids = meta.ids;
@@ -171,15 +296,20 @@ class SemanticIndex {
171
296
  async persist() {
172
297
  if (!this.dirty)
173
298
  return;
299
+ // What is written is this moment's pair; a vector added while the files
300
+ // are written keeps the layer dirty for the next write.
301
+ const version = this.version;
302
+ const vectors = this.vectors;
303
+ const meta = { model: semanticModel(), dtype: semanticDtype(), dim: this.dim, ids: [...this.ids], bytes: vectors.byteLength };
174
304
  await node_fs_1.promises.mkdir(this.dir, { recursive: true });
175
- const meta = { model: semanticModel(), dtype: semanticDtype(), dim: this.dim, ids: this.ids };
176
305
  const tmpMeta = this.metaPath() + '.tmp';
177
306
  const tmpData = this.dataPath() + '.tmp';
178
- await node_fs_1.promises.writeFile(tmpData, Buffer.from(this.vectors.buffer, this.vectors.byteOffset, this.vectors.byteLength));
307
+ await node_fs_1.promises.writeFile(tmpData, Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength));
179
308
  await node_fs_1.promises.writeFile(tmpMeta, JSON.stringify(meta));
180
309
  await node_fs_1.promises.rename(tmpData, this.dataPath());
181
310
  await node_fs_1.promises.rename(tmpMeta, this.metaPath());
182
- this.dirty = false;
311
+ if (this.version === version)
312
+ this.dirty = false;
183
313
  }
184
314
  /** Load transformers.js from the semantic home and build the extractor. */
185
315
  async model() {
@@ -198,8 +328,23 @@ class SemanticIndex {
198
328
  const tf = mod.pipeline ? mod : mod.default;
199
329
  tf.env.cacheDir = node_path_1.default.join(semanticHome(), 'models');
200
330
  tf.env.allowLocalModels = true;
201
- tf.env.allowRemoteModels = true;
331
+ tf.env.allowRemoteModels = this.options.allowDownload === true;
332
+ if (!this.options.allowDownload) {
333
+ const wrong = await verifyModelFiles();
334
+ if (wrong) {
335
+ this.broken = wrong;
336
+ throw new Error(wrong);
337
+ }
338
+ }
202
339
  this.extractor = await tf.pipeline('feature-extraction', semanticModel(), { dtype: semanticDtype() });
340
+ if (this.options.allowDownload) {
341
+ const wrong = await verifyModelFiles();
342
+ if (wrong) {
343
+ this.extractor = null;
344
+ this.broken = wrong;
345
+ throw new Error(wrong);
346
+ }
347
+ }
203
348
  return this.extractor;
204
349
  })().catch(err => {
205
350
  this.broken = this.broken || `semantic model failed to load: ${err instanceof Error ? err.message : String(err)}`;
@@ -215,6 +360,9 @@ class SemanticIndex {
215
360
  const out = [];
216
361
  const BATCH = batchSize();
217
362
  for (let i = 0; i < texts.length; i += BATCH) {
363
+ // Background work gives way to a query that is waiting (2.11).
364
+ while (kind === 'passage' && this.foreground > 0)
365
+ await new Promise(r => this.foregroundWaiters.push(r));
218
366
  const batch = texts.slice(i, i + BATCH).map(t => prefix + t.slice(0, MAX_CHARS));
219
367
  const res = await extractor(batch, { pooling: 'mean', normalize: true });
220
368
  // res.data is a flat typed array of batch × dim.
@@ -243,23 +391,57 @@ class SemanticIndex {
243
391
  const nuevos = entries.filter(e => e.text && !this.pos.has(e.id));
244
392
  if (nuevos.length === 0)
245
393
  return 0;
246
- const vecs = await this.embed(nuevos.map(e => e.text), 'passage');
394
+ // In slices, each one usable at once and the whole saved every half
395
+ // minute (2.11): a pass of thousands used to land all at the end, so a
396
+ // process that left halfway — a classic session — kept nothing, and the
397
+ // next one started over.
398
+ let guardado = Date.now();
399
+ this.left = nuevos.length;
400
+ try {
401
+ for (let i = 0; i < nuevos.length; i += UPSERT_SLICE) {
402
+ const tramo = nuevos.slice(i, i + UPSERT_SLICE);
403
+ const vecs = await this.embed(tramo.map(e => e.text), 'passage');
404
+ this.append(tramo, vecs);
405
+ this.left = Math.max(0, nuevos.length - i - tramo.length);
406
+ if (this.left > 0 && Date.now() - guardado > UPSERT_SAVE_MS) {
407
+ await this.persist().catch(() => undefined);
408
+ guardado = Date.now();
409
+ }
410
+ }
411
+ }
412
+ finally {
413
+ this.left = 0;
414
+ }
415
+ return nuevos.length;
416
+ }
417
+ append(tramo, vecs) {
247
418
  // The base offset is captured once: ids.push inside the loop would
248
419
  // otherwise move it with every vector. Found as a RangeError, silently
249
420
  // swallowed upstream — which is why the first benchmark run showed no
250
421
  // change at all. Measure the effect, not the artefact.
251
422
  const base = this.ids.length;
252
- const grown = new Float32Array((base + nuevos.length) * this.dim);
253
- grown.set(this.vectors, 0);
254
- for (let i = 0; i < nuevos.length; i++) {
255
- grown.set(vecs[i], (base + i) * this.dim);
256
- this.pos.set(nuevos[i].id, base + i);
257
- this.ids.push(nuevos[i].id);
423
+ const need = (base + tramo.length) * this.dim;
424
+ // Room grows by half each time (2.11): a pass that added 64 vectors at a
425
+ // time reallocated and copied the whole array each slice — tens of MB
426
+ // every couple of seconds, and the collector paid for it in pauses.
427
+ let store = this.vectors.buffer.byteLength / 4 >= need && this.vectors.byteOffset === 0
428
+ ? new Float32Array(this.vectors.buffer)
429
+ : null;
430
+ if (!store) {
431
+ store = new Float32Array(Math.max(need, Math.ceil((this.vectors.length || 0) * 1.5), 256 * this.dim));
432
+ store.set(this.vectors, 0);
258
433
  }
259
- this.vectors = grown;
434
+ for (let i = 0; i < tramo.length; i++) {
435
+ store.set(vecs[i], (base + i) * this.dim);
436
+ this.pos.set(tramo[i].id, base + i);
437
+ this.ids.push(tramo[i].id);
438
+ }
439
+ this.vectors = store.subarray(0, need);
440
+ this.version++;
260
441
  this.dirty = true;
261
- return nuevos.length;
262
442
  }
443
+ /** Every id holding a vector (to compare with the index: B14). */
444
+ idList() { return this.ids; }
263
445
  /** Drop vectors for ids that left the index. */
264
446
  remove(ids) {
265
447
  const gone = new Set();
@@ -281,6 +463,7 @@ class SemanticIndex {
281
463
  this.ids = keepIds;
282
464
  this.vectors = keep;
283
465
  this.pos = new Map(keepIds.map((id, i) => [id, i]));
466
+ this.version++;
284
467
  this.dirty = true;
285
468
  return gone.size;
286
469
  }
@@ -288,7 +471,14 @@ class SemanticIndex {
288
471
  async query(text, k) {
289
472
  if (this.ids.length === 0)
290
473
  return [];
291
- const [q] = await this.embed([text], 'query');
474
+ const release = this.hold();
475
+ let q;
476
+ try {
477
+ [q] = await this.embed([text], 'query');
478
+ }
479
+ finally {
480
+ release();
481
+ }
292
482
  const scores = new Float32Array(this.ids.length);
293
483
  for (let i = 0; i < this.ids.length; i++) {
294
484
  let dot = 0;
@@ -11,6 +11,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.STOPWORDS = void 0;
12
12
  exports.fold = fold;
13
13
  exports.tokenize = tokenize;
14
+ exports.indexTokens = indexTokens;
15
+ exports.createIndexTokenizer = createIndexTokenizer;
14
16
  exports.queryTerms = queryTerms;
15
17
  exports.synonymsEnabled = synonymsEnabled;
16
18
  exports.variants = variants;
@@ -59,6 +61,48 @@ function tokenize(text) {
59
61
  .split(/[^a-z0-9_]+/)
60
62
  .filter(Boolean);
61
63
  }
64
+ /**
65
+ * The terms a stored text is indexed under (2.11). Exactly the query side's
66
+ * fold and split — until 2.10 the index used Orama's English splitter, which
67
+ * cuts BEFORE folding and keeps only à è é ì ò ó ù inside a word, so
68
+ * "página" was stored as "p" + "gina" and "diseño" as "dise" + "o" while the
69
+ * query asked for "pagina" and "diseno": 72 % of the entries of a real
70
+ * Spanish brain were unreachable by any word with á, í, ú, ñ or ü. The same
71
+ * splitter kept "crbro-memory" as one token, so "memory" found nothing; the
72
+ * hyphen now splits on both sides. A snake_case identifier is kept whole (a
73
+ * query keeps it whole too) and its parts are added, so "lista" also finds
74
+ * "ocultar_de_lista".
75
+ */
76
+ function indexTokens(text) {
77
+ const out = [];
78
+ for (const t of tokenize(text)) {
79
+ out.push(t);
80
+ if (t.includes('_'))
81
+ for (const p of t.split('_'))
82
+ if (p)
83
+ out.push(p);
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * An Orama tokenizer component built on the functions above. Orama calls it
89
+ * with the property name when it indexes a document and without one when it
90
+ * tokenizes a search term: documents get indexTokens(), terms tokenize(), so
91
+ * both sides share one normalisation by construction. Tokens are
92
+ * de-duplicated per text, like Orama's own tokenizer does by default, so
93
+ * BM25 sees the same term frequencies it always saw.
94
+ */
95
+ function createIndexTokenizer() {
96
+ return {
97
+ language: 'english',
98
+ normalizationCache: new Map(),
99
+ tokenize(raw, _language, prop) {
100
+ if (typeof raw !== 'string')
101
+ return [raw];
102
+ return [...new Set(prop ? indexTokens(raw) : tokenize(raw))];
103
+ },
104
+ };
105
+ }
62
106
  /**
63
107
  * Tokenize a search query: drop stopwords and 1-char noise, dedupe,
64
108
  * and keep the original order. If everything was a stopword, fall back