crbro-memory 2.10.0 → 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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -465
  3. package/SECURITY.md +114 -12
  4. package/bin/crbro.mjs +539 -300
  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 +139 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +667 -232
  25. package/dist/engine/doctor.js +312 -14
  26. package/dist/engine/heat.js +81 -37
  27. package/dist/engine/hippocampus.js +18 -7
  28. package/dist/engine/hookrefresh.js +296 -0
  29. package/dist/engine/hookverify.js +15 -3
  30. package/dist/engine/installer.js +1022 -0
  31. package/dist/engine/installio.js +279 -0
  32. package/dist/engine/keychain.js +721 -162
  33. package/dist/engine/maintenance.js +355 -87
  34. package/dist/engine/modinstall.js +98 -45
  35. package/dist/engine/prefrontal.js +29 -6
  36. package/dist/engine/quarantine.js +204 -0
  37. package/dist/engine/secrets.js +41 -5
  38. package/dist/engine/shelf.js +25 -3
  39. package/dist/engine/synapses.js +183 -80
  40. package/dist/engine/triggers.js +141 -11
  41. package/dist/index.js +37 -3
  42. package/dist/mcp-surface.json +1 -0
  43. package/dist/miner/extractor.js +64 -17
  44. package/dist/miner/scheduler.js +144 -68
  45. package/dist/search/index.js +797 -176
  46. package/dist/search/semantic.js +208 -18
  47. package/dist/search/tokenize.js +44 -0
  48. package/dist/server.js +583 -373
  49. package/dist/sync/git.js +203 -72
  50. package/dist/sync/space.js +100 -25
  51. package/dist/sync/validate.js +204 -0
  52. package/dist/utils/budget.js +15 -2
  53. package/dist/utils/fs.js +40 -6
  54. package/dist/utils/ids.js +128 -14
  55. package/dist/utils/lock.js +94 -7
  56. package/hooks/crbro-codex.mjs +284 -0
  57. package/hooks/crbro-guard.mjs +281 -173
  58. package/hooks/crbro-hooks.shipped.json +20 -0
  59. package/hooks/crbro-lifecycle.mjs +16 -4
  60. package/hooks/crbro-subagent.mjs +180 -180
  61. package/package.json +9 -5
  62. package/dist/daemon/daemon.d.ts +0 -25
  63. package/dist/daemon/daemon.d.ts.map +0 -1
  64. package/dist/daemon/daemon.js.map +0 -1
  65. package/dist/daemon/endpoint.d.ts +0 -66
  66. package/dist/daemon/endpoint.d.ts.map +0 -1
  67. package/dist/daemon/endpoint.js.map +0 -1
  68. package/dist/daemon/lines.d.ts +0 -55
  69. package/dist/daemon/lines.d.ts.map +0 -1
  70. package/dist/daemon/lines.js.map +0 -1
  71. package/dist/daemon/proxy.d.ts +0 -55
  72. package/dist/daemon/proxy.d.ts.map +0 -1
  73. package/dist/daemon/proxy.js.map +0 -1
  74. package/dist/engine/backup.d.ts +0 -68
  75. package/dist/engine/backup.d.ts.map +0 -1
  76. package/dist/engine/backup.js.map +0 -1
  77. package/dist/engine/brain.d.ts +0 -79
  78. package/dist/engine/brain.d.ts.map +0 -1
  79. package/dist/engine/brain.js.map +0 -1
  80. package/dist/engine/cortex.d.ts +0 -682
  81. package/dist/engine/cortex.d.ts.map +0 -1
  82. package/dist/engine/cortex.js.map +0 -1
  83. package/dist/engine/dates.d.ts +0 -28
  84. package/dist/engine/dates.d.ts.map +0 -1
  85. package/dist/engine/dates.js.map +0 -1
  86. package/dist/engine/doctor.d.ts +0 -93
  87. package/dist/engine/doctor.d.ts.map +0 -1
  88. package/dist/engine/doctor.js.map +0 -1
  89. package/dist/engine/evidence.d.ts +0 -26
  90. package/dist/engine/evidence.d.ts.map +0 -1
  91. package/dist/engine/evidence.js.map +0 -1
  92. package/dist/engine/heat.d.ts +0 -28
  93. package/dist/engine/heat.d.ts.map +0 -1
  94. package/dist/engine/heat.js.map +0 -1
  95. package/dist/engine/hippocampus.d.ts +0 -60
  96. package/dist/engine/hippocampus.d.ts.map +0 -1
  97. package/dist/engine/hippocampus.js.map +0 -1
  98. package/dist/engine/hookverify.d.ts +0 -42
  99. package/dist/engine/hookverify.d.ts.map +0 -1
  100. package/dist/engine/hookverify.js.map +0 -1
  101. package/dist/engine/keychain.d.ts +0 -32
  102. package/dist/engine/keychain.d.ts.map +0 -1
  103. package/dist/engine/keychain.js.map +0 -1
  104. package/dist/engine/maintenance.d.ts +0 -218
  105. package/dist/engine/maintenance.d.ts.map +0 -1
  106. package/dist/engine/maintenance.js.map +0 -1
  107. package/dist/engine/modinstall.d.ts +0 -215
  108. package/dist/engine/modinstall.d.ts.map +0 -1
  109. package/dist/engine/modinstall.js.map +0 -1
  110. package/dist/engine/postmortem.d.ts +0 -81
  111. package/dist/engine/postmortem.d.ts.map +0 -1
  112. package/dist/engine/postmortem.js.map +0 -1
  113. package/dist/engine/prefrontal.d.ts +0 -64
  114. package/dist/engine/prefrontal.d.ts.map +0 -1
  115. package/dist/engine/prefrontal.js.map +0 -1
  116. package/dist/engine/projects.d.ts +0 -20
  117. package/dist/engine/projects.d.ts.map +0 -1
  118. package/dist/engine/projects.js.map +0 -1
  119. package/dist/engine/secrets.d.ts +0 -30
  120. package/dist/engine/secrets.d.ts.map +0 -1
  121. package/dist/engine/secrets.js.map +0 -1
  122. package/dist/engine/shelf.d.ts +0 -155
  123. package/dist/engine/shelf.d.ts.map +0 -1
  124. package/dist/engine/shelf.js.map +0 -1
  125. package/dist/engine/source.d.ts +0 -6
  126. package/dist/engine/source.d.ts.map +0 -1
  127. package/dist/engine/source.js.map +0 -1
  128. package/dist/engine/synapses.d.ts +0 -82
  129. package/dist/engine/synapses.d.ts.map +0 -1
  130. package/dist/engine/synapses.js.map +0 -1
  131. package/dist/engine/triggers.d.ts +0 -53
  132. package/dist/engine/triggers.d.ts.map +0 -1
  133. package/dist/engine/triggers.js.map +0 -1
  134. package/dist/engine/usage.d.ts +0 -61
  135. package/dist/engine/usage.d.ts.map +0 -1
  136. package/dist/engine/usage.js.map +0 -1
  137. package/dist/index.d.ts +0 -2
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js.map +0 -1
  140. package/dist/miner/extractor.d.ts +0 -12
  141. package/dist/miner/extractor.d.ts.map +0 -1
  142. package/dist/miner/extractor.js.map +0 -1
  143. package/dist/miner/index.d.ts +0 -61
  144. package/dist/miner/index.d.ts.map +0 -1
  145. package/dist/miner/index.js.map +0 -1
  146. package/dist/miner/scheduler.d.ts +0 -30
  147. package/dist/miner/scheduler.d.ts.map +0 -1
  148. package/dist/miner/scheduler.js.map +0 -1
  149. package/dist/miner/types.d.ts +0 -11
  150. package/dist/miner/types.d.ts.map +0 -1
  151. package/dist/miner/types.js.map +0 -1
  152. package/dist/search/index.d.ts +0 -328
  153. package/dist/search/index.d.ts.map +0 -1
  154. package/dist/search/index.js.map +0 -1
  155. package/dist/search/quality.d.ts +0 -18
  156. package/dist/search/quality.d.ts.map +0 -1
  157. package/dist/search/quality.js.map +0 -1
  158. package/dist/search/semantic.d.ts +0 -80
  159. package/dist/search/semantic.d.ts.map +0 -1
  160. package/dist/search/semantic.js.map +0 -1
  161. package/dist/search/synonyms.d.ts +0 -2
  162. package/dist/search/synonyms.d.ts.map +0 -1
  163. package/dist/search/synonyms.js.map +0 -1
  164. package/dist/search/tokenize.d.ts +0 -39
  165. package/dist/search/tokenize.d.ts.map +0 -1
  166. package/dist/search/tokenize.js.map +0 -1
  167. package/dist/server.d.ts +0 -41
  168. package/dist/server.d.ts.map +0 -1
  169. package/dist/server.js.map +0 -1
  170. package/dist/sync/git.d.ts +0 -36
  171. package/dist/sync/git.d.ts.map +0 -1
  172. package/dist/sync/git.js.map +0 -1
  173. package/dist/sync/materialize.d.ts +0 -36
  174. package/dist/sync/materialize.d.ts.map +0 -1
  175. package/dist/sync/materialize.js.map +0 -1
  176. package/dist/sync/ops.d.ts +0 -147
  177. package/dist/sync/ops.d.ts.map +0 -1
  178. package/dist/sync/ops.js.map +0 -1
  179. package/dist/sync/space.d.ts +0 -118
  180. package/dist/sync/space.d.ts.map +0 -1
  181. package/dist/sync/space.js.map +0 -1
  182. package/dist/types/index.d.ts +0 -369
  183. package/dist/types/index.d.ts.map +0 -1
  184. package/dist/types/index.js.map +0 -1
  185. package/dist/utils/budget.d.ts +0 -44
  186. package/dist/utils/budget.d.ts.map +0 -1
  187. package/dist/utils/budget.js.map +0 -1
  188. package/dist/utils/fs.d.ts +0 -59
  189. package/dist/utils/fs.d.ts.map +0 -1
  190. package/dist/utils/fs.js.map +0 -1
  191. package/dist/utils/hash.d.ts +0 -20
  192. package/dist/utils/hash.d.ts.map +0 -1
  193. package/dist/utils/hash.js.map +0 -1
  194. package/dist/utils/ids.d.ts +0 -47
  195. package/dist/utils/ids.d.ts.map +0 -1
  196. package/dist/utils/ids.js.map +0 -1
  197. package/dist/utils/lock.d.ts +0 -13
  198. package/dist/utils/lock.d.ts.map +0 -1
  199. package/dist/utils/lock.js.map +0 -1
  200. package/dist/utils/transcripts.d.ts +0 -63
  201. package/dist/utils/transcripts.d.ts.map +0 -1
  202. package/dist/utils/transcripts.js.map +0 -1
  203. package/dist/version.d.ts +0 -26
  204. package/dist/version.d.ts.map +0 -1
  205. package/dist/version.js.map +0 -1
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BACKUP_FAIL_DAYS = exports.BACKUP_WARN_DAYS = void 0;
6
7
  exports.diagnose = diagnose;
