crbro-memory 2.8.0 → 2.9.1

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 (52) hide show
  1. package/README.md +16 -5
  2. package/dist/daemon/endpoint.d.ts +1 -0
  3. package/dist/daemon/endpoint.d.ts.map +1 -1
  4. package/dist/daemon/endpoint.js +15 -13
  5. package/dist/daemon/endpoint.js.map +1 -1
  6. package/dist/engine/brain.d.ts.map +1 -1
  7. package/dist/engine/brain.js +30 -0
  8. package/dist/engine/brain.js.map +1 -1
  9. package/dist/engine/cortex.d.ts +55 -1
  10. package/dist/engine/cortex.d.ts.map +1 -1
  11. package/dist/engine/cortex.js +203 -12
  12. package/dist/engine/cortex.js.map +1 -1
  13. package/dist/engine/maintenance.d.ts +22 -0
  14. package/dist/engine/maintenance.d.ts.map +1 -1
  15. package/dist/engine/maintenance.js +59 -2
  16. package/dist/engine/maintenance.js.map +1 -1
  17. package/dist/engine/secrets.d.ts.map +1 -1
  18. package/dist/engine/secrets.js +12 -1
  19. package/dist/engine/secrets.js.map +1 -1
  20. package/dist/engine/shelf.d.ts +156 -0
  21. package/dist/engine/shelf.d.ts.map +1 -0
  22. package/dist/engine/shelf.js +680 -0
  23. package/dist/engine/shelf.js.map +1 -0
  24. package/dist/engine/source.d.ts +6 -0
  25. package/dist/engine/source.d.ts.map +1 -0
  26. package/dist/engine/source.js +101 -0
  27. package/dist/engine/source.js.map +1 -0
  28. package/dist/search/index.d.ts +6 -0
  29. package/dist/search/index.d.ts.map +1 -1
  30. package/dist/search/index.js +46 -1
  31. package/dist/search/index.js.map +1 -1
  32. package/dist/server.d.ts.map +1 -1
  33. package/dist/server.js +221 -47
  34. package/dist/server.js.map +1 -1
  35. package/dist/sync/materialize.d.ts +4 -0
  36. package/dist/sync/materialize.d.ts.map +1 -1
  37. package/dist/sync/materialize.js +80 -1
  38. package/dist/sync/materialize.js.map +1 -1
  39. package/dist/sync/ops.d.ts +29 -2
  40. package/dist/sync/ops.d.ts.map +1 -1
  41. package/dist/sync/ops.js.map +1 -1
  42. package/dist/sync/space.d.ts.map +1 -1
  43. package/dist/sync/space.js +15 -2
  44. package/dist/sync/space.js.map +1 -1
  45. package/dist/types/index.d.ts +49 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/version.d.ts +26 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +65 -0
  50. package/dist/version.js.map +1 -0
  51. package/hooks/crbro-lifecycle.mjs +612 -608
  52. package/package.json +1 -1
@@ -13,6 +13,7 @@ const ids_js_1 = require("../utils/ids.js");
13
13
  const hash_js_1 = require("../utils/hash.js");
14
14
  const ops_js_1 = require("../sync/ops.js");
15
15
  const secrets_js_1 = require("./secrets.js");
16
+ const shelf_js_1 = require("./shelf.js");
16
17
  const TYPE_PREFIX_RE = /^(project_|tech_|lang_|person_|domain_|process_|protocol_)/;
