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,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeReference = sanitizeReference;
4
+ exports.readReference = readReference;
5
+ exports.sanitizeTemporal = sanitizeTemporal;
6
+ exports.makeVerification = makeVerification;
7
+ exports.readVerification = readVerification;
8
+ exports.mergeCheck = mergeCheck;
9
+ exports.mergeFactEvidence = mergeFactEvidence;
10
+ exports.mergeEntryEvidence = mergeEntryEvidence;
11
+ exports.describeEvidence = describeEvidence;
12
+ const secrets_js_1 = require("./secrets.js");
13
+ const shelf_js_1 = require("./shelf.js");
14
+ function safeText(value) {
15
+ if (typeof value !== 'string' || !value.trim() || value.length > 4096 || /[\u0000-\u001f]/.test(value)) {
16
+ throw new Error('Invalid source reference text.');
17
+ }
18
+ return (0, secrets_js_1.redact)(value.trim()).text;
19
+ }
20
+ /** Solo valida y redacta datos. Nunca abre archivos, URLs ni procesos. */
21
+ function sanitizeReference(value) {
22
+ if (!value || typeof value !== 'object')
23
+ throw new Error('Invalid source reference.');
24
+ const r = value;
25
+ if (r.kind !== 'file' && r.kind !== 'url' && r.kind !== 'person')
26
+ throw new Error('Invalid reference kind.');
27
+ const clean = safeText(r.value);
28
+ if (r.kind === 'url') {
29
+ let url;
30
+ try {
31
+ url = new URL(String(r.value));
32
+ }
33
+ catch {
34
+ throw new Error('Invalid reference URL.');
35
+ }
36
+ const secretKey = /token|secret|password|passwd|api.?key|authorization|credential|signature|^sig$|access.?key/i;
37
+ const unsafeParams = (params) => [...params].some(([key, value]) => secretKey.test(key) || (0, secrets_js_1.redact)(value).found.length > 0);
38
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || clean !== String(r.value).trim()
39
+ || unsafeParams(url.searchParams) || unsafeParams(new URLSearchParams(url.hash.slice(1)))) {
40
+ throw new Error('Unsafe reference URL: credentials or secret parameters are not allowed.');
41
+ }
42
+ }
43
+ return {
44
+ kind: r.kind, value: clean,
45
+ ...(r.field !== undefined ? { field: safeText(r.field) } : {}),
46
+ ...(r.project !== undefined ? { project: safeText(r.project) } : {}),
47
+ };
48
+ }
49
+ function readReference(value) {
50
+ try {
51
+ return sanitizeReference(value);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function sanitizeTemporal(value) {
58
+ if (value === undefined || value === 'current' || value === 'historical')
59
+ return value;
60
+ throw new Error('Invalid temporal value.');
61
+ }
62
+ function makeVerification(evidence, at) {
63
+ if (!Number.isFinite(Date.parse(at)))
64
+ throw new Error('Invalid verification date.');
65
+ if (evidence === undefined)
66
+ return { method: 'unspecified', at };
67
+ if (!evidence || (evidence.method !== 'source' && evidence.method !== 'user'))
68
+ throw new Error('Invalid verification method.');
69
+ const reference = evidence.reference === undefined ? undefined : sanitizeReference(evidence.reference);
70
+ if (evidence.method === 'source' && !reference)
71
+ throw new Error('Source verification requires a reference.');
72
+ return { method: evidence.method, at, ...(reference ? { reference } : {}) };
73
+ }
74
+ function readVerification(value) {
75
+ if (!value || typeof value !== 'object')
76
+ return undefined;
77
+ const v = value;
78
+ if (typeof v.at !== 'string' || !(0, shelf_js_1.latestOf)(undefined, v.at))
79
+ return undefined;
80
+ try {
81
+ if (v.method === 'unspecified')
82
+ return { method: 'unspecified', at: v.at };
83
+ return makeVerification(v, v.at);
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ /** Fecha y evidencia forman una unidad: una fecha legacy nueva no hereda una fuente vieja. */
90
+ function mergeCheck(a, b) {
91
+ const invalidated = (0, shelf_js_1.latestOf)(a.verification_invalidated_at, b.verification_invalidated_at);
92
+ const cutoff = invalidated ? Date.parse(invalidated) : -Infinity;
93
+ const candidates = [];
94
+ for (const c of [a, b]) {
95
+ if (typeof c.verified === 'string' && (0, shelf_js_1.latestOf)(undefined, c.verified))
96
+ candidates.push({ at: c.verified });
97
+ const v = readVerification(c.verification);
98
+ if (v)
99
+ candidates.push({ at: v.at, verification: v });
100
+ }
101
+ candidates.sort((x, y) => Date.parse(x.at) - Date.parse(y.at)
102
+ || compare(JSON.stringify(x.verification || null), JSON.stringify(y.verification || null))
103
+ || compare(x.at, y.at));
104
+ const chosen = candidates.filter(c => Date.parse(c.at) > cutoff).at(-1);
105
+ return {
106
+ ...(invalidated ? { verification_invalidated_at: invalidated } : {}),
107
+ ...(chosen ? { verified: chosen.at, ...(chosen.verification ? { verification: chosen.verification } : {}) } : {}),
108
+ };
109
+ }
110
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
111
+ /** Unión estable de metadatos sin fecha propia. Current gana para no ocultar caducidad. */
112
+ function mergeFactEvidence(target, other) {
113
+ const check = mergeCheck(target, other);
114
+ const resetTime = (f) => Date.parse((0, shelf_js_1.latestOf)(undefined, f.verification_invalidated_at) || '') || 0;
115
+ const a = resetTime(target), b = resetTime(other);
116
+ const metadata = a === b ? [target, other] : [a > b ? target : other];
117
+ const references = metadata.map(f => readReference(f.reference))
118
+ .filter((r) => !!r).sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b)));
119
+ if (references.length)
120
+ target.reference = references.at(-1);
121
+ else
122
+ delete target.reference;
123
+ const temporal = metadata.map(f => f.temporal);
124
+ if (temporal.includes('current'))
125
+ target.temporal = 'current';
126
+ else if (temporal.includes('historical'))
127
+ target.temporal = 'historical';
128
+ else
129
+ delete target.temporal;
130
+ if (check.verification_invalidated_at)
131
+ target.verification_invalidated_at = check.verification_invalidated_at;
132
+ if (check.verified)
133
+ target.verified = check.verified;
134
+ else if (check.verification_invalidated_at)
135
+ delete target.verified;
136
+ if (check.verification)
137
+ target.verification = check.verification;
138
+ else
139
+ delete target.verification;
140
+ }
141
+ function mergeEntryEvidence(target, source, live) {
142
+ const dates = {};
143
+ const evidence = {};
144
+ for (const k of [...live].sort()) {
145
+ const check = mergeCheck({ verified: target.entry_verified?.[k], verification: target.entry_verification?.[k] }, { verified: source.entry_verified?.[k], verification: source.entry_verification?.[k] });
146
+ if (check.verified)
147
+ dates[k] = check.verified;
148
+ if (check.verification)
149
+ evidence[k] = check.verification;
150
+ }
151
+ if (Object.keys(dates).length)
152
+ target.entry_verified = dates;
153
+ else
154
+ delete target.entry_verified;
155
+ if (Object.keys(evidence).length)
156
+ target.entry_verification = evidence;
157
+ else
158
+ delete target.entry_verification;
159
+ }
160
+ /** Proyección para recall/inspect; no modifica el historial ni inventa fuentes legacy. */
161
+ function describeEvidence(neuron, id, kind, text) {
162
+ const f = kind === 'fact' ? neuron.facts.find(f => f.id === id || f.text === text) : undefined;
163
+ const check = mergeCheck(f || { verified: neuron.entry_verified?.[id], verification: neuron.entry_verification?.[id] }, {});
164
+ const reference = readReference(f?.reference);
165
+ const temporal = f?.temporal === 'current' || f?.temporal === 'historical' ? f.temporal : undefined;
166
+ return {
167
+ ...(reference ? { reference } : {}), ...(temporal ? { temporal } : {}),
168
+ ...(check.verified ? { verification: check.verification || { method: 'unspecified', at: check.verified } } : {}),
169
+ };
170
+ }
171
+ //# sourceMappingURL=evidence.js.map
@@ -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_';
@@ -35,50 +36,63 @@ class Hippocampus {
35
36
  const limpio = (0, secrets_js_1.redact)(data.summary);
36
37
  data = { ...data, summary: limpio.text };
37
38
  const id = (0, ids_js_1.sessionId)();
38
- // Check if session for today already exists — append to it
39
- let existing = await (0, fs_js_1.readJSON)(this.brain.paths.session(id));
40
- if (existing) {
41
- // Append to existing session
42
- existing.summary += `\n---\n${data.summary}`;
43
- existing.topics_touched = [...new Set([...existing.topics_touched, ...data.topics_touched])];
44
- existing.key_facts_added += data.key_facts_added || 0;
45
- existing.decisions_made += data.decisions_made || 0;
46
- existing.new_neurons_created += data.new_neurons_created || 0;
47
- existing.synapses_updated += data.synapses_updated || 0;
48
- await (0, fs_js_1.writeJSON)(this.brain.paths.session(id), existing);
49
- return { ...existing, redacted: limpio.found };
50
- }
51
- // Create new session log
52
- const session = {
53
- session_id: id,
54
- date: (0, fs_js_1.today)(),
55
- duration_estimate: 'unknown',
56
- topics_touched: data.topics_touched,
57
- summary: data.summary,
58
- key_facts_added: data.key_facts_added || 0,
59
- decisions_made: data.decisions_made || 0,
60
- new_neurons_created: data.new_neurons_created || 0,
61
- synapses_updated: data.synapses_updated || 0,
62
- };
63
- await (0, fs_js_1.writeJSON)(this.brain.paths.session(id), session);
39
+ let created = false;
40
+ const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.session(id), existing => {
41
+ if (existing) {
42
+ // Append to existing session
43
+ existing.summary += `\n---\n${data.summary}`;
44
+ existing.topics_touched = [...new Set([...existing.topics_touched, ...data.topics_touched])];
45
+ existing.key_facts_added += data.key_facts_added || 0;
46
+ existing.decisions_made += data.decisions_made || 0;
47
+ existing.new_neurons_created += data.new_neurons_created || 0;
48
+ existing.synapses_updated += data.synapses_updated || 0;
49
+ return existing;
50
+ }
51
+ // Create new session log
52
+ const session = {
53
+ session_id: id,
54
+ date: (0, fs_js_1.today)(),
55
+ duration_estimate: 'unknown',
56
+ topics_touched: data.topics_touched,
57
+ summary: data.summary,
58
+ key_facts_added: data.key_facts_added || 0,
59
+ decisions_made: data.decisions_made || 0,
60
+ new_neurons_created: data.new_neurons_created || 0,
61
+ synapses_updated: data.synapses_updated || 0,
62
+ };
63
+ created = true;
64
+ return session;
65
+ });
64
66
  // Update manifest
65
- const manifest = await this.brain.getManifest();
66
- await this.brain.updateManifest({ total_sessions: manifest.total_sessions + 1 });
67
- return { ...session, redacted: limpio.found };
67
+ if (created) {
68
+ const manifest = await this.brain.getManifest();
69
+ await this.brain.updateManifest({ total_sessions: manifest.total_sessions + 1 });
70
+ }
71
+ return { ...saved, redacted: limpio.found };
68
72
  }
69
73
  /**
70
74
  * List recent sessions.
71
75
  */
72
76
  async listSessions(limit = 10) {
73
- 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();
74
81
  const sessions = [];
75
82
  for (const id of ids) {
76
- 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 */ }
77
90
  if (session)
78
91
  sessions.push(session);
79
92
  }
80
- // Sort by date descending (newest first)
81
- 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 || '')));
82
96
  return sessions.slice(0, limit);
83
97
  }
84
98
  /**
@@ -124,9 +138,9 @@ class Hippocampus {
124
138
  const log = await (0, fs_js_1.readJSON)(ruta);
125
139
  if (!log)
126
140
  return { session_id, removed: false, backup: null };
127
- const sello = (0, fs_js_1.now)().replace(/[:.]/g, '-');
128
- const backup = `${this.brain.paths.quarantine}/${session_id}.${sello}.json`;
129
- 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');
130
144
  await (0, fs_js_1.deleteJSON)(ruta);
131
145
  const manifest = await this.brain.getManifest();
132
146
  await this.brain.updateManifest({ total_sessions: Math.max(0, manifest.total_sessions - 1) });