nexus-prime 4.3.2 → 4.4.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 (129) hide show
  1. package/README.md +10 -0
  2. package/dist/agents/adapters/mcp.d.ts.map +1 -1
  3. package/dist/agents/adapters/mcp.js +19 -1
  4. package/dist/agents/adapters/mcp.js.map +1 -1
  5. package/dist/dashboard/index.html +22 -2
  6. package/dist/dashboard/server.d.ts.map +1 -1
  7. package/dist/dashboard/server.js +36 -3
  8. package/dist/dashboard/server.js.map +1 -1
  9. package/dist/engines/client-bootstrap.d.ts +1 -0
  10. package/dist/engines/client-bootstrap.d.ts.map +1 -1
  11. package/dist/engines/client-bootstrap.js +36 -6
  12. package/dist/engines/client-bootstrap.js.map +1 -1
  13. package/dist/engines/context-assembler.d.ts +17 -0
  14. package/dist/engines/context-assembler.d.ts.map +1 -1
  15. package/dist/engines/context-assembler.js +72 -31
  16. package/dist/engines/context-assembler.js.map +1 -1
  17. package/dist/engines/embedder.d.ts +3 -1
  18. package/dist/engines/embedder.d.ts.map +1 -1
  19. package/dist/engines/embedder.js +20 -4
  20. package/dist/engines/embedder.js.map +1 -1
  21. package/dist/engines/event-bus.d.ts +9 -1
  22. package/dist/engines/event-bus.d.ts.map +1 -1
  23. package/dist/engines/event-bus.js +21 -0
  24. package/dist/engines/event-bus.js.map +1 -1
  25. package/dist/engines/index.d.ts +1 -1
  26. package/dist/engines/index.d.ts.map +1 -1
  27. package/dist/engines/index.js +1 -1
  28. package/dist/engines/index.js.map +1 -1
  29. package/dist/engines/instruction-gateway.d.ts.map +1 -1
  30. package/dist/engines/instruction-gateway.js +18 -7
  31. package/dist/engines/instruction-gateway.js.map +1 -1
  32. package/dist/engines/knowledge-fabric.d.ts +9 -0
  33. package/dist/engines/knowledge-fabric.d.ts.map +1 -1
  34. package/dist/engines/knowledge-fabric.js +56 -4
  35. package/dist/engines/knowledge-fabric.js.map +1 -1
  36. package/dist/engines/lifetime-tokens.d.ts +14 -0
  37. package/dist/engines/lifetime-tokens.d.ts.map +1 -0
  38. package/dist/engines/lifetime-tokens.js +37 -0
  39. package/dist/engines/lifetime-tokens.js.map +1 -0
  40. package/dist/engines/memory-bridge.d.ts +49 -19
  41. package/dist/engines/memory-bridge.d.ts.map +1 -1
  42. package/dist/engines/memory-bridge.js +141 -29
  43. package/dist/engines/memory-bridge.js.map +1 -1
  44. package/dist/engines/memory-control-plane.d.ts +27 -0
  45. package/dist/engines/memory-control-plane.d.ts.map +1 -1
  46. package/dist/engines/memory-control-plane.js +100 -0
  47. package/dist/engines/memory-control-plane.js.map +1 -1
  48. package/dist/engines/memory-extractor.d.ts +35 -0
  49. package/dist/engines/memory-extractor.d.ts.map +1 -0
  50. package/dist/engines/memory-extractor.js +164 -0
  51. package/dist/engines/memory-extractor.js.map +1 -0
  52. package/dist/engines/memory-fingerprint.d.ts +7 -0
  53. package/dist/engines/memory-fingerprint.d.ts.map +1 -0
  54. package/dist/engines/memory-fingerprint.js +22 -0
  55. package/dist/engines/memory-fingerprint.js.map +1 -0
  56. package/dist/engines/memory.d.ts +62 -8
  57. package/dist/engines/memory.d.ts.map +1 -1
  58. package/dist/engines/memory.js +445 -82
  59. package/dist/engines/memory.js.map +1 -1
  60. package/dist/engines/nexus-layer.d.ts +1 -6
  61. package/dist/engines/nexus-layer.d.ts.map +1 -1
  62. package/dist/engines/ngram-index.d.ts +2 -0
  63. package/dist/engines/ngram-index.d.ts.map +1 -1
  64. package/dist/engines/ngram-index.js +32 -0
  65. package/dist/engines/ngram-index.js.map +1 -1
  66. package/dist/engines/orchestrator.d.ts +3 -1
  67. package/dist/engines/orchestrator.d.ts.map +1 -1
  68. package/dist/engines/orchestrator.js +157 -31
  69. package/dist/engines/orchestrator.js.map +1 -1
  70. package/dist/engines/pattern-registry.d.ts.map +1 -1
  71. package/dist/engines/pattern-registry.js +9 -2
  72. package/dist/engines/pattern-registry.js.map +1 -1
  73. package/dist/engines/rag-collections.d.ts +8 -0
  74. package/dist/engines/rag-collections.d.ts.map +1 -1
  75. package/dist/engines/rag-collections.js +63 -14
  76. package/dist/engines/rag-collections.js.map +1 -1
  77. package/dist/engines/runtime-registry.d.ts +1 -0
  78. package/dist/engines/runtime-registry.d.ts.map +1 -1
  79. package/dist/engines/runtime-registry.js +9 -11
  80. package/dist/engines/runtime-registry.js.map +1 -1
  81. package/dist/engines/semantic-ranking.d.ts +27 -0
  82. package/dist/engines/semantic-ranking.d.ts.map +1 -0
  83. package/dist/engines/semantic-ranking.js +83 -0
  84. package/dist/engines/semantic-ranking.js.map +1 -0
  85. package/dist/engines/skill-runtime.d.ts +1 -0
  86. package/dist/engines/skill-runtime.d.ts.map +1 -1
  87. package/dist/engines/skill-runtime.js +58 -61
  88. package/dist/engines/skill-runtime.js.map +1 -1
  89. package/dist/engines/token-optimizer.d.ts +0 -16
  90. package/dist/engines/token-optimizer.d.ts.map +1 -1
  91. package/dist/engines/token-optimizer.js +0 -40
  92. package/dist/engines/token-optimizer.js.map +1 -1
  93. package/dist/engines/token-supremacy.d.ts +5 -1
  94. package/dist/engines/token-supremacy.d.ts.map +1 -1
  95. package/dist/engines/token-supremacy.js +8 -2
  96. package/dist/engines/token-supremacy.js.map +1 -1
  97. package/dist/engines/workflow-runtime.d.ts +2 -0
  98. package/dist/engines/workflow-runtime.d.ts.map +1 -1
  99. package/dist/engines/workflow-runtime.js +74 -38
  100. package/dist/engines/workflow-runtime.js.map +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +37 -9
  104. package/dist/index.js.map +1 -1
  105. package/dist/nexus-prime.d.ts +1 -1
  106. package/dist/nexus-prime.d.ts.map +1 -1
  107. package/dist/nexus-prime.js +1 -1
  108. package/dist/nexus-prime.js.map +1 -1
  109. package/dist/phantom/index.d.ts +12 -2
  110. package/dist/phantom/index.d.ts.map +1 -1
  111. package/dist/phantom/index.js +29 -9
  112. package/dist/phantom/index.js.map +1 -1
  113. package/dist/phantom/runtime.d.ts +3 -2
  114. package/dist/phantom/runtime.d.ts.map +1 -1
  115. package/dist/phantom/runtime.js +80 -8
  116. package/dist/phantom/runtime.js.map +1 -1
  117. package/dist/postinstall-bootstrap.js +4 -1
  118. package/dist/postinstall-bootstrap.js.map +1 -1
  119. package/dist/synapse/bootstrap.d.ts.map +1 -1
  120. package/dist/synapse/bootstrap.js +9 -2
  121. package/dist/synapse/bootstrap.js.map +1 -1
  122. package/dist/synapse/mandate/asset-matcher.d.ts.map +1 -1
  123. package/dist/synapse/mandate/asset-matcher.js +26 -15
  124. package/dist/synapse/mandate/asset-matcher.js.map +1 -1
  125. package/package.json +2 -3
  126. package/dist/core/memory.d.ts +0 -96
  127. package/dist/core/memory.d.ts.map +0 -1
  128. package/dist/core/memory.js +0 -304
  129. package/dist/core/memory.js.map +0 -1
