crbro-memory 1.16.0 → 2.0.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/README.md +57 -30
- package/dist/engine/brain.js +1 -1
- package/dist/engine/brain.js.map +1 -1
- package/dist/engine/cortex.d.ts +189 -1
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +639 -17
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/hippocampus.d.ts +27 -1
- package/dist/engine/hippocampus.d.ts.map +1 -1
- package/dist/engine/hippocampus.js +51 -2
- package/dist/engine/hippocampus.js.map +1 -1
- package/dist/engine/maintenance.d.ts +49 -2
- package/dist/engine/maintenance.d.ts.map +1 -1
- package/dist/engine/maintenance.js +301 -28
- package/dist/engine/maintenance.js.map +1 -1
- package/dist/engine/prefrontal.d.ts +25 -7
- package/dist/engine/prefrontal.d.ts.map +1 -1
- package/dist/engine/prefrontal.js +55 -27
- package/dist/engine/prefrontal.js.map +1 -1
- package/dist/engine/synapses.d.ts +38 -0
- package/dist/engine/synapses.d.ts.map +1 -1
- package/dist/engine/synapses.js +116 -11
- package/dist/engine/synapses.js.map +1 -1
- package/dist/search/index.d.ts +14 -2
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +40 -6
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +755 -829
- package/dist/server.js.map +1 -1
- package/dist/sync/materialize.d.ts +1 -1
- package/dist/sync/materialize.d.ts.map +1 -1
- package/dist/sync/materialize.js +40 -3
- package/dist/sync/materialize.js.map +1 -1
- package/dist/sync/ops.d.ts +9 -4
- package/dist/sync/ops.d.ts.map +1 -1
- package/dist/sync/ops.js.map +1 -1
- package/dist/sync/space.d.ts +27 -0
- package/dist/sync/space.d.ts.map +1 -1
- package/dist/sync/space.js +113 -1
- package/dist/sync/space.js.map +1 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/engine/cortex.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Neuron CRUD — create, read, update, list neurons
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.Cortex = void 0;
|
|
6
|
+
exports.unionNeuron = unionNeuron;
|
|
6
7
|
exports.normalizeKeys = normalizeKeys;
|
|
7
8
|
const fs_js_1 = require("../utils/fs.js");
|
|
8
9
|
const ids_js_1 = require("../utils/ids.js");
|
|
@@ -62,6 +63,171 @@ function sameNumbers(a, b) {
|
|
|
62
63
|
const nb = b.match(/\d+/g) || [];
|
|
63
64
|
return na.length === nb.length && na.every((v, i) => v === nb[i]);
|
|
64
65
|
}
|
|
66
|
+
/** Where a fact's lifecycle stands, for "take the furthest-along status". */
|
|
67
|
+
function statusRank(status) {
|
|
68
|
+
return status === 'retracted' ? 2 : status === 'superseded' ? 1 : 0;
|
|
69
|
+
}
|
|
70
|
+
function earliestOf(a, b) {
|
|
71
|
+
if (!a)
|
|
72
|
+
return b || '';
|
|
73
|
+
if (!b)
|
|
74
|
+
return a;
|
|
75
|
+
return a < b ? a : b;
|
|
76
|
+
}
|
|
77
|
+
function copyFact(f) {
|
|
78
|
+
const out = { ...f };
|
|
79
|
+
if (f.keys)
|
|
80
|
+
out.keys = [...f.keys];
|
|
81
|
+
if (f.supersedes)
|
|
82
|
+
out.supersedes = [...f.supersedes];
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Content hashes of every entry that can carry a date or a retirement. */
|
|
86
|
+
function liveEntryKeys(n) {
|
|
87
|
+
return new Set([
|
|
88
|
+
...n.decisions.map(d => d.text || ''),
|
|
89
|
+
...n.patterns, ...n.preferences,
|
|
90
|
+
...(n.errors || []), ...(n.debts || []),
|
|
91
|
+
].map(ops_js_1.entryId));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Everything of `source` folded into `target`, as a new object.
|
|
95
|
+
*
|
|
96
|
+
* Shared by restoreNeuron (a quarantine copy back over a neuron that was
|
|
97
|
+
* re-created since) and mergeNeurons (two neurons that turned out to be one
|
|
98
|
+
* topic). Pure on purpose: neither input is touched, so the caller can diff
|
|
99
|
+
* the result against what it had and emit only what actually moved.
|
|
100
|
+
*
|
|
101
|
+
* Facts line up by id (the content hash), falling back to normalised text;
|
|
102
|
+
* on a collision the target's telling stays but takes the furthest-along
|
|
103
|
+
* status (active < superseded < retracted) and the earliest `added`, and the
|
|
104
|
+
* keys are unioned. Everything else is a set union keyed by normalised text
|
|
105
|
+
* with the target's order first. Local observations (heat, access_count)
|
|
106
|
+
* combine as max and sum; identity (id, name, domain, type) is the target's.
|
|
107
|
+
*/
|
|
108
|
+
function unionNeuron(target, source) {
|
|
109
|
+
const moved = { facts: 0, decisions: 0, patterns: 0, preferences: 0, errors: 0, debts: 0, tags: 0, map: 0 };
|
|
110
|
+
// ── Facts ──
|
|
111
|
+
const facts = target.facts.map(copyFact);
|
|
112
|
+
const byId = new Map();
|
|
113
|
+
const byText = new Map();
|
|
114
|
+
for (const f of facts) {
|
|
115
|
+
byId.set(f.id || (0, hash_js_1.factId)(f.text), f);
|
|
116
|
+
byText.set((0, ops_js_1.normalizeText)(f.text), f);
|
|
117
|
+
}
|
|
118
|
+
for (const sf of source.facts || []) {
|
|
119
|
+
const sid = sf.id || (0, hash_js_1.factId)(sf.text);
|
|
120
|
+
const hit = byId.get(sid) || byText.get((0, ops_js_1.normalizeText)(sf.text));
|
|
121
|
+
if (!hit) {
|
|
122
|
+
const copia = copyFact(sf);
|
|
123
|
+
copia.id = sid;
|
|
124
|
+
facts.push(copia);
|
|
125
|
+
byId.set(sid, copia);
|
|
126
|
+
byText.set((0, ops_js_1.normalizeText)(sf.text), copia);
|
|
127
|
+
moved.facts++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (statusRank(sf.status) > statusRank(hit.status)) {
|
|
131
|
+
hit.status = sf.status;
|
|
132
|
+
if (sf.revised)
|
|
133
|
+
hit.revised = sf.revised;
|
|
134
|
+
if (sf.revision_note)
|
|
135
|
+
hit.revision_note = sf.revision_note;
|
|
136
|
+
if (sf.superseded_by)
|
|
137
|
+
hit.superseded_by = sf.superseded_by;
|
|
138
|
+
}
|
|
139
|
+
hit.added = earliestOf(hit.added, sf.added);
|
|
140
|
+
const keys = normalizeKeys([...(hit.keys || []), ...(sf.keys || [])]);
|
|
141
|
+
if (keys.length)
|
|
142
|
+
hit.keys = keys;
|
|
143
|
+
else
|
|
144
|
+
delete hit.keys;
|
|
145
|
+
}
|
|
146
|
+
// ── Decisions: by normalised text, the target's rationale wins ──
|
|
147
|
+
const decisions = target.decisions.map(d => ({ ...d }));
|
|
148
|
+
const vistas = new Set(decisions.map(d => (0, ops_js_1.normalizeText)(d.text)));
|
|
149
|
+
for (const d of source.decisions || []) {
|
|
150
|
+
const k = (0, ops_js_1.normalizeText)(d.text);
|
|
151
|
+
if (vistas.has(k))
|
|
152
|
+
continue;
|
|
153
|
+
vistas.add(k);
|
|
154
|
+
decisions.push({ ...d });
|
|
155
|
+
moved.decisions++;
|
|
156
|
+
}
|
|
157
|
+
// ── Plain string lists: set union, target order first ──
|
|
158
|
+
const unir = (a, b) => {
|
|
159
|
+
const out = [...a];
|
|
160
|
+
const seen = new Set(a.map(ops_js_1.normalizeText));
|
|
161
|
+
let added = 0;
|
|
162
|
+
for (const t of b) {
|
|
163
|
+
const k = (0, ops_js_1.normalizeText)(t);
|
|
164
|
+
if (seen.has(k))
|
|
165
|
+
continue;
|
|
166
|
+
seen.add(k);
|
|
167
|
+
out.push(t);
|
|
168
|
+
added++;
|
|
169
|
+
}
|
|
170
|
+
return { out, added };
|
|
171
|
+
};
|
|
172
|
+
const patterns = unir(target.patterns || [], source.patterns || []);
|
|
173
|
+
const preferences = unir(target.preferences || [], source.preferences || []);
|
|
174
|
+
const errors = unir(target.errors || [], source.errors || []);
|
|
175
|
+
const debts = unir(target.debts || [], source.debts || []);
|
|
176
|
+
const tags = unir(target.tags || [], source.tags || []);
|
|
177
|
+
moved.patterns = patterns.added;
|
|
178
|
+
moved.preferences = preferences.added;
|
|
179
|
+
moved.errors = errors.added;
|
|
180
|
+
moved.debts = debts.added;
|
|
181
|
+
moved.tags = tags.added;
|
|
182
|
+
const neuron = {
|
|
183
|
+
...target,
|
|
184
|
+
facts,
|
|
185
|
+
decisions,
|
|
186
|
+
patterns: patterns.out,
|
|
187
|
+
preferences: preferences.out,
|
|
188
|
+
errors: errors.out,
|
|
189
|
+
debts: debts.out,
|
|
190
|
+
tags: tags.out,
|
|
191
|
+
connections: [...new Set([...(target.connections || []), ...(source.connections || [])])]
|
|
192
|
+
.filter(c => c !== target.id && c !== source.id),
|
|
193
|
+
summary: target.summary || source.summary || '',
|
|
194
|
+
heat: Math.max(target.heat || 0, source.heat || 0),
|
|
195
|
+
access_count: (target.access_count || 0) + (source.access_count || 0),
|
|
196
|
+
created: earliestOf(target.created, source.created) || (0, fs_js_1.now)(),
|
|
197
|
+
last_accessed: (0, fs_js_1.now)(),
|
|
198
|
+
};
|
|
199
|
+
// ── Sidecars: union, then pruned to what is actually in the neuron ──
|
|
200
|
+
const vivos = liveEntryKeys(neuron);
|
|
201
|
+
const fechas = {};
|
|
202
|
+
for (const [k, v] of Object.entries(source.entry_dates || {}))
|
|
203
|
+
fechas[k] = v;
|
|
204
|
+
for (const [k, v] of Object.entries(target.entry_dates || {}))
|
|
205
|
+
fechas[k] = earliestOf(fechas[k], v);
|
|
206
|
+
const fechasVivas = {};
|
|
207
|
+
for (const k of Object.keys(fechas).sort())
|
|
208
|
+
if (vivos.has(k))
|
|
209
|
+
fechasVivas[k] = fechas[k];
|
|
210
|
+
neuron.entry_dates = fechasVivas;
|
|
211
|
+
const estados = { ...(source.entry_status || {}), ...(target.entry_status || {}) };
|
|
212
|
+
const estadosVivos = {};
|
|
213
|
+
for (const k of Object.keys(estados).sort())
|
|
214
|
+
if (vivos.has(k))
|
|
215
|
+
estadosVivos[k] = estados[k];
|
|
216
|
+
if (Object.keys(estadosVivos).length)
|
|
217
|
+
neuron.entry_status = estadosVivos;
|
|
218
|
+
else
|
|
219
|
+
delete neuron.entry_status;
|
|
220
|
+
// ── Map: the target's if it has one, else the source's ──
|
|
221
|
+
if (target.map)
|
|
222
|
+
neuron.map = { ...target.map };
|
|
223
|
+
else if (source.map) {
|
|
224
|
+
neuron.map = { ...source.map };
|
|
225
|
+
moved.map = 1;
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
delete neuron.map;
|
|
229
|
+
return { neuron, moved };
|
|
230
|
+
}
|
|
65
231
|
/**
|
|
66
232
|
* Aliases a future question may use, as stored: trimmed, lower-cased, at
|
|
67
233
|
* most 40 characters each, unique, at most eight. Order is kept.
|
|
@@ -87,6 +253,7 @@ class Cortex {
|
|
|
87
253
|
*/
|
|
88
254
|
indexer = null;
|
|
89
255
|
emitter = null;
|
|
256
|
+
remover = null;
|
|
90
257
|
/** What this session has actually written, for an honest consolidate(). */
|
|
91
258
|
tally = { facts: 0, decisions: 0, topics: new Set() };
|
|
92
259
|
constructor(brain) {
|
|
@@ -109,6 +276,37 @@ class Cortex {
|
|
|
109
276
|
setEmitter(emitter) {
|
|
110
277
|
this.emitter = emitter;
|
|
111
278
|
}
|
|
279
|
+
setRemover(remover) {
|
|
280
|
+
this.remover = remover;
|
|
281
|
+
}
|
|
282
|
+
async unindex(neuronId) {
|
|
283
|
+
if (!this.remover)
|
|
284
|
+
return;
|
|
285
|
+
try {
|
|
286
|
+
await this.remover(neuronId);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// The index is derived data; a stale chunk is a nuisance, not a loss.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Copy a whole neuron to .quarantine/ with a timestamp, before anything
|
|
294
|
+
* destructive. Same stamp format everywhere, so restoreNeuron can pick the
|
|
295
|
+
* newest copy by name alone.
|
|
296
|
+
*/
|
|
297
|
+
async quarantine(neuron) {
|
|
298
|
+
const sello = (0, fs_js_1.now)().replace(/[:.]/g, '-');
|
|
299
|
+
const backup = `${this.brain.paths.quarantine}/${neuron.id}.${sello}.json`;
|
|
300
|
+
await (0, fs_js_1.writeJSON)(backup, neuron);
|
|
301
|
+
return backup;
|
|
302
|
+
}
|
|
303
|
+
/** Remove the neuron file, drop its chunks, and keep the manifest honest. */
|
|
304
|
+
async deleteNeuronFile(id) {
|
|
305
|
+
await (0, fs_js_1.deleteJSON)(this.brain.paths.neuron(id));
|
|
306
|
+
await this.unindex(id);
|
|
307
|
+
const manifest = await this.brain.getManifest();
|
|
308
|
+
await this.brain.updateManifest({ total_neurons: Math.max(0, manifest.total_neurons - 1) });
|
|
309
|
+
}
|
|
112
310
|
async emit(neuronId, change) {
|
|
113
311
|
if (!this.emitter)
|
|
114
312
|
return;
|
|
@@ -292,7 +490,11 @@ class Cortex {
|
|
|
292
490
|
}
|
|
293
491
|
if (!neuron) {
|
|
294
492
|
if (options?.createIfMissing === false) {
|
|
295
|
-
return {
|
|
493
|
+
return {
|
|
494
|
+
neuron: null, action: 'skipped', superseded: 0,
|
|
495
|
+
supersedes_unmatched: options?.supersedes || [], near_duplicates: [], redacted: limpio.found,
|
|
496
|
+
duplicate: false, updated_in_place: false, skipped_retired: null,
|
|
497
|
+
};
|
|
296
498
|
}
|
|
297
499
|
const nType = options?.neuronType || (0, ids_js_1.inferNeuronType)(topic);
|
|
298
500
|
const domain = options?.domain || 'general';
|
|
@@ -306,6 +508,19 @@ class Cortex {
|
|
|
306
508
|
let supersedesUnmatched = [];
|
|
307
509
|
let nearDuplicates = [];
|
|
308
510
|
let emitir = null;
|
|
511
|
+
let duplicate = false;
|
|
512
|
+
let updatedInPlace = false;
|
|
513
|
+
let skippedRetired = null;
|
|
514
|
+
// A retired decision, pattern, error or debt must not come back through
|
|
515
|
+
// learn: the sidecar says so, and the index already hides it.
|
|
516
|
+
const retirada = (n, text) => {
|
|
517
|
+
const k = (0, ops_js_1.entryId)(text);
|
|
518
|
+
const est = n.entry_status?.[k];
|
|
519
|
+
if (!est)
|
|
520
|
+
return false;
|
|
521
|
+
skippedRetired = { id: k, status: est.status, revised: est.revised, note: est.note };
|
|
522
|
+
return true;
|
|
523
|
+
};
|
|
309
524
|
// From here on we work on a fresh read inside the lock. Mutating the copy
|
|
310
525
|
// fetched a moment ago and saving it over the top is exactly how a
|
|
311
526
|
// concurrent writer's facts disappeared.
|
|
@@ -323,16 +538,53 @@ class Cortex {
|
|
|
323
538
|
const id = (0, hash_js_1.factId)(content);
|
|
324
539
|
const keys = normalizeKeys(options?.keys);
|
|
325
540
|
const existente = n.facts.find(f => f.text.toLowerCase() === content.toLowerCase());
|
|
541
|
+
if (existente && (existente.status === 'superseded' || existente.status === 'retracted')) {
|
|
542
|
+
// The same line was deliberately retired. Re-adding it would
|
|
543
|
+
// undo that revision by accident; report it and write nothing.
|
|
544
|
+
skippedRetired = {
|
|
545
|
+
id: existente.id || (0, hash_js_1.factId)(existente.text),
|
|
546
|
+
status: existente.status,
|
|
547
|
+
revised: existente.revised,
|
|
548
|
+
note: existente.revision_note,
|
|
549
|
+
};
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
326
552
|
const isDuplicate = existente !== undefined;
|
|
327
|
-
if (existente
|
|
328
|
-
// The same line again
|
|
329
|
-
// The indexer re-indexes the neuron
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
553
|
+
if (existente) {
|
|
554
|
+
// The same line again: no sibling, but confidence and aliases
|
|
555
|
+
// may be edited in place. The indexer re-indexes the neuron
|
|
556
|
+
// afterwards, keys included.
|
|
557
|
+
duplicate = true;
|
|
558
|
+
let cambiado = false;
|
|
559
|
+
if (options?.confidence !== undefined && options.confidence !== existente.confidence) {
|
|
560
|
+
existente.confidence = options.confidence;
|
|
561
|
+
cambiado = true;
|
|
562
|
+
}
|
|
563
|
+
if (options?.keysReplace) {
|
|
564
|
+
const actuales = existente.keys || [];
|
|
565
|
+
if (keys.length !== actuales.length || keys.some((k, i) => k !== actuales[i])) {
|
|
566
|
+
if (keys.length)
|
|
567
|
+
existente.keys = keys;
|
|
568
|
+
else
|
|
569
|
+
delete existente.keys;
|
|
570
|
+
cambiado = true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else if (keys.length) {
|
|
574
|
+
const merged = normalizeKeys([...(existente.keys || []), ...keys]);
|
|
575
|
+
if (merged.length !== (existente.keys || []).length) {
|
|
576
|
+
existente.keys = merged;
|
|
577
|
+
cambiado = true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (cambiado) {
|
|
581
|
+
// Note for shared neurons: materialize takes max(conf) and
|
|
582
|
+
// unions keys, so lowering confidence or dropping an alias
|
|
583
|
+
// stays local. Documented, not fought — the log is append-only.
|
|
584
|
+
updatedInPlace = true;
|
|
333
585
|
this.tally.topics.add(n.id);
|
|
334
586
|
emitir = { kind: 'fact', text: existente.text, fid: existente.id || id,
|
|
335
|
-
conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys:
|
|
587
|
+
conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys: existente.keys };
|
|
336
588
|
}
|
|
337
589
|
}
|
|
338
590
|
if (!isDuplicate) {
|
|
@@ -367,6 +619,8 @@ class Cortex {
|
|
|
367
619
|
break;
|
|
368
620
|
}
|
|
369
621
|
case 'decision': {
|
|
622
|
+
if (retirada(n, content))
|
|
623
|
+
return null;
|
|
370
624
|
const decision = {
|
|
371
625
|
text: content,
|
|
372
626
|
date: (0, fs_js_1.now)(),
|
|
@@ -380,6 +634,8 @@ class Cortex {
|
|
|
380
634
|
break;
|
|
381
635
|
}
|
|
382
636
|
case 'pattern': {
|
|
637
|
+
if (retirada(n, content))
|
|
638
|
+
return null;
|
|
383
639
|
if (!n.patterns.includes(content)) {
|
|
384
640
|
n.patterns.push(content);
|
|
385
641
|
fechar(n, content);
|
|
@@ -395,6 +651,8 @@ class Cortex {
|
|
|
395
651
|
break;
|
|
396
652
|
}
|
|
397
653
|
case 'error': {
|
|
654
|
+
if (retirada(n, content))
|
|
655
|
+
return null;
|
|
398
656
|
if (!n.errors)
|
|
399
657
|
n.errors = [];
|
|
400
658
|
if (!n.errors.includes(content)) {
|
|
@@ -406,6 +664,8 @@ class Cortex {
|
|
|
406
664
|
break;
|
|
407
665
|
}
|
|
408
666
|
case 'debt': {
|
|
667
|
+
if (retirada(n, content))
|
|
668
|
+
return null;
|
|
409
669
|
if (!n.debts)
|
|
410
670
|
n.debts = [];
|
|
411
671
|
if (!n.debts.includes(content)) {
|
|
@@ -425,12 +685,24 @@ class Cortex {
|
|
|
425
685
|
return n;
|
|
426
686
|
});
|
|
427
687
|
const final = (actualizada || neuron);
|
|
688
|
+
if (skippedRetired) {
|
|
689
|
+
// Nothing was written, so nothing to index, emit or tally.
|
|
690
|
+
return {
|
|
691
|
+
neuron: final, action: 'skipped_retired', superseded: 0, supersedes_unmatched: [],
|
|
692
|
+
near_duplicates: [], redacted: limpio.found,
|
|
693
|
+
duplicate: false, updated_in_place: false, skipped_retired: skippedRetired,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
428
696
|
await this.reindex(final);
|
|
429
697
|
// Preferences are never emitted: they are the field most likely to hold a
|
|
430
698
|
// key and the least likely to be worth sharing.
|
|
431
699
|
if (emitir)
|
|
432
700
|
await this.emit(final.id, emitir);
|
|
433
|
-
return {
|
|
701
|
+
return {
|
|
702
|
+
neuron: final, action, superseded, supersedes_unmatched: supersedesUnmatched,
|
|
703
|
+
near_duplicates: nearDuplicates, redacted: limpio.found,
|
|
704
|
+
duplicate, updated_in_place: updatedInPlace, skipped_retired: null,
|
|
705
|
+
};
|
|
434
706
|
}
|
|
435
707
|
/**
|
|
436
708
|
* Mark facts as no longer current.
|
|
@@ -445,11 +717,14 @@ class Cortex {
|
|
|
445
717
|
const neuron = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
|
|
446
718
|
if (!neuron)
|
|
447
719
|
return { neuron: null, revised: 0, unmatched: targets.slice() };
|
|
720
|
+
const status = options?.status || 'superseded';
|
|
448
721
|
let revised = 0;
|
|
449
722
|
let unmatched = targets.slice();
|
|
450
723
|
const actualizada = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
|
|
451
724
|
const n = current || neuron;
|
|
452
|
-
const retirado =
|
|
725
|
+
const retirado = status === 'active'
|
|
726
|
+
? this.reactivate(n, targets, options?.note)
|
|
727
|
+
: this.retire(n, targets, options?.replacedBy, status, options?.note);
|
|
453
728
|
revised = retirado.count;
|
|
454
729
|
unmatched = retirado.unmatched;
|
|
455
730
|
if (revised === 0)
|
|
@@ -462,8 +737,12 @@ class Cortex {
|
|
|
462
737
|
// which is exactly the failure this feature exists to close.
|
|
463
738
|
const n = (actualizada || neuron);
|
|
464
739
|
await this.reindex(n);
|
|
465
|
-
// 'active' is not a retirement, so it is never emitted
|
|
466
|
-
|
|
740
|
+
// 'active' is not a retirement, so it is never emitted: StatusOp only
|
|
741
|
+
// moves forward, and on a shared neuron the next sync re-applies the
|
|
742
|
+
// retirement from the log. Reactivation is local; the server says so.
|
|
743
|
+
if (status === 'active')
|
|
744
|
+
return { neuron: n, revised, unmatched };
|
|
745
|
+
const estado = status === 'retracted' ? 'retracted' : 'superseded';
|
|
467
746
|
for (const t of targets) {
|
|
468
747
|
const f = n.facts.find(x => (x.id || '') === t || x.text.trim().toLowerCase() === t.trim().toLowerCase());
|
|
469
748
|
if (f?.id) {
|
|
@@ -541,6 +820,168 @@ class Cortex {
|
|
|
541
820
|
}
|
|
542
821
|
return { count, unmatched: targets.filter((_, i) => !hit.has(i)) };
|
|
543
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Bring retired facts back. The inverse of retire: only superseded or
|
|
825
|
+
* retracted facts match, by id or by trimmed lower-cased text. Clears
|
|
826
|
+
* superseded_by (the replacement no longer replaces it) and stamps the
|
|
827
|
+
* revision date; the note, if given, records why it came back.
|
|
828
|
+
*/
|
|
829
|
+
reactivate(neuron, targets, note) {
|
|
830
|
+
let count = 0;
|
|
831
|
+
const wanted = targets.map(t => t.trim().toLowerCase());
|
|
832
|
+
const hit = new Set();
|
|
833
|
+
for (const fact of neuron.facts) {
|
|
834
|
+
if (fact.status !== 'superseded' && fact.status !== 'retracted')
|
|
835
|
+
continue;
|
|
836
|
+
const fid = fact.id || (0, hash_js_1.factId)(fact.text);
|
|
837
|
+
const idPos = wanted.indexOf(fid.toLowerCase());
|
|
838
|
+
const textPos = wanted.indexOf(fact.text.trim().toLowerCase());
|
|
839
|
+
if (idPos === -1 && textPos === -1)
|
|
840
|
+
continue;
|
|
841
|
+
if (idPos !== -1)
|
|
842
|
+
hit.add(idPos);
|
|
843
|
+
if (textPos !== -1)
|
|
844
|
+
hit.add(textPos);
|
|
845
|
+
fact.id = fid;
|
|
846
|
+
fact.status = 'active';
|
|
847
|
+
delete fact.superseded_by;
|
|
848
|
+
fact.revised = (0, fs_js_1.now)();
|
|
849
|
+
if (note)
|
|
850
|
+
fact.revision_note = note;
|
|
851
|
+
count++;
|
|
852
|
+
}
|
|
853
|
+
return { count, unmatched: targets.filter((_, i) => !hit.has(i)) };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Retire — or reactivate — decisions, patterns, errors and debts by exact
|
|
857
|
+
* text (trimmed, case-insensitive). These are plain strings, so their
|
|
858
|
+
* status lives in the entry_status sidecar keyed by entryId(text), the
|
|
859
|
+
* same way their dates do. A retired entry stays in the file and leaves
|
|
860
|
+
* recall exactly like a superseded fact; status 'active' deletes the key.
|
|
861
|
+
*
|
|
862
|
+
* Never emitted: the sidecar travels only through the local file and
|
|
863
|
+
* survives a sync because applyOps copies and prunes it.
|
|
864
|
+
*/
|
|
865
|
+
async retireEntries(neuronRef, targets, options) {
|
|
866
|
+
const neuron = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
|
|
867
|
+
if (!neuron)
|
|
868
|
+
return { neuron: null, revised: 0, unmatched: targets.slice() };
|
|
869
|
+
const status = options?.status || 'superseded';
|
|
870
|
+
const wanted = targets.map(t => (0, ops_js_1.normalizeText)(t).toLowerCase());
|
|
871
|
+
let revised = 0;
|
|
872
|
+
let unmatched = targets.slice();
|
|
873
|
+
const actualizada = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
|
|
874
|
+
const n = current || neuron;
|
|
875
|
+
const estados = n.entry_status || {};
|
|
876
|
+
const hit = new Set();
|
|
877
|
+
const cuando = (0, fs_js_1.now)();
|
|
878
|
+
const textos = [
|
|
879
|
+
...n.decisions.map(d => d.text || ''),
|
|
880
|
+
...n.patterns,
|
|
881
|
+
...(n.errors || []),
|
|
882
|
+
...(n.debts || []),
|
|
883
|
+
];
|
|
884
|
+
for (const t of textos) {
|
|
885
|
+
const pos = wanted.indexOf((0, ops_js_1.normalizeText)(t).toLowerCase());
|
|
886
|
+
if (pos === -1)
|
|
887
|
+
continue;
|
|
888
|
+
const k = (0, ops_js_1.entryId)(t);
|
|
889
|
+
const retirado = estados[k] !== undefined;
|
|
890
|
+
if (status === 'active') {
|
|
891
|
+
// Only a retired entry can come back.
|
|
892
|
+
if (!retirado)
|
|
893
|
+
continue;
|
|
894
|
+
delete estados[k];
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
// An already-retired entry never matches again.
|
|
898
|
+
if (retirado)
|
|
899
|
+
continue;
|
|
900
|
+
const est = { status: status, revised: cuando };
|
|
901
|
+
if (options?.note)
|
|
902
|
+
est.note = options.note;
|
|
903
|
+
estados[k] = est;
|
|
904
|
+
}
|
|
905
|
+
hit.add(pos);
|
|
906
|
+
revised++;
|
|
907
|
+
}
|
|
908
|
+
unmatched = targets.filter((_, i) => !hit.has(i));
|
|
909
|
+
if (revised === 0)
|
|
910
|
+
return null;
|
|
911
|
+
if (Object.keys(estados).length)
|
|
912
|
+
n.entry_status = estados;
|
|
913
|
+
else
|
|
914
|
+
delete n.entry_status;
|
|
915
|
+
n.last_accessed = cuando;
|
|
916
|
+
return n;
|
|
917
|
+
});
|
|
918
|
+
const n = (actualizada || neuron);
|
|
919
|
+
// The index drops retired entries (and picks reactivated ones back up).
|
|
920
|
+
if (revised > 0)
|
|
921
|
+
await this.reindex(n);
|
|
922
|
+
return { neuron: n, revised, unmatched };
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Edit a neuron's metadata: summary, domain, tags, name.
|
|
926
|
+
*
|
|
927
|
+
* Only fields that actually differ are written and reported; a call that
|
|
928
|
+
* changes nothing writes nothing. `tags` replaces the whole list — protocol
|
|
929
|
+
* neurons keep their 'priority:'/'source:' tags only if the caller re-sends
|
|
930
|
+
* them. `name` changes the display name only: the id, and with it the file,
|
|
931
|
+
* the synapses, the shared map and the connections, never move. `domain`
|
|
932
|
+
* replaces unconditionally, unlike learn, which only fills in 'general'.
|
|
933
|
+
*
|
|
934
|
+
* Not emitted: NeuronOp is only an announcement, and metadata is local.
|
|
935
|
+
*/
|
|
936
|
+
async setMeta(neuronRef, meta) {
|
|
937
|
+
const neuron = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
|
|
938
|
+
if (!neuron)
|
|
939
|
+
return { neuron: null, changed: [], redacted: [] };
|
|
940
|
+
let redacted = [];
|
|
941
|
+
let summary;
|
|
942
|
+
if (meta.summary !== undefined) {
|
|
943
|
+
const limpio = (0, secrets_js_1.redact)(meta.summary);
|
|
944
|
+
summary = limpio.text;
|
|
945
|
+
redacted = limpio.found;
|
|
946
|
+
}
|
|
947
|
+
const tags = meta.tags === undefined
|
|
948
|
+
? undefined
|
|
949
|
+
: [...new Set(meta.tags.map(t => String(t || '').trim()).filter(Boolean))];
|
|
950
|
+
const domain = meta.domain === undefined ? undefined : meta.domain.trim();
|
|
951
|
+
const name = meta.name === undefined ? undefined : meta.name.trim();
|
|
952
|
+
const changed = [];
|
|
953
|
+
const actualizada = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
|
|
954
|
+
const n = current || neuron;
|
|
955
|
+
if (summary !== undefined && summary !== n.summary) {
|
|
956
|
+
n.summary = summary;
|
|
957
|
+
changed.push('summary');
|
|
958
|
+
}
|
|
959
|
+
if (domain !== undefined && domain && domain !== n.domain) {
|
|
960
|
+
n.domain = domain;
|
|
961
|
+
changed.push('domain');
|
|
962
|
+
}
|
|
963
|
+
if (tags !== undefined) {
|
|
964
|
+
const actuales = n.tags || [];
|
|
965
|
+
if (tags.length !== actuales.length || tags.some((t, i) => t !== actuales[i])) {
|
|
966
|
+
n.tags = tags;
|
|
967
|
+
changed.push('tags');
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (name !== undefined && name && name !== n.name) {
|
|
971
|
+
n.name = name;
|
|
972
|
+
changed.push('name');
|
|
973
|
+
}
|
|
974
|
+
if (changed.length === 0)
|
|
975
|
+
return null;
|
|
976
|
+
n.last_accessed = (0, fs_js_1.now)();
|
|
977
|
+
return n;
|
|
978
|
+
});
|
|
979
|
+
const n = (actualizada || neuron);
|
|
980
|
+
// The header chunk carries name and tags; the summary is indexed too.
|
|
981
|
+
if (changed.length > 0)
|
|
982
|
+
await this.reindex(n);
|
|
983
|
+
return { neuron: n, changed, redacted };
|
|
984
|
+
}
|
|
544
985
|
/**
|
|
545
986
|
* Replace the neuron's system map.
|
|
546
987
|
*
|
|
@@ -613,9 +1054,10 @@ class Cortex {
|
|
|
613
1054
|
}
|
|
614
1055
|
// Sort by heat (descending)
|
|
615
1056
|
neurons.sort((a, b) => b.heat - a.heat);
|
|
616
|
-
// Apply limit
|
|
1057
|
+
// Apply offset, then limit
|
|
617
1058
|
const limit = options?.limit || 50;
|
|
618
|
-
|
|
1059
|
+
const offset = Math.max(0, options?.offset || 0);
|
|
1060
|
+
return neurons.slice(offset, offset + limit);
|
|
619
1061
|
}
|
|
620
1062
|
/**
|
|
621
1063
|
* Update a neuron's summary.
|
|
@@ -663,9 +1105,7 @@ class Cortex {
|
|
|
663
1105
|
const found = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
|
|
664
1106
|
if (!found)
|
|
665
1107
|
return { neuron_id: null, removed: 0, backup: null };
|
|
666
|
-
const
|
|
667
|
-
const backup = `${this.brain.paths.quarantine}/${found.id}.${sello}.json`;
|
|
668
|
-
await (0, fs_js_1.writeJSON)(backup, found);
|
|
1108
|
+
const backup = await this.quarantine(found);
|
|
669
1109
|
let removed = 0;
|
|
670
1110
|
const wanted = targets.map(t => t.trim().toLowerCase());
|
|
671
1111
|
const after = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(found.id), current => {
|
|
@@ -716,6 +1156,17 @@ class Cortex {
|
|
|
716
1156
|
delete current.entry_dates[k];
|
|
717
1157
|
}
|
|
718
1158
|
}
|
|
1159
|
+
// Same for retirements: a key whose entry is gone is pruned, so the
|
|
1160
|
+
// sidecar cannot hide a line that no longer exists.
|
|
1161
|
+
if (current.entry_status) {
|
|
1162
|
+
const vivos = liveEntryKeys(current);
|
|
1163
|
+
for (const k of Object.keys(current.entry_status)) {
|
|
1164
|
+
if (!vivos.has(k))
|
|
1165
|
+
delete current.entry_status[k];
|
|
1166
|
+
}
|
|
1167
|
+
if (Object.keys(current.entry_status).length === 0)
|
|
1168
|
+
delete current.entry_status;
|
|
1169
|
+
}
|
|
719
1170
|
current.last_accessed = (0, fs_js_1.now)();
|
|
720
1171
|
return current;
|
|
721
1172
|
});
|
|
@@ -733,6 +1184,16 @@ class Cortex {
|
|
|
733
1184
|
await this.emit(found.id, { kind: 'status', fid: f.id || (0, hash_js_1.factId)(f.text), to: 'retracted', at: cuando, why: 'forgotten' });
|
|
734
1185
|
}
|
|
735
1186
|
}
|
|
1187
|
+
for (const d of found.decisions || []) {
|
|
1188
|
+
if (wanted.includes((d.text || '').trim().toLowerCase())) {
|
|
1189
|
+
await this.emit(found.id, { kind: 'decision_purge', key: (0, ops_js_1.entryId)(d.text), at: cuando });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
for (const p of found.patterns || []) {
|
|
1193
|
+
if (wanted.includes((p || '').trim().toLowerCase())) {
|
|
1194
|
+
await this.emit(found.id, { kind: 'pattern_purge', key: (0, ops_js_1.entryId)(p), at: cuando });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
736
1197
|
for (const e of found.errors || []) {
|
|
737
1198
|
if (wanted.includes((e || '').trim().toLowerCase())) {
|
|
738
1199
|
await this.emit(found.id, { kind: 'error_purge', key: (0, ops_js_1.entryId)(e), at: cuando });
|
|
@@ -749,6 +1210,167 @@ class Cortex {
|
|
|
749
1210
|
}
|
|
750
1211
|
return { neuron_id: found.id, removed, backup: removed > 0 ? backup : null };
|
|
751
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Delete a whole neuron. Quarantine copy first, then the file goes, the
|
|
1215
|
+
* index drops its chunks and the manifest is decremented.
|
|
1216
|
+
*
|
|
1217
|
+
* Synapses are not touched here (the Cortex has no Synapses): the server
|
|
1218
|
+
* calls synapses.removeAllFor(id) right after. Nor is shared state checked
|
|
1219
|
+
* — the server refuses beforehand when the neuron is shared, because the
|
|
1220
|
+
* next sync would simply re-create it from the team log. Nothing is
|
|
1221
|
+
* emitted: there is no neuron-delete op.
|
|
1222
|
+
*/
|
|
1223
|
+
async forgetNeuron(neuronRef) {
|
|
1224
|
+
const found = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
|
|
1225
|
+
if (!found) {
|
|
1226
|
+
return {
|
|
1227
|
+
neuron_id: null, backup: null,
|
|
1228
|
+
counts: { facts: 0, decisions: 0, patterns: 0, preferences: 0, errors: 0, debts: 0, connections: 0 },
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const backup = await this.quarantine(found);
|
|
1232
|
+
await this.deleteNeuronFile(found.id);
|
|
1233
|
+
return {
|
|
1234
|
+
neuron_id: found.id,
|
|
1235
|
+
backup,
|
|
1236
|
+
counts: {
|
|
1237
|
+
facts: (found.facts || []).length,
|
|
1238
|
+
decisions: (found.decisions || []).length,
|
|
1239
|
+
patterns: (found.patterns || []).length,
|
|
1240
|
+
preferences: (found.preferences || []).length,
|
|
1241
|
+
errors: (found.errors || []).length,
|
|
1242
|
+
debts: (found.debts || []).length,
|
|
1243
|
+
connections: (found.connections || []).length,
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Quarantine copies of one neuron, newest first. Stamps sort lexically, so
|
|
1249
|
+
* the file name alone orders them.
|
|
1250
|
+
*/
|
|
1251
|
+
async quarantineCopies(neuronId) {
|
|
1252
|
+
const prefijo = `${neuronId}.`;
|
|
1253
|
+
const files = await (0, fs_js_1.listJSONFiles)(this.brain.paths.quarantine);
|
|
1254
|
+
return files.filter(f => f.startsWith(prefijo)).sort().reverse();
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Bring a neuron back from quarantine.
|
|
1258
|
+
*
|
|
1259
|
+
* The newest copy is used unless `file` names an exact basename. If the
|
|
1260
|
+
* neuron no longer exists the copy is written back as it was; if it does
|
|
1261
|
+
* (a forget of facts, or the topic was re-created since) the copy is
|
|
1262
|
+
* unioned into it. The quarantine file stays where it is, so a restore is
|
|
1263
|
+
* repeatable. Nothing is emitted.
|
|
1264
|
+
*/
|
|
1265
|
+
async restoreNeuron(neuronId, options) {
|
|
1266
|
+
const nada = { neuron: null, restored_from: null, merged_into_existing: false, moved: null };
|
|
1267
|
+
let basename = null;
|
|
1268
|
+
if (options?.file) {
|
|
1269
|
+
const pedido = options.file.replace(/\.json$/i, '');
|
|
1270
|
+
const copias = await this.quarantineCopies(neuronId);
|
|
1271
|
+
basename = copias.includes(pedido) ? pedido : null;
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
basename = (await this.quarantineCopies(neuronId))[0] || null;
|
|
1275
|
+
}
|
|
1276
|
+
if (!basename)
|
|
1277
|
+
return nada;
|
|
1278
|
+
const ruta = `${this.brain.paths.quarantine}/${basename}.json`;
|
|
1279
|
+
const copia = await (0, fs_js_1.readJSON)(ruta);
|
|
1280
|
+
if (!copia || !copia.id)
|
|
1281
|
+
return nada;
|
|
1282
|
+
copia.id = neuronId;
|
|
1283
|
+
// Decide inside the lock, like create: whether the neuron exists again is
|
|
1284
|
+
// only known for sure while nobody else can write it.
|
|
1285
|
+
let escrita = false;
|
|
1286
|
+
let moved = null;
|
|
1287
|
+
const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuronId), current => {
|
|
1288
|
+
if (!current) {
|
|
1289
|
+
escrita = true;
|
|
1290
|
+
return copia;
|
|
1291
|
+
}
|
|
1292
|
+
const r = unionNeuron(current, copia);
|
|
1293
|
+
moved = r.moved;
|
|
1294
|
+
return r.neuron;
|
|
1295
|
+
});
|
|
1296
|
+
const final = (saved || copia);
|
|
1297
|
+
if (escrita) {
|
|
1298
|
+
const manifest = await this.brain.getManifest();
|
|
1299
|
+
await this.brain.updateManifest({ total_neurons: manifest.total_neurons + 1 });
|
|
1300
|
+
}
|
|
1301
|
+
await this.reindex(final);
|
|
1302
|
+
return { neuron: final, restored_from: ruta, merged_into_existing: !escrita, moved };
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Fold one neuron into another and delete the first.
|
|
1306
|
+
*
|
|
1307
|
+
* `from` is quarantined, unioned into `into`, and then removed exactly like
|
|
1308
|
+
* forgetNeuron. Each element that actually moved is emitted for `into`, so
|
|
1309
|
+
* a shared target receives the knowledge (the emitter no-ops when `into`
|
|
1310
|
+
* is unshared). Synapses are not touched — the server calls
|
|
1311
|
+
* synapses.rewire(from, into) right after — and the server refuses
|
|
1312
|
+
* beforehand when `from` is shared (unshare first, or the next sync
|
|
1313
|
+
* re-creates it).
|
|
1314
|
+
*/
|
|
1315
|
+
async mergeNeurons(fromRef, intoRef) {
|
|
1316
|
+
const from = (await this.peek(fromRef)) || (await this.findByName(fromRef));
|
|
1317
|
+
const into = (await this.peek(intoRef)) || (await this.findByName(intoRef));
|
|
1318
|
+
if (!from || !into || from.id === into.id) {
|
|
1319
|
+
return { from: from?.id ?? null, into: into?.id ?? null, backup: null, moved: null };
|
|
1320
|
+
}
|
|
1321
|
+
const backup = await this.quarantine(from);
|
|
1322
|
+
let antes = into;
|
|
1323
|
+
let moved = null;
|
|
1324
|
+
const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(into.id), current => {
|
|
1325
|
+
antes = current || into;
|
|
1326
|
+
const r = unionNeuron(antes, from);
|
|
1327
|
+
moved = r.moved;
|
|
1328
|
+
return r.neuron;
|
|
1329
|
+
});
|
|
1330
|
+
const final = (saved || into);
|
|
1331
|
+
await this.reindex(final);
|
|
1332
|
+
// Emit what moved, by diffing the result against what `into` had. The
|
|
1333
|
+
// union is pure, so this is exact: nothing the target already held is
|
|
1334
|
+
// re-announced.
|
|
1335
|
+
const cuando = (0, fs_js_1.now)();
|
|
1336
|
+
const teniaFact = new Set(antes.facts.map(f => f.id || (0, hash_js_1.factId)(f.text)));
|
|
1337
|
+
for (const f of final.facts) {
|
|
1338
|
+
const fid = f.id || (0, hash_js_1.factId)(f.text);
|
|
1339
|
+
if (teniaFact.has(fid))
|
|
1340
|
+
continue;
|
|
1341
|
+
await this.emit(final.id, { kind: 'fact', text: f.text, fid, conf: f.confidence ?? 1, at: f.added, src: f.source, keys: f.keys });
|
|
1342
|
+
// A moved fact that was already retired must arrive retired, or the
|
|
1343
|
+
// teammates would see as current what the source had superseded.
|
|
1344
|
+
if (f.status === 'superseded' || f.status === 'retracted') {
|
|
1345
|
+
await this.emit(final.id, { kind: 'status', fid, to: f.status, at: f.revised || cuando, why: f.revision_note });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const teniaDecision = new Set(antes.decisions.map(d => (0, ops_js_1.normalizeText)(d.text)));
|
|
1349
|
+
for (const d of final.decisions) {
|
|
1350
|
+
if (teniaDecision.has((0, ops_js_1.normalizeText)(d.text)))
|
|
1351
|
+
continue;
|
|
1352
|
+
await this.emit(final.id, { kind: 'decision', text: d.text, why: d.rationale || undefined, at: d.date || cuando });
|
|
1353
|
+
}
|
|
1354
|
+
const fecha = (t) => final.entry_dates?.[(0, ops_js_1.entryId)(t)] || cuando;
|
|
1355
|
+
const teniaPattern = new Set((antes.patterns || []).map(ops_js_1.normalizeText));
|
|
1356
|
+
for (const p of final.patterns) {
|
|
1357
|
+
if (!teniaPattern.has((0, ops_js_1.normalizeText)(p)))
|
|
1358
|
+
await this.emit(final.id, { kind: 'pattern', text: p, at: fecha(p) });
|
|
1359
|
+
}
|
|
1360
|
+
const teniaError = new Set((antes.errors || []).map(ops_js_1.normalizeText));
|
|
1361
|
+
for (const e of final.errors || []) {
|
|
1362
|
+
if (!teniaError.has((0, ops_js_1.normalizeText)(e)))
|
|
1363
|
+
await this.emit(final.id, { kind: 'error', text: e, at: fecha(e) });
|
|
1364
|
+
}
|
|
1365
|
+
const teniaDebt = new Set((antes.debts || []).map(ops_js_1.normalizeText));
|
|
1366
|
+
for (const d of final.debts || []) {
|
|
1367
|
+
if (!teniaDebt.has((0, ops_js_1.normalizeText)(d)))
|
|
1368
|
+
await this.emit(final.id, { kind: 'debt', text: d, at: fecha(d) });
|
|
1369
|
+
}
|
|
1370
|
+
// Preferences are never emitted, here as in learn.
|
|
1371
|
+
await this.deleteNeuronFile(from.id);
|
|
1372
|
+
return { from: from.id, into: into.id, backup, moved };
|
|
1373
|
+
}
|
|
752
1374
|
/**
|
|
753
1375
|
* Which neurons hold something that looks like a credential.
|
|
754
1376
|
* Reports the kind and where it is, never the value.
|