17
18
  /**
18
19
  * Similarity above which a new fact is flagged as a near-duplicate of one
@@ -360,6 +361,14 @@ function unionNeuron(target, source) {
360
361
  hit.keys = keys;
361
362
  else
362
363
  delete hit.keys;
364
+ // The later check is the newer evidence; the more volatile explicit class
365
+ // wins, as in a team space (a needless warning is cheaper than a missing one).
366
+ const verificado = (0, shelf_js_1.latestOf)(hit.verified, sf.verified);
367
+ if (verificado)
368
+ hit.verified = verificado;
369
+ const vida = (0, shelf_js_1.mostVolatile)(hit.shelf_life, sf.shelf_life);
370
+ if (vida)
371
+ hit.shelf_life = vida;
363
372
  }
364
373
  // ── Decisions: by normalised text, the target's rationale wins ──
365
374
  const decisions = target.decisions.map(d => ({ ...d }));
@@ -435,6 +444,22 @@ function unionNeuron(target, source) {
435
444
  neuron.entry_status = estadosVivos;
436
445
  else
437
446
  delete neuron.entry_status;
447
+ const verificadas = { ...(source.entry_verified || {}) };
448
+ for (const [k, v] of Object.entries(target.entry_verified || {})) {
449
+ const l = (0, shelf_js_1.latestOf)(verificadas[k], v);
450
+ if (l)
451
+ verificadas[k] = l;
452
+ else if (!verificadas[k])
453
+ verificadas[k] = v;
454
+ }
455
+ const verificadasVivas = {};
456
+ for (const k of Object.keys(verificadas).sort())
457
+ if (vivos.has(k))
458
+ verificadasVivas[k] = verificadas[k];
459
+ if (Object.keys(verificadasVivas).length)
460
+ neuron.entry_verified = verificadasVivas;
461
+ else
462
+ delete neuron.entry_verified;
438
463
  const origenes = { ...(source.entry_source || {}), ...(target.entry_source || {}) };
439
464
  const origenesVivos = {};
440
465
  for (const k of Object.keys(origenes).sort())
@@ -750,6 +775,11 @@ class Cortex {
750
775
  let duplicate = false;
751
776
  let updatedInPlace = false;
752
777
  let confirmations;
778
+ let shelf;
779
+ let reconfirmed = false;
780
+ let shelfLengthened = false;
781
+ let verificar = null;
782
+ const vidaPedida = (0, shelf_js_1.isShelfLife)(options?.shelfLife) ? options.shelfLife : undefined;
753
783
  let skippedRetired = null;
754
784
  // A retired decision, pattern, error or debt must not come back through
755
785
  // learn: the sidecar says so, and the index already hides it.
@@ -822,6 +852,16 @@ class Cortex {
822
852
  vistos.add(testigo);
823
853
  confirmations = existente.confirmations ?? 1;
824
854
  let cambiado = false;
855
+ if (vidaPedida && vidaPedida !== existente.shelf_life) {
856
+ // Lengthening an explicit class (volatile → durable, say). On a
857
+ // shared neuron the more volatile value comes back from the
858
+ // log on the next sync; the server warns about it.
859
+ if (existente.shelf_life && (0, shelf_js_1.mostVolatile)(existente.shelf_life, vidaPedida) === existente.shelf_life) {
860
+ shelfLengthened = true;
861
+ }
862
+ existente.shelf_life = vidaPedida;
863
+ cambiado = true;
864
+ }
825
865
  if (options?.confidence !== undefined && options.confidence !== existente.confidence) {
826
866
  existente.confidence = options.confidence;
827
867
  cambiado = true;
@@ -843,6 +883,24 @@ class Cortex {
843
883
  cambiado = true;
844
884
  }
845
885
  }
886
+ // Saying the same line again, and nothing else, is a check
887
+ // (shelf life): the clock restarts. Not when the call also
888
+ // brings keywords, a confidence or a class — that is editing the
889
+ // line, and the server itself asks for exactly that re-learn to
890
+ // add keywords, so counting it would mark an unchecked line as
891
+ // checked. Not for the miner either: re-reading an old
892
+ // transcript is not looking at the source.
893
+ if (options?.source !== 'miner' && !cambiado) {
894
+ existente.verified = (0, fs_js_1.now)();
895
+ reconfirmed = true;
896
+ // One op per line per session: a retry or the re-save after a
897
+ // compaction must not add a line to the team's log each time.
898
+ const yaVerificadas = (this.tally.verifiedEmitted ??= new Set());
899
+ if (!yaVerificadas.has(testigo)) {
900
+ yaVerificadas.add(testigo);
901
+ verificar = { eid: existente.id || (0, hash_js_1.factId)(existente.text), at: existente.verified };
902
+ }
903
+ }
846
904
  if (cambiado) {
847
905
  // Note for shared neurons: materialize takes max(conf) and
848
906
  // unions keys, so lowering confidence or dropping an alias
@@ -850,8 +908,11 @@ class Cortex {
850
908
  updatedInPlace = true;
851
909
  this.tally.topics.add(n.id);
852
910
  emitir = { kind: 'fact', text: existente.text, fid: existente.id || id,
853
- conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys: existente.keys };
911
+ conf: existente.confidence ?? 1, at: existente.added, src: existente.source, keys: existente.keys,
912
+ ...(existente.shelf_life ? { shelf: existente.shelf_life } : {}) };
854
913
  }
914
+ const sv = (0, shelf_js_1.shelfOfFact)(existente);
915
+ shelf = { shelf_life: sv.shelf, inferred: sv.inferred, ...(sv.reason ? { reason: sv.reason } : {}) };
855
916
  }
856
917
  if (!isDuplicate) {
857
918
  if (options?.supersedes?.length) {
@@ -876,13 +937,18 @@ class Cortex {
876
937
  fact.supersedes = options.supersedes;
877
938
  if (keys.length)
878
939
  fact.keys = keys;
940
+ if (vidaPedida)
941
+ fact.shelf_life = vidaPedida;
942
+ const sv = (0, shelf_js_1.shelfOfFact)(fact);
943
+ shelf = { shelf_life: sv.shelf, inferred: sv.inferred, ...(sv.reason ? { reason: sv.reason } : {}) };
879
944
  n.facts.push(fact);
880
945
  if (fact.source !== 'miner')
881
946
  (this.tally.witnessed ??= new Set()).add(`${n.id}|${id}`);
882
947
  this.tally.facts++;
883
948
  this.tally.topics.add(n.id);
884
949
  emitir = { kind: 'fact', text: content, fid: id,
885
- conf: fact.confidence, at: fact.added, src: fact.source, keys: fact.keys };
950
+ conf: fact.confidence, at: fact.added, src: fact.source, keys: fact.keys,
951
+ ...(fact.shelf_life ? { shelf: fact.shelf_life } : {}) };
886
952
  }
887
953
  break;
888
954
  }
@@ -970,11 +1036,18 @@ class Cortex {
970
1036
  // key and the least likely to be worth sharing.
971
1037
  if (emitir)
972
1038
  await this.emit(final.id, emitir);
1039
+ // A reconfirming re-learn reaches the team as a check: the latest wins.
1040
+ const v = verificar;
1041
+ if (v)
1042
+ await this.emit(final.id, { kind: 'verify', eid: v.eid, ekind: 'fact', at: v.at });
973
1043
  return {
974
1044
  neuron: final, action, superseded, supersedes_unmatched: supersedesUnmatched,
975
1045
  near_duplicates: nearDuplicates, redacted: limpio.found,
976
1046
  duplicate, updated_in_place: updatedInPlace, skipped_retired: null,
977
1047
  ...(confirmations !== undefined ? { confirmations } : {}),
1048
+ ...(shelf ? { shelf } : {}),
1049
+ ...(reconfirmed ? { reconfirmed } : {}),
1050
+ ...(shelfLengthened ? { shelf_lengthened: true } : {}),
978
1051
  };
979
1052
  }
980
1053
  /**
@@ -1025,6 +1098,93 @@ class Cortex {
1025
1098
  }
1026
1099
  return { neuron: (actualizada || neuron), revised, unmatched };
1027
1100
  }
1101
+ /**
1102
+ * Reconfirm: the caller checked these lines against their source and they
1103
+ * still hold, so their shelf-life clock restarts now.
1104
+ *
1105
+ * `facts` match ACTIVE facts by id or exact text (trimmed, case-insensitive);
1106
+ * `entries` match live decisions and patterns by entry id or exact text —
1107
+ * the two kinds whose class is durable. A retired fact or entry is not
1108
+ * verifiable: it comes back in `retired` with its status (reactivate it with
1109
+ * status active first, if it holds again), and in `unmatched` like any miss.
1110
+ * Preferences, errors and debts never go stale, so there is nothing to
1111
+ * reconfirm on them; they are unmatched.
1112
+ *
1113
+ * Each stamp is emitted as a verify op, so on a shared neuron a teammate's
1114
+ * clock restarts too: the fact is shared, and so is the world it describes.
1115
+ */
1116
+ async verify(neuronRef, targets) {
1117
+ const facts = (targets.facts || []).filter(t => typeof t === 'string');
1118
+ const entries = (targets.entries || []).filter(t => typeof t === 'string');
1119
+ const neuron = (await this.peek(neuronRef)) || (await this.findByName(neuronRef));
1120
+ if (!neuron)
1121
+ return { neuron: null, verified: [], unmatched: [...facts, ...entries], retired: [] };
1122
+ const verified = [];
1123
+ let unmatched = [];
1124
+ let retired = [];
1125
+ const emitir = [];
1126
+ const actualizada = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuron.id), current => {
1127
+ const n = current || neuron;
1128
+ const cuando = (0, fs_js_1.now)();
1129
+ verified.length = 0;
1130
+ unmatched = [];
1131
+ retired = [];
1132
+ emitir.length = 0;
1133
+ for (const t of facts) {
1134
+ const w = t.trim().toLowerCase();
1135
+ const hits = n.facts.filter(f => (f.id || (0, hash_js_1.factId)(f.text)).toLowerCase() === w || f.text.trim().toLowerCase() === w);
1136
+ const vivos = hits.filter(f => f.status !== 'superseded' && f.status !== 'retracted');
1137
+ if (vivos.length === 0) {
1138
+ const r = hits.find(f => f.status === 'superseded' || f.status === 'retracted');
1139
+ if (r)
1140
+ retired.push({ target: t, id: r.id || (0, hash_js_1.factId)(r.text), status: r.status });
1141
+ unmatched.push(t);
1142
+ continue;
1143
+ }
1144
+ for (const f of vivos) {
1145
+ f.id = f.id || (0, hash_js_1.factId)(f.text);
1146
+ f.verified = cuando;
1147
+ if (!verified.includes(f.id))
1148
+ verified.push(f.id);
1149
+ emitir.push({ eid: f.id, ekind: 'fact', at: cuando });
1150
+ }
1151
+ }
1152
+ const candidatas = [
1153
+ ...n.decisions.map(d => ({ text: d.text || '', ids: [(0, ops_js_1.entryId)(d.text || ''), ...(d.id ? [d.id] : [])] })),
1154
+ ...n.patterns.map(p => ({ text: p, ids: [(0, ops_js_1.entryId)(p)] })),
1155
+ ];
1156
+ for (const t of entries) {
1157
+ const w = (0, ops_js_1.normalizeText)(t).toLowerCase();
1158
+ const hits = candidatas.filter(c => c.ids.some(i => i.toLowerCase() === w) || (0, ops_js_1.normalizeText)(c.text).toLowerCase() === w);
1159
+ const vivas = hits.filter(c => !n.entry_status?.[(0, ops_js_1.entryId)(c.text)]);
1160
+ if (vivas.length === 0) {
1161
+ const r = hits[0];
1162
+ const est = r ? n.entry_status?.[(0, ops_js_1.entryId)(r.text)] : undefined;
1163
+ if (r && est)
1164
+ retired.push({ target: t, id: (0, ops_js_1.entryId)(r.text), status: est.status });
1165
+ unmatched.push(t);
1166
+ continue;
1167
+ }
1168
+ for (const c of vivas) {
1169
+ const k = (0, ops_js_1.entryId)(c.text);
1170
+ (n.entry_verified ||= {})[k] = cuando;
1171
+ if (!verified.includes(k))
1172
+ verified.push(k);
1173
+ emitir.push({ eid: k, ekind: 'entry', at: cuando });
1174
+ }
1175
+ }
1176
+ if (verified.length === 0)
1177
+ return null;
1178
+ n.last_accessed = cuando;
1179
+ return n;
1180
+ });
1181
+ const final = (actualizada || neuron);
1182
+ // No reindex: neither the text nor the indexed date changed. `since`
1183
+ // filters on when a line was recorded, not on when it was last checked.
1184
+ for (const e of emitir)
1185
+ await this.emit(final.id, { kind: 'verify', ...e });
1186
+ return { neuron: final, verified: [...verified], unmatched, retired };
1187
+ }
1028
1188
  /**
1029
1189
  * Which active facts does this text closely resemble?
1030
1190
  *
@@ -1379,6 +1539,17 @@ class Cortex {
1379
1539
  const backup = await this.quarantine(found);
1380
1540
  let removed = 0;
1381
1541
  const wanted = targets.map(t => t.trim().toLowerCase());
1542
+ // An entry goes when a target names its text or its id: the entryId the
1543
+ // neuron index and recall show, or a decision's own id. Until 2.9.1 only
1544
+ // facts matched by id, so taking a credential out of a preference meant
1545
+ // pasting the credential back into the conversation to name it.
1546
+ const matches = (text, ownId) => {
1547
+ const t = (text || '').trim().toLowerCase();
1548
+ if (!t)
1549
+ return false;
1550
+ return wanted.includes(t) || wanted.includes((0, ops_js_1.entryId)(text).toLowerCase())
1551
+ || (!!ownId && wanted.includes(ownId.toLowerCase()));
1552
+ };
1382
1553
  const after = await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(found.id), current => {
1383
1554
  if (!current)
1384
1555
  return null;
@@ -1395,16 +1566,16 @@ class Cortex {
1395
1566
  });
1396
1567
  // Decisions, patterns and preferences too. A credential is no less
1397
1568
  // exposed for sitting in one of those, and nothing else could remove it.
1398
- current.decisions = current.decisions.filter(d => !wanted.includes((d.text || '').trim().toLowerCase()));
1399
- current.patterns = current.patterns.filter(p => !wanted.includes((p || '').trim().toLowerCase()));
1400
- current.preferences = current.preferences.filter(p => !wanted.includes((p || '').trim().toLowerCase()));
1569
+ current.decisions = current.decisions.filter(d => !matches(d.text, d.id));
1570
+ current.patterns = current.patterns.filter(p => !matches(p));
1571
+ current.preferences = current.preferences.filter(p => !matches(p));
1401
1572
  const erroresAntes = (current.errors || []).length;
1402
1573
  if (current.errors) {
1403
- current.errors = current.errors.filter(e => !wanted.includes((e || '').trim().toLowerCase()));
1574
+ current.errors = current.errors.filter(e => !matches(e));
1404
1575
  }
1405
1576
  const deudasAntes = (current.debts || []).length;
1406
1577
  if (current.debts) {
1407
- current.debts = current.debts.filter(d => !wanted.includes((d || '').trim().toLowerCase()));
1578
+ current.debts = current.debts.filter(d => !matches(d));
1408
1579
  }
1409
1580
  let mapaBorrado = 0;
1410
1581
  if (current.map && wanted.includes(current.map.text.trim().toLowerCase())) {
@@ -1438,6 +1609,16 @@ class Cortex {
1438
1609
  if (Object.keys(current.entry_status).length === 0)
1439
1610
  delete current.entry_status;
1440
1611
  }
1612
+ // Verifications go with their entry too.
1613
+ if (current.entry_verified) {
1614
+ const vivos = liveEntryKeys(current);
1615
+ for (const k of Object.keys(current.entry_verified)) {
1616
+ if (!vivos.has(k))
1617
+ delete current.entry_verified[k];
1618
+ }
1619
+ if (Object.keys(current.entry_verified).length === 0)
1620
+ delete current.entry_verified;
1621
+ }
1441
1622
  // And for provenance: a forgotten teammate line that comes back through
1442
1623
  // a sync replay must be labelled by that replay, not by a stale key.
1443
1624
  if (current.entry_source) {
@@ -1467,22 +1648,22 @@ class Cortex {
1467
1648
  }
1468
1649
  }
1469
1650
  for (const d of found.decisions || []) {
1470
- if (wanted.includes((d.text || '').trim().toLowerCase())) {
1651
+ if (matches(d.text, d.id)) {
1471
1652
  await this.emit(found.id, { kind: 'decision_purge', key: (0, ops_js_1.entryId)(d.text), at: cuando });
1472
1653
  }
1473
1654
  }
1474
1655
  for (const p of found.patterns || []) {
1475
- if (wanted.includes((p || '').trim().toLowerCase())) {
1656
+ if (matches(p)) {
1476
1657
  await this.emit(found.id, { kind: 'pattern_purge', key: (0, ops_js_1.entryId)(p), at: cuando });
1477
1658
  }
1478
1659
  }
1479
1660
  for (const e of found.errors || []) {
1480
- if (wanted.includes((e || '').trim().toLowerCase())) {
1661
+ if (matches(e)) {
1481
1662
  await this.emit(found.id, { kind: 'error_purge', key: (0, ops_js_1.entryId)(e), at: cuando });
1482
1663
  }
1483
1664
  }
1484
1665
  for (const d of found.debts || []) {
1485
- if (wanted.includes((d || '').trim().toLowerCase())) {
1666
+ if (matches(d)) {
1486
1667
  await this.emit(found.id, { kind: 'debt_purge', key: (0, ops_js_1.entryId)(d), at: cuando });
1487
1668
  }
1488
1669
  }
@@ -1627,7 +1808,10 @@ class Cortex {
1627
1808
  const fid = f.id || (0, hash_js_1.factId)(f.text);
1628
1809
  if (teniaFact.has(fid))
1629
1810
  continue;
1630
- await this.emit(final.id, { kind: 'fact', text: f.text, fid, conf: f.confidence ?? 1, at: f.added, src: f.source, keys: f.keys });
1811
+ await this.emit(final.id, { kind: 'fact', text: f.text, fid, conf: f.confidence ?? 1, at: f.added, src: f.source, keys: f.keys,
1812
+ ...(f.shelf_life ? { shelf: f.shelf_life } : {}) });
1813
+ if (f.verified)
1814
+ await this.emit(final.id, { kind: 'verify', eid: fid, ekind: 'fact', at: f.verified });
1631
1815
  // A moved fact that was already retired must arrive retired, or the
1632
1816
  // teammates would see as current what the source had superseded.
1633
1817
  if (f.status === 'superseded' || f.status === 'retracted') {
@@ -1656,6 +1840,12 @@ class Cortex {
1656
1840
  if (!teniaDebt.has((0, ops_js_1.normalizeText)(d)))
1657
1841
  await this.emit(final.id, { kind: 'debt', text: d, at: fecha(d) });
1658
1842
  }
1843
+ // Checks of decisions and patterns that arrived with them, or are newer than the target's.
1844
+ for (const [k, at] of Object.entries(final.entry_verified || {})) {
1845
+ if (antes.entry_verified?.[k] === at)
1846
+ continue;
1847
+ await this.emit(final.id, { kind: 'verify', eid: k, ekind: 'entry', at });
1848
+ }
1659
1849
  // Preferences are never emitted, here as in learn.
1660
1850
  }
1661
1851
  /**
@@ -1700,6 +1890,7 @@ class Cortex {
1700
1890
  tags: [], connections: [], summary: '', heat: 0, access_count: 0,
1701
1891
  entry_dates: { ...(from.entry_dates || {}) },
1702
1892
  entry_status: { ...(from.entry_status || {}) },
1893
+ entry_verified: { ...(from.entry_verified || {}) },
1703
1894
  };
1704
1895
  delete parte.map;
1705
1896
  const textos = [