@@ -21,7 +21,9 @@ import { podNetwork } from './pod-network.js';
21
21
  import { nexusEventBus } from './event-bus.js';
22
22
  import { SECRET_PATTERNS } from './security-shield.js';
23
23
  import { resolveNexusStateDir } from './runtime-registry.js';
24
- import { createEmptyReconciliationSummary, createMemoryProvenance, deriveCandidateFacts, } from './memory-control-plane.js';
24
+ import { createEmptyReconciliationSummary, createMemoryProvenance, reconcileCandidateFact, } from './memory-control-plane.js';
25
+ import { MemoryExtractor } from './memory-extractor.js';
26
+ import { createMemoryContentFingerprint } from './memory-fingerprint.js';
25
27
  // ─────────────────────────────────────────────────────────────────────────────
26
28
  // Helpers
27
29
  // ─────────────────────────────────────────────────────────────────────────────
@@ -42,7 +44,18 @@ function resolveCurrentRepoId() {
42
44
  return _cachedRepoId;
43
45
  }
44
46
  function resolvePreferredStateRoot() {
45
- return process.env.NEXUS_STATE_DIR?.trim() || resolveNexusStateDir();
47
+ const explicit = process.env.NEXUS_STATE_DIR?.trim();
48
+ if (explicit) {
49
+ return explicit;
50
+ }
51
+ const homeStateRoot = path.join(os.homedir(), '.nexus-prime');
52
+ try {
53
+ ensureWritableDirectory(homeStateRoot);
54
+ return homeStateRoot;
55
+ }
56
+ catch {
57
+ return resolveNexusStateDir();
58
+ }
46
59
  }