8
+ exports.findings = findings;
7
9
  exports.formatDoctor = formatDoctor;
8
10
  // Diagnóstico observacional: no construye motores, no arranca servidores y no
9
11
  // abre el llavero. Las salidas se construyen por lista blanca, nunca volcados.
@@ -17,6 +19,9 @@ const backup_js_1 = require("./backup.js");
17
19
  const endpoint_js_1 = require("../daemon/endpoint.js");
18
20
  const proxy_js_1 = require("../daemon/proxy.js");
19
21
  const version_js_1 = require("../version.js");
22
+ const hookverify_js_1 = require("./hookverify.js");
23
+ const hookrefresh_js_1 = require("./hookrefresh.js");
24
+ const installio_js_1 = require("./installio.js");
20
25
  async function readParsed(file) {
21
26
  let text;
22
27
  try {
@@ -184,6 +189,32 @@ function codexEntries(text) {
184
189
  }
185
190
  if (!inEnv && key === 'env')
186
191
  uncertain = true; // tabla inline no interpretada
192
+ // Sólo para comprobaciones booleanas (existe, usa npx) y los plazos: nunca se devuelven.
193
+ if (!inEnv && key === 'command') {
194
+ const m = /^"((?:\\.|[^"\\])*)"$|^'([^']*)'$/.exec(value);
195
+ if (m) {
196
+ try {
197
+ current.command = m[2] ?? JSON.parse(`"${m[1]}"`);
198
+ }
199
+ catch { /* no interpretado */ }
200
+ }
201
+ }
202
+ if (!inEnv && key === 'args' && value.startsWith('[') && value.endsWith(']')) {
203
+ current.args = [...value.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'/g)].map(x => {
204
+ try {
205
+ return x[2] ?? JSON.parse(`"${x[1]}"`);
206
+ }
207
+ catch {
208
+ return '';
209
+ }
210
+ });
211
+ }
212
+ if (!inEnv && (key === 'startup_timeout_sec' || key === 'tool_timeout_sec')) {
213
+ if (/^\+?\d+(\.\d+)?$/.test(value))
214
+ current[key] = Number(value);
215
+ else
216
+ uncertain = true;
217
+ }
187
218
  }
