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.
- package/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/engine/doctor.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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');
|
package/dist/engine/heat.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
|
145
|
+
// Second pass — calculate heat
|
|
86
146
|
const hotTopics = [];
|
|
87
|
-
for (const
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
await (0,
|
|
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) });
|