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.
- package/README.md +10 -0
- package/dist/agents/adapters/mcp.d.ts.map +1 -1
- package/dist/agents/adapters/mcp.js +19 -1
- package/dist/agents/adapters/mcp.js.map +1 -1
- package/dist/dashboard/index.html +22 -2
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +36 -3
- package/dist/dashboard/server.js.map +1 -1
- package/dist/engines/client-bootstrap.d.ts +1 -0
- package/dist/engines/client-bootstrap.d.ts.map +1 -1
- package/dist/engines/client-bootstrap.js +36 -6
- package/dist/engines/client-bootstrap.js.map +1 -1
- package/dist/engines/context-assembler.d.ts +17 -0
- package/dist/engines/context-assembler.d.ts.map +1 -1
- package/dist/engines/context-assembler.js +72 -31
- package/dist/engines/context-assembler.js.map +1 -1
- package/dist/engines/embedder.d.ts +3 -1
- package/dist/engines/embedder.d.ts.map +1 -1
- package/dist/engines/embedder.js +20 -4
- package/dist/engines/embedder.js.map +1 -1
- package/dist/engines/event-bus.d.ts +9 -1
- package/dist/engines/event-bus.d.ts.map +1 -1
- package/dist/engines/event-bus.js +21 -0
- package/dist/engines/event-bus.js.map +1 -1
- package/dist/engines/index.d.ts +1 -1
- package/dist/engines/index.d.ts.map +1 -1
- package/dist/engines/index.js +1 -1
- package/dist/engines/index.js.map +1 -1
- package/dist/engines/instruction-gateway.d.ts.map +1 -1
- package/dist/engines/instruction-gateway.js +18 -7
- package/dist/engines/instruction-gateway.js.map +1 -1
- package/dist/engines/knowledge-fabric.d.ts +9 -0
- package/dist/engines/knowledge-fabric.d.ts.map +1 -1
- package/dist/engines/knowledge-fabric.js +56 -4
- package/dist/engines/knowledge-fabric.js.map +1 -1
- package/dist/engines/lifetime-tokens.d.ts +14 -0
- package/dist/engines/lifetime-tokens.d.ts.map +1 -0
- package/dist/engines/lifetime-tokens.js +37 -0
- package/dist/engines/lifetime-tokens.js.map +1 -0
- package/dist/engines/memory-bridge.d.ts +49 -19
- package/dist/engines/memory-bridge.d.ts.map +1 -1
- package/dist/engines/memory-bridge.js +141 -29
- package/dist/engines/memory-bridge.js.map +1 -1
- package/dist/engines/memory-control-plane.d.ts +27 -0
- package/dist/engines/memory-control-plane.d.ts.map +1 -1
- package/dist/engines/memory-control-plane.js +100 -0
- package/dist/engines/memory-control-plane.js.map +1 -1
- package/dist/engines/memory-extractor.d.ts +35 -0
- package/dist/engines/memory-extractor.d.ts.map +1 -0
- package/dist/engines/memory-extractor.js +164 -0
- package/dist/engines/memory-extractor.js.map +1 -0
- package/dist/engines/memory-fingerprint.d.ts +7 -0
- package/dist/engines/memory-fingerprint.d.ts.map +1 -0
- package/dist/engines/memory-fingerprint.js +22 -0
- package/dist/engines/memory-fingerprint.js.map +1 -0
- package/dist/engines/memory.d.ts +62 -8
- package/dist/engines/memory.d.ts.map +1 -1
- package/dist/engines/memory.js +445 -82
- package/dist/engines/memory.js.map +1 -1
- package/dist/engines/nexus-layer.d.ts +1 -6
- package/dist/engines/nexus-layer.d.ts.map +1 -1
- package/dist/engines/ngram-index.d.ts +2 -0
- package/dist/engines/ngram-index.d.ts.map +1 -1
- package/dist/engines/ngram-index.js +32 -0
- package/dist/engines/ngram-index.js.map +1 -1
- package/dist/engines/orchestrator.d.ts +3 -1
- package/dist/engines/orchestrator.d.ts.map +1 -1
- package/dist/engines/orchestrator.js +157 -31
- package/dist/engines/orchestrator.js.map +1 -1
- package/dist/engines/pattern-registry.d.ts.map +1 -1
- package/dist/engines/pattern-registry.js +9 -2
- package/dist/engines/pattern-registry.js.map +1 -1
- package/dist/engines/rag-collections.d.ts +8 -0
- package/dist/engines/rag-collections.d.ts.map +1 -1
- package/dist/engines/rag-collections.js +63 -14
- package/dist/engines/rag-collections.js.map +1 -1
- package/dist/engines/runtime-registry.d.ts +1 -0
- package/dist/engines/runtime-registry.d.ts.map +1 -1
- package/dist/engines/runtime-registry.js +9 -11
- package/dist/engines/runtime-registry.js.map +1 -1
- package/dist/engines/semantic-ranking.d.ts +27 -0
- package/dist/engines/semantic-ranking.d.ts.map +1 -0
- package/dist/engines/semantic-ranking.js +83 -0
- package/dist/engines/semantic-ranking.js.map +1 -0
- package/dist/engines/skill-runtime.d.ts +1 -0
- package/dist/engines/skill-runtime.d.ts.map +1 -1
- package/dist/engines/skill-runtime.js +58 -61
- package/dist/engines/skill-runtime.js.map +1 -1
- package/dist/engines/token-optimizer.d.ts +0 -16
- package/dist/engines/token-optimizer.d.ts.map +1 -1
- package/dist/engines/token-optimizer.js +0 -40
- package/dist/engines/token-optimizer.js.map +1 -1
- package/dist/engines/token-supremacy.d.ts +5 -1
- package/dist/engines/token-supremacy.d.ts.map +1 -1
- package/dist/engines/token-supremacy.js +8 -2
- package/dist/engines/token-supremacy.js.map +1 -1
- package/dist/engines/workflow-runtime.d.ts +2 -0
- package/dist/engines/workflow-runtime.d.ts.map +1 -1
- package/dist/engines/workflow-runtime.js +74 -38
- package/dist/engines/workflow-runtime.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/dist/nexus-prime.d.ts +1 -1
- package/dist/nexus-prime.d.ts.map +1 -1
- package/dist/nexus-prime.js +1 -1
- package/dist/nexus-prime.js.map +1 -1
- package/dist/phantom/index.d.ts +12 -2
- package/dist/phantom/index.d.ts.map +1 -1
- package/dist/phantom/index.js +29 -9
- package/dist/phantom/index.js.map +1 -1
- package/dist/phantom/runtime.d.ts +3 -2
- package/dist/phantom/runtime.d.ts.map +1 -1
- package/dist/phantom/runtime.js +80 -8
- package/dist/phantom/runtime.js.map +1 -1
- package/dist/postinstall-bootstrap.js +4 -1
- package/dist/postinstall-bootstrap.js.map +1 -1
- package/dist/synapse/bootstrap.d.ts.map +1 -1
- package/dist/synapse/bootstrap.js +9 -2
- package/dist/synapse/bootstrap.js.map +1 -1
- package/dist/synapse/mandate/asset-matcher.d.ts.map +1 -1
- package/dist/synapse/mandate/asset-matcher.js +26 -15
- package/dist/synapse/mandate/asset-matcher.js.map +1 -1
- package/package.json +2 -3
- package/dist/core/memory.d.ts +0 -96
- package/dist/core/memory.d.ts.map +0 -1
- package/dist/core/memory.js +0 -304
- package/dist/core/memory.js.map +0 -1
package/dist/engines/memory.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
971
|
-
if (files.length <= 10)
|
|
1124
|
+
if (items.length <= this.config.vaultCompactionThreshold)
|
|
972
1125
|
return null;
|
|
973
|
-
const consolidated =
|
|
974
|
-
for (const
|
|
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 {
|
|
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 {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1509
|
-
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1721
|
-
.
|
|
1722
|
-
item,
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
.
|
|
1728
|
-
|
|
1729
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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;
|