188
219
  return { entries: [...entries.values()], uncertain: uncertain || !!multiline };
189
220
  }
@@ -194,7 +225,7 @@ async function inspectConfigs(home, env, root, cwd) {
194
225
  : node_path_1.default.join(env.XDG_CONFIG_HOME || node_path_1.default.join(home, '.config'), 'Claude', 'claude_desktop_config.json');
195
226
  const locations = [
196
227
  { client: 'Claude Desktop', file: desktop },
197
- { client: 'Claude Code', file: node_path_1.default.join(home, '.claude.json') },
228
+ { client: 'Claude Code', file: (0, installio_js_1.claudeJson)(home, env) },
198
229
  { client: 'Cursor', file: node_path_1.default.join(home, '.cursor', 'mcp.json') },
199
230
  { client: 'Codex', file: node_path_1.default.join(env.CODEX_HOME || node_path_1.default.join(home, '.codex'), 'config.toml') },
200
231
  ];
@@ -207,7 +238,9 @@ async function inspectConfigs(home, env, root, cwd) {
207
238
  continue;
208
239
  seen.add(canonical(location.file));
209
240
  const row = { client: location.client, path: location.file, exists: await exists(location.file),
210
- status: 'unknown', registered: null, enabled: null, brain_paths: [], inherited_brain_path: false, client_autorun: 'unknown' };
241
+ status: 'unknown', registered: null, enabled: null, brain_paths: [], inherited_brain_path: false, client_autorun: 'unknown',
242
+ uses_npx: null, command_exists: null, script_exists: null, startup_timeout_sec: null, tool_timeout_sec: null };
243
+ let problem = false;
211
244
  let entries = [], uncertain = false;
212
245
  try {
213
246
  if (location.file.endsWith('.toml')) {
@@ -245,7 +278,25 @@ async function inspectConfigs(home, env, root, cwd) {
245
278
  uncertain = true;
246
279
  }
247
280
  }
248
- row.status = uncertain ? 'unknown' : 'ok';
281
+ // What starts it: booleans only, never the command line (it can carry a token).
282
+ const first = entries.find(e => e && typeof e === 'object' && !Array.isArray(e));
283
+ if (first && typeof first.command === 'string') {
284
+ const command = first.command;
285
+ const args = Array.isArray(first.args) ? first.args.filter((a) => typeof a === 'string') : [];
286
+ row.uses_npx = /(^|[\\/])npx(\.cmd|\.exe)?$/i.test(command);
287
+ row.command_exists = node_path_1.default.isAbsolute(command) ? !!(await exists(command)) : (0, installio_js_1.findOnPath)(command, env) !== null;
288
+ const script = args.find(a => node_path_1.default.isAbsolute(a) && /\.(mjs|cjs|js)$/i.test(a));
289
+ row.script_exists = script ? !!(await exists(script)) : null;
290
+ if (row.command_exists === false || row.script_exists === false)
291
+ problem = true;
292
+ }
293
+ if (location.file.endsWith('.toml') && first) {
294
+ row.startup_timeout_sec = typeof first.startup_timeout_sec === 'number' ? first.startup_timeout_sec : null;
295
+ row.tool_timeout_sec = typeof first.tool_timeout_sec === 'number' ? first.tool_timeout_sec : null;
296
+ if (row.startup_timeout_sec === null || row.tool_timeout_sec === null)
297
+ problem = true;
298
+ }
299
+ row.status = uncertain ? 'unknown' : problem ? 'warning' : 'ok';
249
300
  }
250
301
  catch { /* missing, unreadable or unsupported: unknown, no raw error */ }
251
302
  clients.push(row);
@@ -255,7 +306,8 @@ async function inspectConfigs(home, env, root, cwd) {
255
306
  for (const file of client.brain_paths)
256
307
  paths.set(canonical(file), file);
257
308
  const distinct_brain_paths = [...paths.values()].sort();
258
- return { status: distinct_brain_paths.length > 1 ? 'warning' : clients.some(c => c.status === 'unknown') ? 'unknown' : 'ok',
309
+ return { status: distinct_brain_paths.length > 1 || clients.some(c => c.status === 'warning') ? 'warning'
310
+ : clients.some(c => c.status === 'unknown') ? 'unknown' : 'ok',
259
311
  clients, distinct_brain_paths, different_brain_paths: distinct_brain_paths.length > 1,
260
312
  inherited_environment: 'unknown' };
261
313
  }
@@ -287,16 +339,163 @@ async function inspectSemantic(root, home, env) {
287
339
  && m.dtype === (env.CRBRO_SEMANTIC_DTYPE || 'q8') && bytes === m.ids.length * m.dim * 4;
288
340
  const index = { status: valid ? 'ok' : meta.status === 'missing' && bytes === null ? 'unknown' : 'warning',
289
341
  vectors: valid ? m.ids.length : null, metadata_present: meta.status !== 'missing', data_present: bytes !== null };
290
- return { status: index.status === 'warning' ? 'warning' : 'unknown',
342
+ // A valid index is evidence it works; no index is no evidence (semantic recall not installed), not a fault.
343
+ return { status: index.status,
291
344
  mode, home: semanticHome, runtime_present, model_cache_present, index, operation: 'not_probed' };
292
345
  }
293
- async function inspectBackups(root, env, home) {
346
+ const DAY_MS = 24 * 60 * 60 * 1000;
347
+ /** Newest backup older than this: a warning; older than BACKUP_FAIL_DAYS: an error. */
348
+ exports.BACKUP_WARN_DAYS = 2;
349
+ exports.BACKUP_FAIL_DAYS = 7;
350
+ async function inspectBackups(root, env, home, now) {
294
351
  const directory = (0, backup_js_1.resolveBackupDir)(root, env, home);
295
352
  const files = await (0, backup_js_1.listBackups)(directory);
296
- return { status: files.length ? 'ok' : 'unknown', directory,
297
- latest: files[0] ? { path: files[0].path, created: files[0].created, size_bytes: files[0].size_bytes } : null,
353
+ const latest = files[0] ? { path: files[0].path, created: files[0].created, size_bytes: files[0].size_bytes } : null;
354
+ const age_days = latest ? Math.max(0, Math.floor((now - Date.parse(latest.created)) / DAY_MS)) : null;
355
+ const brainThere = await exists(node_path_1.default.join(root, 'manifest.json'));
356
+ // Automatic copies come from consolidate once a day. A consolidate newer than
357
+ // the newest copy by more than a day, with them switched on, means they fail.
358
+ const enabled = !['0', 'off', 'false', 'no'].includes(String(env.CRBRO_AUTOBACKUP ?? '').trim().toLowerCase());
359
+ const manifest = await readParsed(node_path_1.default.join(root, 'manifest.json'));
360
+ const lastConsolidation = stamp(manifest.value?.last_consolidation);
361
+ const failing = !enabled || !lastConsolidation ? null
362
+ : (latest === null || Date.parse(lastConsolidation) - Date.parse(latest.created) > DAY_MS) && now - Date.parse(lastConsolidation) < exports.BACKUP_FAIL_DAYS * DAY_MS;
363
+ const status = !brainThere ? 'unknown'
364
+ : age_days === null ? 'warning'
365
+ : age_days > exports.BACKUP_FAIL_DAYS ? 'error'
366
+ : age_days > exports.BACKUP_WARN_DAYS || failing ? 'warning' : 'ok';
367
+ return { status, directory, latest, age_days, warn_after_days: exports.BACKUP_WARN_DAYS, fail_after_days: exports.BACKUP_FAIL_DAYS,
368
+ automatic: { enabled, last_consolidation: lastConsolidation, failing },
298
369
  integrity: 'not_probed' };
299
370
  }
371
+ /** A pid that exists right now (signal 0 checks, it sends nothing). */
372
+ function pidAlive(pid) {
373
+ if (!Number.isSafeInteger(pid) || pid <= 0)
374
+ return null;
375
+ try {
376
+ process.kill(pid, 0);
377
+ return true;
378
+ }
379
+ catch (e) {
380
+ return e.code === 'EPERM';
381
+ }
382
+ }
383
+ async function inspectHooks(home, env) {
384
+ const pkg = (0, hookrefresh_js_1.packageHooksDir)();
385
+ // dryRun: compares and reports, writes nothing.
386
+ const copies = (0, hookrefresh_js_1.refreshHooks)({ home, env, pkgHooks: pkg, dryRun: true });
387
+ const dirs = [
388
+ { client: 'claude-code', dir: (0, installio_js_1.claudeDir)(home, env), settings: 'settings.json' },
389
+ { client: 'codex', dir: (0, installio_js_1.codexHome)(home, env), settings: 'hooks.json' },
390
+ ];
391
+ const missing_scripts = [];
392
+ let settings_unreadable = false;
393
+ let npx_cache_references = 0;
394
+ for (const d of dirs) {
395
+ const settingsPath = node_path_1.default.join(d.dir, d.settings);
396
+ if (!(await exists(settingsPath)))
397
+ continue;
398
+ const v = (0, hookverify_js_1.verifyHooks)({ packageDir: pkg, installedDir: node_path_1.default.join(d.dir, 'crbro-hooks'), settingsPath });
399
+ missing_scripts.push(...v.missing_scripts);
400
+ settings_unreadable ||= v.settings_unreadable;
401
+ const parsed = await readParsed(settingsPath);
402
+ for (const list of Object.values(parsed.value?.hooks ?? {})) {
403
+ for (const entry of Array.isArray(list) ? list : []) {
404
+ for (const h of Array.isArray(entry?.hooks) ? entry.hooks : []) {
405
+ const cmd = `${h?.command ?? ''} ${h?.commandWindows ?? ''}`;
406
+ if (/crbro/i.test(cmd) && /[\\/]_npx[\\/]/.test(cmd))
407
+ npx_cache_references++;
408
+ }
409
+ }
410
+ }
411
+ }
412
+ const installed = copies.map(c => ({ client: c.client, file: node_path_1.default.join(c.dir, c.name),
413
+ state: c.state === 'stale' ? 'older_release' : c.state }));
414
+ const status = missing_scripts.length || settings_unreadable || npx_cache_references
415
+ || installed.some(c => c.state === 'older_release' || c.state === 'modified') ? 'warning' : 'ok';
416
+ return { status, installed, missing_scripts: [...new Set(missing_scripts)].sort(), settings_unreadable, npx_cache_references };
417
+ }
418
+ async function inspectDaemonLeftovers(root, now) {
419
+ const dir = node_path_1.default.join(root, '.daemon');
420
+ let names = [];
421
+ try {
422
+ names = await node_fs_1.promises.readdir(dir);
423
+ }
424
+ catch {
425
+ return { status: 'ok', directory: dir, descriptors: 0, dead_descriptors: 0, orphan_tmp: 0 };
426
+ }
427
+ let descriptors = 0, dead = 0, orphan = 0;
428
+ for (const n of names) {
429
+ const file = node_path_1.default.join(dir, n);
430
+ if (/\.json$/.test(n)) {
431
+ descriptors++;
432
+ const st = await readParsed(file);
433
+ if (st.status === 'ok' && pidAlive(st.value?.pid) === false)
434
+ dead++;
435
+ }
436
+ else if (/\.tmp$/.test(n)) {
437
+ // A write that never finished: its writer is gone, or it is older than an hour.
438
+ const pid = Number(/\.(\d+)\.tmp$/.exec(n)?.[1]);
439
+ let old = false;
440
+ try {
441
+ old = now - (await node_fs_1.promises.stat(file)).mtimeMs > 60 * 60 * 1000;
442
+ }
443
+ catch {
444
+ continue;
445
+ }
446
+ if (old || pidAlive(pid) === false)
447
+ orphan++;
448
+ }
449
+ }
450
+ return { status: (dead || orphan ? 'warning' : 'ok'), directory: dir, descriptors, dead_descriptors: dead, orphan_tmp: orphan };
451
+ }
452
+ async function inspectQuarantine(root, env, now) {
453
+ const dir = node_path_1.default.join(root, '.quarantine');
454
+ const days = Number(env.CRBRO_QUARANTINE_DAYS ?? 180);
455
+ const retention_days = Number.isFinite(days) && days >= 0 ? days : 180;
456
+ let files = 0, bytes = 0, oldest = null;
457
+ const walk = async (d) => {
458
+ let entries;
459
+ try {
460
+ entries = await node_fs_1.promises.readdir(d, { withFileTypes: true });
461
+ }
462
+ catch {
463
+ return;
464
+ }
465
+ for (const e of entries) {
466
+ const p = node_path_1.default.join(d, e.name);
467
+ if (e.isDirectory()) {
468
+ await walk(p);
469
+ continue;
470
+ }
471
+ try {
472
+ const st = await node_fs_1.promises.stat(p);
473
+ files++;
474
+ bytes += st.size;
475
+ oldest = oldest === null ? st.mtimeMs : Math.min(oldest, st.mtimeMs);
476
+ }
477
+ catch { /* gone */ }
478
+ }
479
+ };
480
+ await walk(dir);
481
+ const oldest_days = oldest === null ? null : Math.floor((now - oldest) / DAY_MS);
482
+ const status = (retention_days > 0 && oldest_days !== null && oldest_days > retention_days) || bytes > 500 * 1024 * 1024 ? 'warning' : 'ok';
483
+ return { status, directory: dir, files, bytes, oldest_days, retention_days };
484
+ }
485
+ async function inspectTriggers(root, home, env) {
486
+ const file = node_path_1.default.join(root, '.search', 'triggers.json');
487
+ const parsed = await readParsed(file);
488
+ const settings = await readParsed(node_path_1.default.join((0, installio_js_1.claudeDir)(home, env), 'settings.json'));
489
+ const guard_wired = /crbro-guard/.test(JSON.stringify(settings.value?.hooks ?? {}));
490
+ const v = parsed.value;
491
+ const valid = parsed.status === 'ok' && v && typeof v === 'object' && v.keys && typeof v.keys === 'object' && Array.isArray(v.entries);
492
+ const version = valid && Number.isSafeInteger(v.v) ? v.v : null;
493
+ const status = parsed.status === 'invalid' || parsed.status === 'unreadable' ? 'warning'
494
+ : parsed.status === 'missing' ? (guard_wired ? 'warning' : 'ok')
495
+ : !valid || ![2, 3].includes(version ?? -1) ? 'warning' : 'ok';
496
+ return { status, path: file, exists: parsed.status !== 'missing', version, built: valid ? stamp(v.built) : null,
497
+ entries: valid ? v.entries.length : null, guard_wired };
498
+ }
300
499
  function gitRead(dir, args, env) {
301
500
  return new Promise(resolve => {
302
501
  (0, node_child_process_1.execFile)('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false', '-C', dir, ...args], {
@@ -348,19 +547,114 @@ async function diagnose(options = {}) {
348
547
  const env = options.env ?? process.env;
349
548
  const home = node_path_1.default.resolve(options.homeDir || env.HOME || env.USERPROFILE || node_os_1.default.homedir());
350
549
  const root = node_path_1.default.resolve(options.brainDir || (0, brain_js_1.resolveBrainDir)({ ...env, HOME: home }, () => undefined));
351
- const [brain, versions, configs, semantic, backups, teams] = await Promise.all([
550
+ const now = options.now ?? Date.now();
551
+ const [brain, versions, configs, semantic, backups, teams, hooks, daemon_leftovers, quarantine, triggers] = await Promise.all([
352
552
  inspectBrain(root), inspectVersions(root), inspectConfigs(home, env, root, options.cwd),
353
- inspectSemantic(root, home, env), inspectBackups(root, env, home), inspectTeams(root, env),
553
+ inspectSemantic(root, home, env), inspectBackups(root, env, home, now), inspectTeams(root, env),
554
+ inspectHooks(home, env), inspectDaemonLeftovers(root, now), inspectQuarantine(root, env, now), inspectTriggers(root, home, env),
354
555
  ]);
355
- const statuses = [brain.status, configs.status, semantic.status, backups.status, teams.status];
556
+ // A problem anywhere decides; otherwise a brain that reads well is "ok".
557
+ // What could not be observed (no daemon running, semantic not installed, a
558
+ // client not installed) is listed as not checked, never as a fault.
559
+ const statuses = [brain.status, configs.status, semantic.status, backups.status, teams.status,
560
+ hooks.status, daemon_leftovers.status, quarantine.status, triggers.status];
356
561
  const status = statuses.includes('error') ? 'error' : statuses.includes('warning') ? 'warning'
357
- : statuses.includes('unknown') || versions.daemon_status === 'unknown' ? 'unknown' : 'ok';
358
- return { schema_version: 1, read_only: true, status, versions, brain, configs, semantic, backups, teams };
562
+ : brain.status === 'ok' ? 'ok' : 'unknown';
563
+ return { schema_version: 1, read_only: true, status, versions, brain, configs, semantic, backups, teams,
564
+ hooks, daemon_leftovers, quarantine, triggers };
565
+ }
566
+ /** What the report means, in three lists, each with the command that fixes it. */
567
+ function findings(report) {
568
+ const out = [];
569
+ const add = (level, text, action) => out.push({ level, text, action });
570
+ const b = report.brain;
571
+ if (b.exists === false)
572
+ add('unchecked', `no brain at ${b.path} yet`, 'npx crbro-memory init (or start any client: the first boot creates it)');
573
+ for (const f of b.invalid_json_paths)
574
+ add('problem', `invalid JSON: ${f}`, 'restore that file from a backup: npx crbro-memory backup list');
575
+ for (const f of b.unreadable_paths)
576
+ add('problem', `unreadable: ${f}`, 'check its permissions');
577
+ const k = report.backups;
578
+ if (k.status === 'error')
579
+ add('problem', `the newest backup is ${k.age_days} days old (${k.latest?.path})`, 'npx crbro-memory backup');
580
+ else if (k.status === 'warning' && k.latest === null)
581
+ add('warning', 'no backup yet', 'npx crbro-memory backup');
582
+ else if (k.status === 'warning')
583
+ add('warning', `the newest backup is ${k.age_days} day(s) old`, 'npx crbro-memory backup');
584
+ if (k.automatic.failing)
585
+ add('warning', `automatic backups look like they fail: consolidate ran on ${k.automatic.last_consolidation}, the newest copy is older`, `check that ${k.directory} is writable, then npx crbro-memory backup`);
586
+ for (const c of report.configs.clients) {
587
+ if (!c.registered)
588
+ continue;
589
+ if (c.command_exists === false)
590
+ add('warning', `${c.client}: the command its CRBRO registration runs does not exist (${c.path})`, 'npx crbro-memory install');
591
+ if (c.script_exists === false)
592
+ add('warning', `${c.client}: the script its CRBRO registration runs does not exist (${c.path})`, 'npx crbro-memory install');
593
+ if (c.client.startsWith('Codex') && (c.startup_timeout_sec === null || c.tool_timeout_sec === null)) {
594
+ add('warning', `${c.client}: [mcp_servers.crbro] has no ${c.startup_timeout_sec === null ? 'startup_timeout_sec' : 'tool_timeout_sec'}: a slow start loses the memory (${c.path})`, 'npx crbro-memory install-boot');
595
+ }
596
+ if (c.uses_npx)
597
+ add('unchecked', `${c.client} starts CRBRO through npx (works; a stable engine starts faster and keeps one daemon)`, 'npx crbro-memory install');
598
+ }
599
+ if (report.configs.different_brain_paths)
600
+ add('warning', `clients point at different brains: ${report.configs.distinct_brain_paths.join(', ')}`, 'set the same CRBRO_PATH in every client');
601
+ const h = report.hooks;
602
+ for (const c of h.installed) {
603
+ if (c.state === 'older_release')
604
+ add('warning', `hook from an older CRBRO: ${c.file}`, 'refreshed at the next crbro_boot, or now: npx crbro-memory install-hooks …');
605
+ if (c.state === 'modified')
606
+ add('warning', `hook edited by hand, not refreshed: ${c.file}`, 'keep it, or take this version (backup kept): run the install-hooks you used again');
607
+ }
608
+ for (const s of h.missing_scripts)
609
+ add('warning', `a hook runs ${s}, which does not exist: it fails in every session`, 'npx crbro-memory install-boot, or remove that hook');
610
+ if (h.npx_cache_references)
611
+ add('warning', `${h.npx_cache_references} CRBRO hook(s) run out of the npx cache, which disappears when it is cleaned`, 'npx crbro-memory install');
612
+ if (h.settings_unreadable)
613
+ add('warning', 'a client settings file with hooks is not valid JSON', 'fix the JSON by hand');
614
+ const d = report.daemon_leftovers;
615
+ if (d.dead_descriptors)
616
+ add('warning', `${d.dead_descriptors} daemon descriptor(s) of processes that are gone in ${d.directory}`, 'npx crbro-memory daemon status (removes them)');
617
+ if (d.orphan_tmp)
618
+ add('warning', `${d.orphan_tmp} unfinished temp file(s) in ${d.directory}`, 'safe to delete while no daemon starts');
619
+ const q = report.quarantine;
620
+ if (q.status === 'warning')
621
+ add('warning', `quarantine holds ${q.files} file(s), ${(q.bytes / 1048576).toFixed(1)} MB, the oldest ${q.oldest_days} day(s) old`, `review ${q.directory}`);
622
+ const t = report.triggers;
623
+ if (t.status === 'warning')
624
+ add('warning', t.exists ? `the guard's trigger index (${t.path}) is unreadable or an old format: the guard says nothing` : 'the guard is installed but its trigger index does not exist yet', 'npx crbro-memory guard --rebuild');
625
+ if (report.semantic.index.status === 'warning')
626
+ add('warning', 'the semantic vectors do not match their metadata', 'npx crbro-memory semantic build');
627
+ for (const s of report.teams.spaces)
628
+ if (s.status === 'warning')
629
+ add('warning', `team space ${s.path} is ${s.ahead} ahead / ${s.behind} behind`, 'crbro_boot syncs it');
630
+ if (report.versions.daemon_status !== 'ok')
631
+ add('unchecked', 'no daemon answered (normal when daemon mode is off or no client is open)');
632
+ if (report.semantic.index.status === 'unknown')
633
+ add('unchecked', 'semantic recall: no vectors yet (not installed, or never built)', 'npx crbro-memory semantic status');
634
+ add('unchecked', 'semantic operation and backup integrity: not_probed (doctor never loads the model or unpacks a backup)');
635
+ add('unchecked', 'whether each client runs crbro_boot on its own: unknown (doctor reads files, it does not start clients)');
636
+ return out;
359
637
  }
360
638
  function formatDoctor(report) {
361
639
  const show = (v) => v === null || v === undefined ? 'unknown' : String(v);
640
+ const list = findings(report);
641
+ const block = (title, level) => {
642
+ const items = list.filter(f => f.level === level);
643
+ if (!items.length)
644
+ return [];
645
+ return [title, ...items.map(f => ` • ${f.text}${f.action ? `\n → ${f.action}` : ''}`), ''];
646
+ };
647
+ const verdict = report.status === 'ok' ? 'Everything observed is in order.'
648
+ : report.status === 'error' ? 'There are problems to fix (below).'
649
+ : report.status === 'warning' ? 'Working, with warnings (below).' : 'Not enough evidence to say (below).';
362
650
  return [
363
651
  `CRBRO doctor — read-only — ${report.status}`,
652
+ verdict,
653
+ '',
654
+ ...block('Problems:', 'problem'),
655
+ ...block('Warnings:', 'warning'),
656
+ ...block('Not checked:', 'unchecked'),
657
+ 'Details:',
364
658
  `Installed: ${show(report.versions.installed)}`,
365
659
  `Running daemons: ${report.versions.daemons.map(d => show(d.running_version)).join(', ') || 'unknown'} (authenticated status only)`,
366
660
  `Brain: ${report.brain.path} [${report.brain.status}]`,
@@ -370,7 +664,11 @@ function formatDoctor(report) {
370
664
  ...report.configs.clients.map(c => `${c.client}: ${c.path} [${c.status}]; registered=${show(c.registered)}; enabled=${show(c.enabled)}; brain=${c.brain_paths.join(', ') || 'unknown (inherited)'}`),
371
665
  `Different configured brain paths: ${report.configs.different_brain_paths}`,
372
666
  `Semantic: ${report.semantic.mode}; runtime=${show(report.semantic.runtime_present)}; model cache=${show(report.semantic.model_cache_present)}; index=${report.semantic.index.status}; operation=not_probed`,
373
- `Latest backup: ${report.backups.latest?.path || 'unknown'}; integrity=not_probed`,
667
+ `Latest backup: ${report.backups.latest?.path || 'unknown'}${report.backups.age_days !== null ? ` (${report.backups.age_days} day(s) old)` : ''}; integrity=not_probed`,
668
+ `Hooks: ${report.hooks.installed.map(h => `${node_path_1.default.basename(h.file)} (${h.client}) ${h.state}`).join(', ') || 'none installed'} [${report.hooks.status}]`,
669
+ `Daemon leftovers: ${report.daemon_leftovers.dead_descriptors} dead descriptor(s), ${report.daemon_leftovers.orphan_tmp} orphan temp file(s) [${report.daemon_leftovers.status}]`,
670
+ `Quarantine: ${report.quarantine.files} file(s), ${report.quarantine.bytes} bytes, oldest ${show(report.quarantine.oldest_days)} day(s) [${report.quarantine.status}]`,
671
+ `Trigger index: ${report.triggers.exists ? `v${show(report.triggers.version)}, ${show(report.triggers.entries)} lessons, built ${show(report.triggers.built)}` : 'none'}${report.triggers.guard_wired ? ' (guard wired)' : ''} [${report.triggers.status}]`,
374
672
  ...report.teams.spaces.map(s => `Team: ${s.path}; dirty=${show(s.dirty)}; worktree=${s.worktree_state}; ahead=${show(s.ahead)}; behind=${show(s.behind)}; remote=not_fetched`),
375
673
  `Team remote state: not_fetched. Unknown values are not evidence of a working integration.`,
376
674
  ].join('\n');
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  // ─── CRBRO Heat Score Engine ─────────────────────────────────────
3
3
  // Calculates relevance score based on frequency, recency, and connectivity
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
4
7
  Object.defineProperty(exports, "__esModule", { value: true });
5
8
  exports.HeatEngine = void 0;
6
9
  exports.recencyScore = recencyScore;
7
10
  exports.frequencyScore = frequencyScore;
8
11
  exports.connectivityScore = connectivityScore;
9
12
  exports.calculateHeat = calculateHeat;
13
+ exports.heatStatePath = heatStatePath;
14
+ exports.readHeatState = readHeatState;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = __importDefault(require("node:path"));
10
17
  const fs_js_1 = require("../utils/fs.js");
11
18
  // Weight constants
12
19
  const WEIGHT_FREQUENCY = 0.35;
@@ -56,67 +63,104 @@ function calculateHeat(accessCount, lastAccessed, connectionCount, maxAccessCoun
56
63
  // Round to 3 decimal places
57
64
  return Math.round(heat * 1000) / 1000;
58
65
  }
59
- // ─── Heat Manager ────────────────────────────────────────────────
66
+ function heatStatePath(brain) {
67
+ return node_path_1.default.join(brain.paths.prefrontal, 'heat_state.json');
68
+ }
69
+ /** The derived heat state, or null when never computed or unreadable (it is rebuilt then). */
70
+ async function readHeatState(brain) {
71
+ try {
72
+ const s = await (0, fs_js_1.readJSON)(heatStatePath(brain));
73
+ return s && s.version === 1 && s.neurons && typeof s.neurons === 'object' ? s : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
60
79
  class HeatEngine {
61
80
  brain;
62
81
  constructor(brain) {
63
82
  this.brain = brain;
64
83
  }
84
+ /** Neuron files the last pass could not parse: skipped, never fatal. */
85
+ lastUnreadable = [];
86
+ /** How many neuron files the last pass actually read (the rest came from the state). */
87
+ lastRead = 0;
65
88
  /**
66
89
  * Recalculate heat scores for all neurons.
67
90
  * Returns updated hot topics list.
68
91
  */
69
92
  async recalculate() {
70
93
  const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.cortex);
71
- // First pass — find maximums for normalization
94
+ const previo = (await readHeatState(this.brain))?.neurons || {};
95
+ const entradas = {};
96
+ const ilegibles = [];
97
+ let leidas = 0;
98
+ // First pass — inputs: from the state when the file has not changed,
99
+ // from the file otherwise.
72
100
  let maxAccess = 1;
73
101
  let maxConnections = 1;
74
- const neurons = [];
75
102
  for (const id of ids) {
76
- const neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
77
- if (!neuron)
103
+ const file = this.brain.paths.neuron(id);
104
+ let st;
105
+ try {
106
+ st = await node_fs_1.promises.stat(file);
107
+ }
108
+ catch {
78
109
  continue;
79
- neurons.push(neuron);
80
- if (neuron.access_count > maxAccess)
81
- maxAccess = neuron.access_count;
82
- if (neuron.connections.length > maxConnections)
83
- maxConnections = neuron.connections.length;
110
+ }
111
+ const antes = previo[id];
112
+ let e;
113
+ if (antes && antes.mtime === st.mtimeMs && antes.size === st.size) {
114
+ e = { ...antes };
115
+ }
116
+ else {
117
+ let neuron;
118
+ try {
119
+ neuron = await (0, fs_js_1.readJSON)(file);
120
+ }
121
+ catch {
122
+ ilegibles.push(id); // corrupted: skipped, the integrity check names it
123
+ continue;
124
+ }
125
+ if (!neuron)
126
+ continue;
127
+ leidas++;
128
+ e = {
129
+ heat: 0,
130
+ access_count: neuron.access_count || 0,
131
+ last_accessed: neuron.last_accessed || neuron.created || '',
132
+ connections: (neuron.connections || []).length,
133
+ name: neuron.name || id,
134
+ domain: neuron.domain || 'general',
135
+ size: st.size,
136
+ mtime: st.mtimeMs,
137
+ };
138
+ }
139
+ entradas[id] = e;
140
+ if (e.access_count > maxAccess)
141
+ maxAccess = e.access_count;
142
+ if (e.connections > maxConnections)
143
+ maxConnections = e.connections;
84
144
  }
85
- // Second pass — calculate and update heat
145
+ // Second pass — calculate heat
86
146
  const hotTopics = [];
87
- for (const neuron of neurons) {
88
- const heat = calculateHeat(neuron.access_count, neuron.last_accessed, neuron.connections.length, maxAccess, maxConnections);
89
- // Only touch the file when the number actually moved. Rewriting all
90
- // neurons on every pass was pure churn — between two consecutive runs
91
- // not one of them changes — and each rewrite is a window in which a
92
- // concurrent write from another client gets clobbered.
93
- if (Math.abs((neuron.heat ?? 0) - heat) > 1e-9) {
94
- // Under lock and re-reading: heat runs over every neuron, so without
95
- // this it would happily overwrite whatever another client just saved.
96
- await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
97
- if (!current)
98
- return null;
99
- current.heat = heat;
100
- return current;
101
- });
102
- }
103
- neuron.heat = heat;
104
- hotTopics.push({
105
- id: neuron.id,
106
- name: neuron.name,
107
- heat,
108
- last_access: neuron.last_accessed,
109
- domain: neuron.domain,
110
- });
147
+ for (const [id, e] of Object.entries(entradas)) {
148
+ e.heat = calculateHeat(e.access_count, e.last_accessed, e.connections, maxAccess, maxConnections);
149
+ hotTopics.push({ id, name: e.name, heat: e.heat, last_access: e.last_accessed, domain: e.domain });
111
150
  }
112
151
  // Sort by heat descending
113
- hotTopics.sort((a, b) => b.heat - a.heat);
152
+ hotTopics.sort((a, b) => b.heat - a.heat || (a.id < b.id ? -1 : 1));
153
+ const cuando = (0, fs_js_1.now)();
154
+ const estado = { version: 1, computed_at: cuando, neurons: entradas };
155
+ await (0, fs_js_1.writeJSON)(heatStatePath(this.brain), estado, { pretty: false });
114
156
  // Save hot topics (top 20)
115
157
  const result = {
116
158
  topics: hotTopics.slice(0, 20),
117
- last_recalculated: (0, fs_js_1.now)(),
159
+ last_recalculated: cuando,
118
160
  };
119
161
  await (0, fs_js_1.writeJSON)(this.brain.paths.hotTopics(), result);
162
+ this.lastUnreadable = ilegibles;
163
+ this.lastRead = leidas;
120
164
  return result;
121
165
  }
122
166
  }
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.Hippocampus = void 0;
6
6
  exports.normalizeSessionId = normalizeSessionId;
7
7
  const fs_js_1 = require("../utils/fs.js");
8
+ const quarantine_js_1 = require("./quarantine.js");
8
9
  const ids_js_1 = require("../utils/ids.js");
9
10
  const secrets_js_1 = require("./secrets.js");
10
11
  const SESSION_PREFIX = 'session_';
@@ -73,15 +74,25 @@ class Hippocampus {
73
74
  * List recent sessions.
74
75
  */
75
76
  async listSessions(limit = 10) {
76
- const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.hippocampus);
77
+ // The names are session_YYYY-MM-DD: they sort by day, so only the newest
78
+ // `limit` files are read (2.11). Boot asks for 3 and used to parse all
79
+ // 146 logs of the reference brain to keep 3. An unreadable log is skipped.
80
+ const ids = (await (0, fs_js_1.listJSONFiles)(this.brain.paths.hippocampus)).sort().reverse();
77
81
  const sessions = [];
78
82
  for (const id of ids) {
79
- const session = await (0, fs_js_1.readJSON)(this.brain.paths.session(id));
83
+ if (sessions.length >= limit)
84
+ break;
85
+ let session = null;
86
+ try {
87
+ session = await (0, fs_js_1.readJSON)(this.brain.paths.session(id));
88
+ }
89
+ catch { /* corrupt: skipped */ }
80
90
  if (session)
81
91
  sessions.push(session);
82
92
  }
83
- // Sort by date descending (newest first)
84
- sessions.sort((a, b) => b.date.localeCompare(a.date));
93
+ // Sort by date descending (newest first): a log whose file name is not a
94
+ // day id still lands by its own date among the ones read.
95
+ sessions.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
85
96
  return sessions.slice(0, limit);
86
97
  }
87
98
  /**
@@ -127,9 +138,9 @@ class Hippocampus {
127
138
  const log = await (0, fs_js_1.readJSON)(ruta);
128
139
  if (!log)
129
140
  return { session_id, removed: false, backup: null };
130
- const sello = (0, fs_js_1.now)().replace(/[:.]/g, '-');
131
- const backup = `${this.brain.paths.quarantine}/${session_id}.${sello}.json`;
132
- await (0, fs_js_1.writeJSON)(backup, log);
141
+ // Same quarantine as the cortex: retention, and sealed when encryption at
142
+ // rest is on (throws, deleting nothing, when it is on and keyless).
143
+ const backup = await (0, quarantine_js_1.writeQuarantine)(this.brain.paths.quarantine, session_id, log, 'whole');
133
144
  await (0, fs_js_1.deleteJSON)(ruta);
134
145
  const manifest = await this.brain.getManifest();
135
146
  await this.brain.updateManifest({ total_sessions: Math.max(0, manifest.total_sessions - 1) });