47
60
  function isWritableStorageError(error) {
48
61
  const message = error instanceof Error ? error.message : String(error || '');
@@ -138,22 +151,32 @@ export class MemoryEngine {
138
151
  vaultDirty = new Set();
139
152
  vaultFlushTimer;
140
153
  vaultNeedsFullSync = false;
154
+ lastDatabaseVacuumAt = 0;
141
155
  // In-RAM working tiers (flushed to DB periodically)
142
156
  prefrontal = [];
143
157
  maxPrefrontal = 7;
144
158
  maxHippocampus = 200;
145
159
  // Vector index (in-memory TF-IDF) — rebuilt on load
146
160
  embedder;
161
+ extractor;
147
162
  // Maps vector item id → memory id
148
163
  vectorIdToMemoryId = new Map();
149
164
  memoryIdToVectorId = new Map();
150
165
  nextVectorId = 1;
166
+ sqliteRetryAttempts = 3;
167
+ sqliteRetryDelayMs = 25;
151
168
  constructor(dbPath, config) {
152
169
  this.config = {
153
170
  decayRate: config?.decayRate ?? 0.05,
154
171
  priorityRetention: config?.priorityRetention ?? 0.95,
155
172
  flushEntropyThreshold: config?.flushEntropyThreshold ?? 0.9,
156
173
  recallCandidateLimit: Math.max(config?.recallCandidateLimit ?? 200, 1),
174
+ vaultCompactionThreshold: Math.max(config?.vaultCompactionThreshold ?? 50, 1),
175
+ expiredPurgeBatchSize: Math.max(config?.expiredPurgeBatchSize ?? 500, 1),
176
+ vacuumMinIntervalMs: Math.max(config?.vacuumMinIntervalMs ?? 12 * 60 * 60 * 1000, 1),
177
+ reconciliationContradictionThreshold: config?.reconciliationContradictionThreshold ?? 0.42,
178
+ reconciliationMergeThreshold: config?.reconciliationMergeThreshold ?? 0.68,
179
+ reconciliationDuplicateThreshold: config?.reconciliationDuplicateThreshold ?? 0.9,
157
180
  };
158
181
  const requestedDbPath = dbPath ?? path.join(resolvePreferredStateRoot(), 'memory.db');
159
182
  const opened = openMemoryDatabase(requestedDbPath);
@@ -193,6 +216,7 @@ export class MemoryEngine {
193
216
  fs.mkdirSync(this.vaultSessionNotesDir, { recursive: true });
194
217
  this.initSchema();
195
218
  this.embedder = new Embedder(this.db);
219
+ this.extractor = new MemoryExtractor();
196
220
  this.prepareStatements();
197
221
  this.load();
198
222
  this.loadBackupMetadata();
@@ -217,6 +241,7 @@ export class MemoryEngine {
217
241
  initSchema() {
218
242
  this.db.pragma('journal_mode = WAL');
219
243
  this.db.pragma('synchronous = NORMAL');
244
+ this.db.pragma('busy_timeout = 5000');
220
245
  this.db.pragma('cache_size = -32000');
221
246
  this.db.pragma('foreign_keys = ON');
222
247
  this.db.pragma('temp_store = MEMORY');
@@ -230,6 +255,33 @@ export class MemoryEngine {
230
255
  }
231
256
  });
232
257
  }
258
+ withSqliteRetry(operation, fn) {
259
+ for (let attempt = 1; attempt <= this.sqliteRetryAttempts; attempt += 1) {
260
+ try {
261
+ return fn();
262
+ }
263
+ catch (error) {
264
+ if (!this.isSqliteBusyError(error) || attempt === this.sqliteRetryAttempts) {
265
+ throw error;
266
+ }
267
+ const delayMs = this.sqliteRetryDelayMs * attempt;
268
+ nexusEventBus.emit('memory.sqlite.retry', {
269
+ operation,
270
+ attempt,
271
+ maxAttempts: this.sqliteRetryAttempts,
272
+ delayMs,
273
+ message: error instanceof Error ? error.message : String(error),
274
+ });
275
+ const signal = new Int32Array(new SharedArrayBuffer(4));
276
+ Atomics.wait(signal, 0, 0, delayMs);
277
+ }
278
+ }
279
+ throw new Error(`SQLite retry exhausted for ${operation}`);
280
+ }
281
+ isSqliteBusyError(error) {
282
+ const message = error instanceof Error ? error.message : String(error ?? '');
283
+ return /SQLITE_BUSY/i.test(message);
284
+ }
233
285
  runMigrations() {
234
286
  this.db.exec(`
235
287
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -746,12 +798,16 @@ export class MemoryEngine {
746
798
  provenance,
747
799
  };
748
800
  // Write to DB immediately (don't wait for flush)
749
- this.db.prepare(`
750
- INSERT INTO memories(id, content, priority, timestamp, tags, tier, scope, state, source, session_id, access_count, parent_id, depth, entropy, mass, trust, provenance_json, expires_at, supersedes, superseded_by)
751
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0.0, ?, ?, ?, ?, ?, ?)
752
- `).run(id, content, priority, item.timestamp, JSON.stringify(item.tags), item.tier, item.scope, item.state, item.source, item.sessionId, parentId, depth, priority, item.trust, JSON.stringify(item.provenance), item.expiresAt ?? null, item.supersedes ?? null, item.supersededBy ?? null);
801
+ this.withSqliteRetry('store.insert', () => {
802
+ this.db.prepare(`
803
+ INSERT INTO memories(id, content, priority, timestamp, tags, tier, scope, state, source, session_id, access_count, parent_id, depth, entropy, mass, trust, provenance_json, expires_at, supersedes, superseded_by)
804
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0.0, ?, ?, ?, ?, ?, ?)
805
+ `).run(id, content, priority, item.timestamp, JSON.stringify(item.tags), item.tier, item.scope, item.state, item.source, item.sessionId, parentId, depth, priority, item.trust, JSON.stringify(item.provenance), item.expiresAt ?? null, item.supersedes ?? null, item.supersededBy ?? null);
806
+ });
753
807
  if (item.supersedes) {
754
- this.db.prepare('UPDATE memories SET superseded_by = ?, state = ? WHERE id = ?').run(id, 'expired', item.supersedes);
808
+ this.withSqliteRetry('store.supersede', () => {
809
+ this.db.prepare('UPDATE memories SET superseded_by = ?, state = ? WHERE id = ?').run(id, 'expired', item.supersedes);
810
+ });
755
811
  }
756
812
  // Update vocabulary and add to vector index
757
813
  this.fitVocabularyWithRecovery([content]);
@@ -785,9 +841,11 @@ export class MemoryEngine {
785
841
  return id;
786
842
  }
787
843
  storeWithControlPlane(content, priority = 0.7, tags = [], parentId, depth = 0, options = {}) {
788
- const candidates = deriveCandidateFacts(content, tags, options.maxCandidates ?? 2);
844
+ const extraction = this.extractor.extract(content, tags, options.maxCandidates ?? 2);
845
+ const candidates = extraction.facts;
789
846
  const summary = createEmptyReconciliationSummary();
790
847
  const storedIds = [];
848
+ const rawFingerprint = extraction.rawFingerprint;
791
849
  for (const candidate of candidates) {
792
850
  const decision = this.reconcileCandidate(candidate, {
793
851
  parentId,
@@ -821,13 +879,20 @@ export class MemoryEngine {
821
879
  tags: dedupeStrings([...(options.provenance?.tags ?? []), ...candidate.tags]),
822
880
  references: dedupeStrings([...(options.provenance?.references ?? []), ...decision.relatedIds]),
823
881
  lane: options.provenance?.lane ?? candidateLane,
882
+ rawContext: {
883
+ preview: content.slice(0, 240),
884
+ fingerprint: rawFingerprint,
885
+ extractionMode: candidate.extractionMode,
886
+ },
824
887
  },
825
888
  });
826
889
  storedIds.push(storedId);
827
890
  }
828
891
  else if (decision.action === 'DELETE') {
829
892
  decision.relatedIds.forEach((relatedId) => {
830
- this.db.prepare('UPDATE memories SET state = ?, superseded_by = ? WHERE id = ?').run('expired', 'deleted-by-policy', relatedId);
893
+ this.withSqliteRetry('storeWithControlPlane.delete', () => {
894
+ this.db.prepare('UPDATE memories SET state = ?, superseded_by = ? WHERE id = ?').run('expired', 'deleted-by-policy', relatedId);
895
+ });
831
896
  });
832
897
  }
833
898
  summary.entries.push({
@@ -837,6 +902,9 @@ export class MemoryEngine {
837
902
  relatedIds: decision.relatedIds,
838
903
  storedId,
839
904
  expiresAt,
905
+ overlapScore: decision.overlapScore,
906
+ extractorMode: candidate.extractionMode,
907
+ resolutionReason: decision.resolutionReason,
840
908
  });
841
909
  }
842
910
  this.lastReconciliationSummary = {
@@ -954,6 +1022,92 @@ export class MemoryEngine {
954
1022
  }
955
1023
  return top.map(r => r.content);
956
1024
  }
1025
+ /** Recall with quality metadata for token budget allocation */
1026
+ async recallWithMetadata(query, k = 5, filters = {}) {
1027
+ this.expireMemories();
1028
+ const now = Date.now();
1029
+ let candidateRows;
1030
+ const ngramCandidates = this.ngramIndex?.search(query, this.config.recallCandidateLimit);
1031
+ if (ngramCandidates && ngramCandidates.length > 0) {
1032
+ const ids = ngramCandidates.map((c) => c.docId);
1033
+ const placeholders = ids.map(() => '?').join(',');
1034
+ candidateRows = this.db.prepare(`
1035
+ SELECT * FROM memories WHERE id IN (${placeholders}) AND state = 'active'
1036
+ AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, timestamp DESC
1037
+ `).all(...ids, now);
1038
+ if (candidateRows.length < k * 2) {
1039
+ const existingIds = new Set(candidateRows.map((r) => r.id));
1040
+ const supplement = this.db.prepare(`
1041
+ SELECT * FROM memories WHERE state = 'active' AND (expires_at IS NULL OR expires_at > ?)
1042
+ ORDER BY priority DESC, timestamp DESC LIMIT ?
1043
+ `).all(now, this.config.recallCandidateLimit);
1044
+ for (const row of supplement) {
1045
+ if (!existingIds.has(row.id)) {
1046
+ candidateRows.push(row);
1047
+ existingIds.add(row.id);
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ else {
1053
+ candidateRows = this.db.prepare(`
1054
+ SELECT * FROM memories WHERE state = 'active' AND (expires_at IS NULL OR expires_at > ?)
1055
+ ORDER BY priority DESC, timestamp DESC LIMIT ?
1056
+ `).all(now, this.config.recallCandidateLimit);
1057
+ }
1058
+ const candidateItems = candidateRows.map((row) => this.rowToItem(row)).filter((item) => this.matchesRecallFilters(item, filters));
1059
+ const queryVector = await this.embedder.embed(query);
1060
+ const scored = candidateItems.map((item) => {
1061
+ const itemVector = this.embedder.localEmbed(item.content);
1062
+ const hDist = HyperbolicMath.dist(queryVector, itemVector);
1063
+ const vectorScore = 1 / (1 + hDist);
1064
+ const recencyScore = Math.exp(-(now - item.timestamp) / (7 * 24 * 3600 * 1000));
1065
+ const priorityScore = item.priority;
1066
+ const accessBonus = Math.min(item.accessCount * 0.05, 0.3);
1067
+ const massBoost = (item.mass ?? 1.0) * 0.2;
1068
+ const qmdScore = this.computeQmdScore(item);
1069
+ const finalScore = vectorScore * 0.4 + priorityScore * 0.15 + recencyScore * 0.1 + accessBonus * 0.05 + massBoost + qmdScore * 0.3;
1070
+ return {
1071
+ content: item.content,
1072
+ id: item.id,
1073
+ score: finalScore,
1074
+ confidence: Math.min(1, vectorScore * 0.6 + priorityScore * 0.4),
1075
+ entropy: item.entropy ?? 0,
1076
+ trust: item.trust ?? 1.0,
1077
+ tier: item.tier,
1078
+ priority: item.priority,
1079
+ };
1080
+ });
1081
+ const top = scored.sort((a, b) => b.score - a.score).slice(0, k);
1082
+ if (top.length > 0) {
1083
+ const ids = top.filter(e => e.id).map(e => e.id);
1084
+ if (ids.length > 0)
1085
+ this.incrementAccessTxn(ids);
1086
+ }
1087
+ return top;
1088
+ }
1089
+ /**
1090
+ * Compute file relevance boosts from memory content.
1091
+ * Scans recent active memories for file path mentions and returns boost scores.
1092
+ */
1093
+ getFileBoosts(candidateFiles) {
1094
+ const boosts = new Map();
1095
+ if (candidateFiles.length === 0)
1096
+ return boosts;
1097
+ const recentMemories = this.db.prepare(`
1098
+ SELECT content FROM memories
1099
+ WHERE state = 'active' AND tier IN ('prefrontal', 'hippocampus')
1100
+ ORDER BY timestamp DESC LIMIT 50
1101
+ `).all();
1102
+ const contentBlock = recentMemories.map(r => r.content).join('\n').toLowerCase();
1103
+ for (const filePath of candidateFiles) {
1104
+ const basename = filePath.split('/').pop()?.replace(/\.[^.]+$/, '')?.toLowerCase() ?? '';
1105
+ if (basename.length >= 3 && contentBlock.includes(basename)) {
1106
+ boosts.set(filePath, 0.25);
1107
+ }
1108
+ }
1109
+ return boosts;
1110
+ }
957
1111
  /** Promote a memory to 'shared' scope so it becomes visible across all repos */
958
1112
  shareMemory(id) {
959
1113
  const item = this.getById(id);
@@ -964,19 +1118,27 @@ export class MemoryEngine {
964
1118
  return true;
965
1119
  }
966
1120
  /** Compact vault by merging individual item files into a single snapshot */
967
- compactVault() {
1121
+ compactVault(items = this.listVaultLiveItems()) {
968
1122
  if (!fs.existsSync(this.vaultItemsDir))
969
1123
  return null;
970
- const files = fs.readdirSync(this.vaultItemsDir).filter(f => f.endsWith('.json'));
971
- if (files.length <= 10)
1124
+ if (items.length <= this.config.vaultCompactionThreshold)
972
1125
  return null;
973
- const consolidated = [];
974
- for (const file of files) {
1126
+ const consolidated = items.map((item) => this.buildVaultItemPayload(item));
1127
+ for (const snapshot of this.listVaultSnapshotPaths()) {
1128
+ try {
1129
+ fs.unlinkSync(snapshot);
1130
+ }
1131
+ catch {
1132
+ // Ignore stale snapshot cleanup failures.
1133
+ }
1134
+ }
1135
+ for (const file of fs.readdirSync(this.vaultItemsDir).filter((entry) => entry.endsWith('.json'))) {
975
1136
  try {
976
- consolidated.push(JSON.parse(fs.readFileSync(path.join(this.vaultItemsDir, file), 'utf8')));
977
1137
  fs.unlinkSync(path.join(this.vaultItemsDir, file));
978
1138
  }
979
- catch { /* skip corrupted files */ }
1139
+ catch {
1140
+ // Ignore stale item cleanup failures.
1141
+ }
980
1142
  }
981
1143
  const snapshotPath = path.join(this.vaultDir, `vault-snapshot-${Date.now()}.json`);
982
1144
  fs.writeFileSync(snapshotPath, JSON.stringify(consolidated, null, 2));
@@ -1230,21 +1392,25 @@ export class MemoryEngine {
1230
1392
  `).run(item.id);
1231
1393
  this.markVaultDirty(item.id);
1232
1394
  }
1233
- /** Periodic cooling cycle: increases entropy and decays priority */
1395
+ /** Periodic cooling cycle: increases entropy and decays priority with tier-specific rates */
1234
1396
  coolDown() {
1235
- const { decayRate, priorityRetention } = this.config;
1236
- this.db.prepare(`
1237
- UPDATE memories
1238
- SET entropy = MAX(0.0, MIN(1.0, entropy + ?)),
1239
- priority = priority * ?
1240
- WHERE tier != 'cortex'
1241
- `).run(decayRate, priorityRetention);
1242
- // Apply access-frequency retention: frequently accessed memories decay slower
1243
- this.db.prepare(`
1244
- UPDATE memories
1245
- SET entropy = MAX(0.0, MIN(1.0, entropy - (? / (1.0 + CAST(access_count AS REAL) * 0.1))))
1246
- WHERE tier != 'cortex' AND access_count > 0
1247
- `).run(decayRate);
1397
+ const { priorityRetention } = this.config;
1398
+ // Tier-specific decay: prefrontal decays fast, hippocampus moderate, cortex very slow
1399
+ const tierDecay = { prefrontal: 0.15, hippocampus: 0.05, cortex: 0.01 };
1400
+ for (const [tier, rate] of Object.entries(tierDecay)) {
1401
+ this.db.prepare(`
1402
+ UPDATE memories
1403
+ SET entropy = MAX(0.0, MIN(1.0, entropy + ?)),
1404
+ priority = priority * ?
1405
+ WHERE tier = ?
1406
+ `).run(rate, tier === 'cortex' ? 1.0 : priorityRetention, tier);
1407
+ // Apply access-frequency retention: frequently accessed memories decay slower
1408
+ this.db.prepare(`
1409
+ UPDATE memories
1410
+ SET entropy = MAX(0.0, MIN(1.0, entropy - (? / (1.0 + CAST(access_count AS REAL) * 0.1))))
1411
+ WHERE tier = ? AND access_count > 0
1412
+ `).run(rate, tier);
1413
+ }
1248
1414
  // Force flush high entropy items (this will promote/demote based on priority)
1249
1415
  this.consolidate();
1250
1416
  this.syncVault(true);
@@ -1252,6 +1418,7 @@ export class MemoryEngine {
1252
1418
  /** Maintenance cycle: runs coolDown() then expires memories where entropy > threshold AND accessCount < 2 AND age > 7 days */
1253
1419
  maintenanceCycle() {
1254
1420
  this.coolDown();
1421
+ this.expireMemories();
1255
1422
  this.backgroundLinkMaintenance();
1256
1423
  const { flushEntropyThreshold } = this.config;
1257
1424
  const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
@@ -1263,7 +1430,9 @@ export class MemoryEngine {
1263
1430
  AND access_count < 2
1264
1431
  AND timestamp < ?
1265
1432
  `).run(flushEntropyThreshold, sevenDaysAgo);
1266
- this.syncVault(true);
1433
+ const purgedCount = this.purgeExpiredMemories();
1434
+ this.flushVaultSync();
1435
+ this.optimizeDatabase(purgedCount);
1267
1436
  }
1268
1437
  backgroundLinkMaintenance() {
1269
1438
  // Prune weak links
@@ -1480,7 +1649,7 @@ export class MemoryEngine {
1480
1649
  this.vaultDirty.clear();
1481
1650
  const items = ids
1482
1651
  .map((id) => this.getById(id))
1483
- .filter((item) => Boolean(item));
1652
+ .filter((item) => Boolean(item) && item.state !== 'expired');
1484
1653
  const currentIds = new Set(items.map((item) => item.id));
1485
1654
  for (const item of items) {
1486
1655
  this.writeVaultItem(item);
@@ -1500,22 +1669,40 @@ export class MemoryEngine {
1500
1669
  }
1501
1670
  syncVaultFull() {
1502
1671
  this.expireMemories();
1503
- const items = this.getAllItems();
1504
- const seen = new Set();
1672
+ const items = this.listVaultLiveItems();
1505
1673
  const memoryNotesDir = path.join(this.vaultNotesDir, 'memories');
1506
1674
  const noteSeen = new Set();
1675
+ const shouldCompact = items.length > this.config.vaultCompactionThreshold;
1507
1676
  for (const item of items) {
1508
- seen.add(item.id);
1509
- this.writeVaultItem(item);
1677
+ if (!shouldCompact) {
1678
+ this.writeVaultItem(item);
1679
+ }
1680
+ else {
1681
+ this.writeVaultNote(item);
1682
+ }
1510
1683
  const notePath = path.join(memoryNotesDir, `${item.id}.md`);
1511
1684
  noteSeen.add(notePath);
1512
1685
  }
1513
- for (const entry of fs.readdirSync(this.vaultItemsDir)) {
1514
- if (!entry.endsWith('.json'))
1515
- continue;
1516
- const memoryId = entry.replace(/\.json$/, '');
1517
- if (!seen.has(memoryId)) {
1518
- fs.unlinkSync(path.join(this.vaultItemsDir, entry));
1686
+ if (shouldCompact) {
1687
+ this.compactVault(items);
1688
+ }
1689
+ else {
1690
+ for (const snapshot of this.listVaultSnapshotPaths()) {
1691
+ try {
1692
+ fs.unlinkSync(snapshot);
1693
+ }
1694
+ catch {
1695
+ // Ignore stale snapshot cleanup failures.
1696
+ }
1697
+ }
1698
+ const seen = new Set(items.map((item) => item.id));
1699
+ for (const entry of fs.readdirSync(this.vaultItemsDir)) {
1700
+ if (!entry.endsWith('.json'))
1701
+ continue;
1702
+ const memoryId = entry.replace(/\.json$/, '');
1703
+ if (!seen.has(memoryId)) {
1704
+ fs.unlinkSync(path.join(this.vaultItemsDir, entry));
1705
+ }
1519
1706
  }
1520
1707
  }
1521
1708
  for (const entry of fs.readdirSync(memoryNotesDir)) {
@@ -1535,14 +1722,10 @@ export class MemoryEngine {
1535
1722
  }, null, 2), 'utf8');
1536
1723
  }
1537
1724
  writeVaultItem(item) {
1538
- fs.writeFileSync(path.join(this.vaultItemsDir, `${item.id}.json`), JSON.stringify({
1539
- ...item,
1540
- excerpt: item.content.length > 140 ? `${item.content.slice(0, 137)}...` : item.content,
1541
- relevanceScore: this.relevanceScore(item),
1542
- importanceScore: this.importanceScore(item),
1543
- freshnessScore: this.freshnessScore(item),
1544
- trustScore: item.trust,
1545
- }, null, 2), 'utf8');
1725
+ fs.writeFileSync(path.join(this.vaultItemsDir, `${item.id}.json`), JSON.stringify(this.buildVaultItemPayload(item), null, 2), 'utf8');
1726
+ this.writeVaultNote(item);
1727
+ }
1728
+ writeVaultNote(item) {
1546
1729
  fs.writeFileSync(path.join(this.vaultNotesDir, 'memories', `${item.id}.md`), this.renderMemoryNote(item), 'utf8');
1547
1730
  }
1548
1731
  removeVaultItem(id) {
@@ -1716,33 +1899,17 @@ export class MemoryEngine {
1716
1899
  return [...ids];
1717
1900
  }
1718
1901
  reconcileCandidate(candidate, options) {
1719
- const related = this.listReconciliationCandidates()
1720
- .filter((item) => item.state !== 'expired')
1721
- .map((item) => ({
1722
- item,
1723
- overlap: this.wordOverlap(candidate.content.toLowerCase().split(/\W+/).filter(Boolean), item.content.toLowerCase().split(/\W+/).filter(Boolean)),
1724
- }))
1725
- .filter((entry) => entry.overlap >= 0.42)
1726
- .sort((left, right) => right.overlap - left.overlap)
1727
- .slice(0, 4);
1728
- const relatedIds = related.map((entry) => entry.item.id);
1729
- const contradiction = related.some((entry) => /\b(not|never|no longer|cannot|can't)\b/i.test(candidate.content) !== /\b(not|never|no longer|cannot|can't)\b/i.test(entry.item.content));
1730
- if (/delete|remove|obsolete|deprecated|no longer needed|superseded/i.test(candidate.content) && relatedIds.length > 0) {
1731
- return { action: 'DELETE', reason: 'Candidate indicates the prior memory should expire.', relatedIds };
1732
- }
1733
- if (contradiction && relatedIds.length > 0) {
1734
- return { action: 'UPDATE', reason: 'Candidate contradicts an existing memory and should supersede it.', relatedIds };
1735
- }
1736
- if (related.some((entry) => entry.overlap >= 0.9)) {
1737
- return { action: 'NONE', reason: 'Candidate duplicates an existing memory.', relatedIds };
1738
- }
1739
- if (related.some((entry) => entry.overlap >= 0.68)) {
1740
- return { action: 'MERGE', reason: 'Candidate overlaps strongly with an existing memory and should be linked.', relatedIds };
1741
- }
1742
- if (candidate.confidence < 0.52 || options.priority < 0.45) {
1743
- return { action: 'QUARANTINE', reason: 'Candidate has low confidence and should remain quarantined until validated.', relatedIds };
1744
- }
1745
- return { action: 'ADD', reason: 'Candidate is net new and worth storing.', relatedIds };
1902
+ const related = this.listReconciliationCandidates().map((item) => ({
1903
+ id: item.id,
1904
+ content: item.content,
1905
+ state: item.state,
1906
+ }));
1907
+ return reconcileCandidateFact(candidate, related, {
1908
+ priority: options.priority,
1909
+ contradictionThreshold: this.config.reconciliationContradictionThreshold,
1910
+ mergeThreshold: this.config.reconciliationMergeThreshold,
1911
+ duplicateThreshold: this.config.reconciliationDuplicateThreshold,
1912
+ });
1746
1913
  }
1747
1914
  expireMemories() {
1748
1915
  this.db.prepare(`
@@ -1818,6 +1985,7 @@ export class MemoryEngine {
1818
1985
  const rows = this.db.prepare(`
1819
1986
  SELECT id, tier, scope, state, source, priority, tags, trust
1820
1987
  FROM memories
1988
+ WHERE state != 'expired'
1821
1989
  ORDER BY priority DESC, timestamp DESC
1822
1990
  `).all();
1823
1991
  return rows.map((row) => ({
@@ -1860,6 +2028,15 @@ export class MemoryEngine {
1860
2028
  const rows = this.db.prepare('SELECT * FROM memories').all();
1861
2029
  return rows.map(row => this.rowToItem(row));
1862
2030
  }
2031
+ listVaultLiveItems() {
2032
+ const rows = this.db.prepare(`
2033
+ SELECT *
2034
+ FROM memories
2035
+ WHERE state != 'expired'
2036
+ ORDER BY priority DESC, timestamp DESC
2037
+ `).all();
2038
+ return rows.map((row) => this.rowToItem(row));
2039
+ }
1863
2040
  toSnapshot(item) {
1864
2041
  return {
1865
2042
  id: item.id,
@@ -2244,6 +2421,17 @@ export class MemoryEngine {
2244
2421
  sessionId,
2245
2422
  });
2246
2423
  }
2424
+ deriveExportOrigin(items) {
2425
+ const first = items[0];
2426
+ return {
2427
+ source: 'nexus-prime',
2428
+ tool: 'memory-engine',
2429
+ repoId: first?.provenance?.repoId ?? resolveCurrentRepoId(),
2430
+ workspaceId: first?.provenance?.workspaceId,
2431
+ projectId: first?.provenance?.projectId,
2432
+ sessionId: this.sessionId,
2433
+ };
2434
+ }
2247
2435
  exportBundle(options = {}) {
2248
2436
  const items = this.getAllItems()
2249
2437
  .filter((item) => !options.scope || item.scope === options.scope)
@@ -2254,6 +2442,10 @@ export class MemoryEngine {
2254
2442
  .map((item) => ({
2255
2443
  id: item.id,
2256
2444
  content: item.content,
2445
+ contentFingerprint: createMemoryContentFingerprint(item.content, {
2446
+ scope: item.scope,
2447
+ provenance: item.provenance,
2448
+ }),
2257
2449
  priority: item.priority,
2258
2450
  timestamp: item.timestamp,
2259
2451
  tags: item.tags,
@@ -2273,10 +2465,13 @@ export class MemoryEngine {
2273
2465
  supersededBy: item.supersededBy,
2274
2466
  provenance: item.provenance,
2275
2467
  }));
2468
+ const origin = this.deriveExportOrigin(items);
2276
2469
  return {
2277
- version: 1,
2470
+ version: 2,
2471
+ schemaVersion: 'nexus-memory-bundle.v2',
2278
2472
  exportedAt: Date.now(),
2279
2473
  sessionId: this.sessionId,
2474
+ origin,
2280
2475
  stats: this.getStats(),
2281
2476
  health: this.getHealthSummary(),
2282
2477
  items,
@@ -2293,20 +2488,96 @@ export class MemoryEngine {
2293
2488
  ? JSON.parse(fs.readFileSync(path.resolve(input.path), 'utf8'))
2294
2489
  : undefined);
2295
2490
  if (!bundle) {
2296
- return { imported: 0, duplicates: 0, quarantined: 0, importedIds: [] };
2491
+ return { imported: 0, duplicates: 0, added: 0, updated: 0, skipped: 0, quarantined: 0, importedIds: [] };
2297
2492
  }
2298
- let imported = 0;
2493
+ let added = 0;
2494
+ let updated = 0;
2299
2495
  let duplicates = 0;
2496
+ let skipped = 0;
2300
2497
  let quarantined = 0;
2301
2498
  const importedIds = [];
2499
+ const existingItems = this.getAllItems().filter((item) => item.state !== 'expired');
2500
+ const existingByFingerprint = new Map();
2501
+ for (const item of existingItems) {
2502
+ const fingerprint = createMemoryContentFingerprint(item.content, {
2503
+ scope: item.scope,
2504
+ provenance: item.provenance,
2505
+ });
2506
+ const current = existingByFingerprint.get(fingerprint);
2507
+ if (!current || current.timestamp < item.timestamp) {
2508
+ existingByFingerprint.set(fingerprint, item);
2509
+ }
2510
+ }
2302
2511
  for (const item of bundle.items) {
2512
+ const fingerprint = item.contentFingerprint ?? createMemoryContentFingerprint(item.content, {
2513
+ scope: item.scope,
2514
+ provenance: item.provenance,
2515
+ });
2303
2516
  const check = this.checkContent(item.content, {
2304
2517
  tags: item.tags,
2305
2518
  priority: item.priority,
2306
2519
  parentId: item.parentId,
2307
2520
  });
2521
+ const existing = existingByFingerprint.get(fingerprint);
2522
+ if (existing) {
2523
+ const mergedTags = dedupeStrings([
2524
+ ...existing.tags,
2525
+ ...item.tags,
2526
+ '#imported',
2527
+ item.scope === 'shared' ? '#shared' : '',
2528
+ item.scope === 'project' ? '#project' : '',
2529
+ item.scope === 'user' ? '#user' : '',
2530
+ item.scope === 'promoted' ? '#promoted' : '',
2531
+ (item.state === 'quarantined' || check.action === 'quarantine' || check.action === 'block') ? '#quarantine' : '',
2532
+ ].filter(Boolean));
2533
+ const nextState = item.state === 'quarantined' || check.action === 'quarantine' || check.action === 'block'
2534
+ ? 'quarantined'
2535
+ : item.state;
2536
+ const shouldUpdate = (existing.timestamp < item.timestamp
2537
+ || existing.priority < item.priority
2538
+ || existing.trust < item.trust
2539
+ || mergedTags.length !== existing.tags.length
2540
+ || nextState !== existing.state);
2541
+ if (!shouldUpdate) {
2542
+ duplicates += 1;
2543
+ skipped += 1;
2544
+ continue;
2545
+ }
2546
+ const mergedProvenance = createMemoryProvenance({
2547
+ ...existing.provenance,
2548
+ ...item.provenance,
2549
+ source: 'imported',
2550
+ tags: dedupeStrings([...(existing.provenance.tags ?? []), ...(item.provenance?.tags ?? []), ...mergedTags]),
2551
+ references: dedupeStrings([...(existing.provenance.references ?? []), ...(item.provenance?.references ?? [])]),
2552
+ summary: item.provenance?.summary ?? existing.provenance.summary,
2553
+ rawContext: item.provenance?.rawContext ?? existing.provenance.rawContext,
2554
+ });
2555
+ this.withSqliteRetry('importBundle.update-existing', () => {
2556
+ this.db.prepare(`
2557
+ UPDATE memories
2558
+ SET priority = ?, timestamp = ?, tags = ?, scope = ?, state = ?, source = ?, access_count = ?,
2559
+ entropy = ?, mass = ?, trust = ?, provenance_json = ?, expires_at = ?, supersedes = ?, superseded_by = ?
2560
+ WHERE id = ?
2561
+ `).run(Math.max(existing.priority, item.priority), Math.max(existing.timestamp, item.timestamp), JSON.stringify(mergedTags), item.scope, nextState, 'imported', Math.max(existing.accessCount, item.accessCount), Math.min(existing.entropy, item.entropy), Math.max(existing.mass, item.mass), Math.max(existing.trust, item.trust), JSON.stringify(mergedProvenance), item.expiresAt ?? existing.expiresAt ?? null, item.supersedes ?? existing.supersedes ?? null, item.supersededBy ?? existing.supersededBy ?? null, existing.id);
2562
+ });
2563
+ updated += 1;
2564
+ if (mergedTags.includes('#quarantine'))
2565
+ quarantined += 1;
2566
+ importedIds.push(existing.id);
2567
+ existingByFingerprint.set(fingerprint, {
2568
+ ...existing,
2569
+ priority: Math.max(existing.priority, item.priority),
2570
+ timestamp: Math.max(existing.timestamp, item.timestamp),
2571
+ tags: mergedTags,
2572
+ state: nextState,
2573
+ trust: Math.max(existing.trust, item.trust),
2574
+ provenance: mergedProvenance,
2575
+ });
2576
+ continue;
2577
+ }
2308
2578
  if (check.duplicateCluster.length > 0) {
2309
2579
  duplicates += 1;
2580
+ skipped += 1;
2310
2581
  continue;
2311
2582
  }
2312
2583
  const tags = dedupeStrings([
@@ -2332,13 +2603,25 @@ export class MemoryEngine {
2332
2603
  supersededBy: item.supersededBy,
2333
2604
  provenance: item.provenance,
2334
2605
  });
2335
- imported += 1;
2606
+ added += 1;
2336
2607
  if (tags.includes('#quarantine'))
2337
2608
  quarantined += 1;
2338
2609
  importedIds.push(id);
2610
+ const storedItem = this.getAllItems().find((entry) => entry.id === id);
2611
+ if (storedItem) {
2612
+ existingByFingerprint.set(fingerprint, storedItem);
2613
+ }
2339
2614
  }
2340
2615
  this.syncVault(true);
2341
- return { imported, duplicates, quarantined, importedIds };
2616
+ return {
2617
+ imported: added + updated,
2618
+ duplicates,
2619
+ added,
2620
+ updated,
2621
+ skipped,
2622
+ quarantined,
2623
+ importedIds,
2624
+ };
2342
2625
  }
2343
2626
  /**
2344
2627
  * Query memories by tag using direct SQL — precise, no fuzzy matching.
@@ -2358,10 +2641,90 @@ export class MemoryEngine {
2358
2641
  }
2359
2642
  close() {
2360
2643
  this.flush();
2644
+ this.expireMemories();
2645
+ const purgedCount = this.purgeExpiredMemories();
2361
2646
  this.flushVaultSync();
2647
+ this.optimizeDatabase(purgedCount, true);
2648
+ this.ngramIndex?.close();
2362
2649
  this.db.close();
2363
2650
  this.graphMirror?.close();
2364
2651
  }
2652
+ listVaultSnapshotPaths() {
2653
+ if (!fs.existsSync(this.vaultDir))
2654
+ return [];
2655
+ return fs.readdirSync(this.vaultDir)
2656
+ .filter((entry) => /^vault-snapshot-\d+\.json$/.test(entry))
2657
+ .map((entry) => path.join(this.vaultDir, entry));
2658
+ }
2659
+ buildVaultItemPayload(item) {
2660
+ return {
2661
+ ...item,
2662
+ excerpt: item.content.length > 140 ? `${item.content.slice(0, 137)}...` : item.content,
2663
+ relevanceScore: this.relevanceScore(item),
2664
+ importanceScore: this.importanceScore(item),
2665
+ freshnessScore: this.freshnessScore(item),
2666
+ trustScore: item.trust,
2667
+ };
2668
+ }
2669
+ purgeExpiredMemories() {
2670
+ let deleted = 0;
2671
+ let hasMore = true;
2672
+ while (hasMore) {
2673
+ const ids = this.db.prepare(`
2674
+ SELECT id
2675
+ FROM memories
2676
+ WHERE state = 'expired'
2677
+ ORDER BY timestamp ASC
2678
+ LIMIT ?
2679
+ `).all(this.config.expiredPurgeBatchSize);
2680
+ if (ids.length === 0) {
2681
+ hasMore = false;
2682
+ continue;
2683
+ }
2684
+ const deleteStmt = this.db.prepare('DELETE FROM memories WHERE id = ?');
2685
+ const deleteTxn = this.db.transaction((targetIds) => {
2686
+ for (const id of targetIds) {
2687
+ deleteStmt.run(id);
2688
+ }
2689
+ });
2690
+ const targetIds = ids.map((row) => row.id);
2691
+ deleteTxn(targetIds);
2692
+ for (const id of targetIds) {
2693
+ try {
2694
+ this.ngramIndex?.removeDocument(id);
2695
+ }
2696
+ catch {
2697
+ // Best effort index cleanup.
2698
+ }
2699
+ this.removeVaultItem(id);
2700
+ }
2701
+ deleted += targetIds.length;
2702
+ if (targetIds.length < this.config.expiredPurgeBatchSize) {
2703
+ hasMore = false;
2704
+ }
2705
+ }
2706
+ return deleted;
2707
+ }
2708
+ optimizeDatabase(purgedCount = 0, force = false) {
2709
+ try {
2710
+ this.db.exec('PRAGMA optimize');
2711
+ }
2712
+ catch {
2713
+ // Best effort optimize.
2714
+ }
2715
+ const now = Date.now();
2716
+ if (!force && (purgedCount <= 0 || now - this.lastDatabaseVacuumAt < this.config.vacuumMinIntervalMs)) {
2717
+ return;
2718
+ }
2719
+ try {
2720
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
2721
+ }
2722
+ catch {
2723
+ // Best effort checkpoint before vacuum.
2724
+ }
2725
+ this.db.exec('VACUUM');
2726
+ this.lastDatabaseVacuumAt = now;
2727
+ }
2365
2728
  mirrorIntoGraph(item) {
2366
2729
  if (!this.graphMirror)
2367
2730
  return;