@hbarefoot/engram 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/engram.js +421 -0
- package/dashboard/dist/assets/index-BHkLa5w_.css +1 -0
- package/dashboard/dist/assets/index-D9QR_Cnu.js +45 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/package.json +21 -0
- package/package.json +76 -0
- package/src/config/index.js +150 -0
- package/src/embed/index.js +249 -0
- package/src/export/static.js +396 -0
- package/src/extract/rules.js +233 -0
- package/src/extract/secrets.js +114 -0
- package/src/index.js +54 -0
- package/src/memory/consolidate.js +420 -0
- package/src/memory/context.js +346 -0
- package/src/memory/feedback.js +197 -0
- package/src/memory/recall.js +350 -0
- package/src/memory/store.js +626 -0
- package/src/server/mcp.js +668 -0
- package/src/server/rest.js +499 -0
- package/src/utils/id.js +9 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/time.js +296 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patterns for detecting various types of secrets
|
|
3
|
+
*/
|
|
4
|
+
const SECRET_PATTERNS = [
|
|
5
|
+
// API Keys
|
|
6
|
+
{ pattern: /sk-[a-zA-Z0-9]{32,}/, name: 'OpenAI API Key' },
|
|
7
|
+
{ pattern: /pk_[a-zA-Z0-9]{32,}/, name: 'Stripe Publishable Key' },
|
|
8
|
+
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/, name: 'Stripe Secret Key' },
|
|
9
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, name: 'AWS Access Key ID' },
|
|
10
|
+
{ pattern: /ghp_[a-zA-Z0-9]{20,}/, name: 'GitHub Personal Access Token' },
|
|
11
|
+
{ pattern: /gho_[a-zA-Z0-9]{36,}/, name: 'GitHub OAuth Token' },
|
|
12
|
+
{ pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, name: 'GitHub Fine-grained PAT' },
|
|
13
|
+
{ pattern: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/, name: 'Slack Bot Token' },
|
|
14
|
+
{ pattern: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/, name: 'Slack User Token' },
|
|
15
|
+
{ pattern: /AIza[0-9A-Za-z\\-_]{35}/, name: 'Google API Key' },
|
|
16
|
+
{ pattern: /ya29\.[0-9A-Za-z\\-_]+/, name: 'Google OAuth Token' },
|
|
17
|
+
|
|
18
|
+
// Private Keys
|
|
19
|
+
{ pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, name: 'Private Key' },
|
|
20
|
+
{ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/, name: 'PGP Private Key' },
|
|
21
|
+
|
|
22
|
+
// Connection Strings with Credentials
|
|
23
|
+
{ pattern: /[a-zA-Z]+:\/\/[^:]+:[^@]+@[^\/]+/, name: 'Connection String with Credentials' },
|
|
24
|
+
{ pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@/, name: 'MongoDB Connection String' },
|
|
25
|
+
{ pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@/, name: 'PostgreSQL Connection String' },
|
|
26
|
+
{ pattern: /mysql:\/\/[^:]+:[^@]+@/, name: 'MySQL Connection String' },
|
|
27
|
+
|
|
28
|
+
// JWT Tokens
|
|
29
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/, name: 'JWT Token' },
|
|
30
|
+
|
|
31
|
+
// Generic High-Entropy Strings (common in secrets)
|
|
32
|
+
{ pattern: /['"]([\da-f]{32,}|[A-Za-z0-9+/]{40,}={0,2})['"]/, name: 'High-Entropy String' },
|
|
33
|
+
|
|
34
|
+
// Common Secret Environment Variables
|
|
35
|
+
{ pattern: /(password|passwd|pwd|secret|token|api_key|apikey|access_key|secret_key|private_key)\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i, name: 'Secret Environment Variable' }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if content contains secrets
|
|
40
|
+
* @param {string} content - Content to check
|
|
41
|
+
* @returns {Object} Result object with detected secrets
|
|
42
|
+
*/
|
|
43
|
+
export function detectSecrets(content) {
|
|
44
|
+
const detected = [];
|
|
45
|
+
|
|
46
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
47
|
+
const matches = content.match(pattern);
|
|
48
|
+
if (matches) {
|
|
49
|
+
detected.push({
|
|
50
|
+
type: name,
|
|
51
|
+
match: matches[0],
|
|
52
|
+
position: matches.index
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
hasSecrets: detected.length > 0,
|
|
59
|
+
secrets: detected
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Redact secrets from content
|
|
65
|
+
* @param {string} content - Content to redact
|
|
66
|
+
* @returns {string} Content with secrets redacted
|
|
67
|
+
*/
|
|
68
|
+
export function redactSecrets(content) {
|
|
69
|
+
let redacted = content;
|
|
70
|
+
|
|
71
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
72
|
+
// Use global flag to replace all occurrences
|
|
73
|
+
const flags = pattern.flags || '';
|
|
74
|
+
const globalPattern = new RegExp(pattern.source, flags.includes('g') ? flags : flags + 'g');
|
|
75
|
+
redacted = redacted.replace(globalPattern, '[REDACTED]');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return redacted;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate that content is safe to store
|
|
83
|
+
* @param {string} content - Content to validate
|
|
84
|
+
* @param {Object} [options] - Validation options
|
|
85
|
+
* @param {boolean} [options.autoRedact=false] - Auto-redact instead of rejecting
|
|
86
|
+
* @returns {Object} Validation result
|
|
87
|
+
*/
|
|
88
|
+
export function validateContent(content, options = {}) {
|
|
89
|
+
const { autoRedact = false } = options;
|
|
90
|
+
const detection = detectSecrets(content);
|
|
91
|
+
|
|
92
|
+
if (!detection.hasSecrets) {
|
|
93
|
+
return {
|
|
94
|
+
valid: true,
|
|
95
|
+
content,
|
|
96
|
+
warnings: []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (autoRedact) {
|
|
101
|
+
return {
|
|
102
|
+
valid: true,
|
|
103
|
+
content: redactSecrets(content),
|
|
104
|
+
warnings: detection.secrets.map(s => `Redacted ${s.type}`)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
content,
|
|
111
|
+
errors: detection.secrets.map(s => `Detected ${s.type}: ${s.match.substring(0, 20)}...`),
|
|
112
|
+
warnings: ['Content contains sensitive information and was rejected']
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engram - Persistent Memory for AI Agents
|
|
3
|
+
* Main entry point for programmatic usage
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { createMemory, recallMemories, initDatabase } from 'engram';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Configuration
|
|
10
|
+
export { loadConfig, getDatabasePath, getModelsPath } from './config/index.js';
|
|
11
|
+
|
|
12
|
+
// Memory operations
|
|
13
|
+
export {
|
|
14
|
+
initDatabase,
|
|
15
|
+
createMemory,
|
|
16
|
+
getMemory,
|
|
17
|
+
deleteMemory,
|
|
18
|
+
listMemories,
|
|
19
|
+
getStats
|
|
20
|
+
} from './memory/store.js';
|
|
21
|
+
|
|
22
|
+
// Recall & search
|
|
23
|
+
export {
|
|
24
|
+
recallMemories,
|
|
25
|
+
formatRecallResults
|
|
26
|
+
} from './memory/recall.js';
|
|
27
|
+
|
|
28
|
+
// Consolidation
|
|
29
|
+
export {
|
|
30
|
+
consolidate,
|
|
31
|
+
getConflicts
|
|
32
|
+
} from './memory/consolidate.js';
|
|
33
|
+
|
|
34
|
+
// Extraction
|
|
35
|
+
export {
|
|
36
|
+
extractMemory
|
|
37
|
+
} from './extract/rules.js';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
validateContent
|
|
41
|
+
} from './extract/secrets.js';
|
|
42
|
+
|
|
43
|
+
// Embedding
|
|
44
|
+
export {
|
|
45
|
+
generateEmbedding,
|
|
46
|
+
cosineSimilarity
|
|
47
|
+
} from './embed/index.js';
|
|
48
|
+
|
|
49
|
+
// Utilities
|
|
50
|
+
export {
|
|
51
|
+
generateId
|
|
52
|
+
} from './utils/id.js';
|
|
53
|
+
|
|
54
|
+
export * as logger from './utils/logger.js';
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { getMemoriesWithEmbeddings, listMemories, updateMemory, deleteMemory } from './store.js';
|
|
2
|
+
import { cosineSimilarity } from '../embed/index.js';
|
|
3
|
+
import * as logger from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run full consolidation process
|
|
7
|
+
* @param {Database} db - SQLite database instance
|
|
8
|
+
* @param {Object} [options] - Consolidation options
|
|
9
|
+
* @param {boolean} [options.detectDuplicates=true] - Enable duplicate detection
|
|
10
|
+
* @param {boolean} [options.detectContradictions=true] - Enable contradiction detection
|
|
11
|
+
* @param {boolean} [options.applyDecay=true] - Enable confidence decay
|
|
12
|
+
* @param {boolean} [options.cleanupStale=false] - Enable stale cleanup
|
|
13
|
+
* @param {number} [options.duplicateThreshold=0.92] - Similarity threshold for duplicates
|
|
14
|
+
* @returns {Promise<Object>} Consolidation results
|
|
15
|
+
*/
|
|
16
|
+
export async function consolidate(db, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
detectDuplicates = true,
|
|
19
|
+
detectContradictions = true,
|
|
20
|
+
applyDecay = true,
|
|
21
|
+
cleanupStale = false,
|
|
22
|
+
duplicateThreshold = 0.92
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
logger.info('Starting memory consolidation', options);
|
|
26
|
+
|
|
27
|
+
const results = {
|
|
28
|
+
duplicatesRemoved: 0,
|
|
29
|
+
contradictionsDetected: 0,
|
|
30
|
+
memoriesDecayed: 0,
|
|
31
|
+
staleMemoriesCleaned: 0,
|
|
32
|
+
startTime: Date.now()
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Step 1: Detect and merge duplicates
|
|
37
|
+
if (detectDuplicates) {
|
|
38
|
+
logger.debug('Detecting duplicates...');
|
|
39
|
+
const duplicates = await findDuplicates(db, duplicateThreshold);
|
|
40
|
+
results.duplicatesRemoved = await mergeDuplicates(db, duplicates);
|
|
41
|
+
logger.info('Duplicates removed', { count: results.duplicatesRemoved });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 2: Detect contradictions
|
|
45
|
+
if (detectContradictions) {
|
|
46
|
+
logger.debug('Detecting contradictions...');
|
|
47
|
+
const contradictions = await findContradictions(db);
|
|
48
|
+
results.contradictionsDetected = contradictions.length;
|
|
49
|
+
// Flag contradictions (don't auto-resolve)
|
|
50
|
+
await flagContradictions(db, contradictions);
|
|
51
|
+
logger.info('Contradictions detected', { count: results.contradictionsDetected });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 3: Apply confidence decay
|
|
55
|
+
if (applyDecay) {
|
|
56
|
+
logger.debug('Applying confidence decay...');
|
|
57
|
+
results.memoriesDecayed = await applyConfidenceDecay(db);
|
|
58
|
+
logger.info('Memories decayed', { count: results.memoriesDecayed });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 4: Cleanup stale memories
|
|
62
|
+
if (cleanupStale) {
|
|
63
|
+
logger.debug('Cleaning up stale memories...');
|
|
64
|
+
results.staleMemoriesCleaned = await cleanupStaleMemories(db);
|
|
65
|
+
logger.info('Stale memories cleaned', { count: results.staleMemoriesCleaned });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
results.duration = Date.now() - results.startTime;
|
|
69
|
+
logger.info('Consolidation complete', results);
|
|
70
|
+
|
|
71
|
+
return results;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error('Consolidation failed', { error: error.message });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find duplicate memories based on embedding similarity
|
|
80
|
+
* @param {Database} db - SQLite database instance
|
|
81
|
+
* @param {number} threshold - Similarity threshold (default 0.92)
|
|
82
|
+
* @returns {Promise<Array>} Array of duplicate pairs
|
|
83
|
+
*/
|
|
84
|
+
async function findDuplicates(db, threshold = 0.92) {
|
|
85
|
+
const memories = getMemoriesWithEmbeddings(db);
|
|
86
|
+
const duplicates = [];
|
|
87
|
+
|
|
88
|
+
logger.debug('Checking for duplicates', { memories: memories.length, threshold });
|
|
89
|
+
|
|
90
|
+
// Compare each memory with every other memory
|
|
91
|
+
for (let i = 0; i < memories.length; i++) {
|
|
92
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
93
|
+
const memA = memories[i];
|
|
94
|
+
const memB = memories[j];
|
|
95
|
+
|
|
96
|
+
// Skip if in different namespaces
|
|
97
|
+
if (memA.namespace !== memB.namespace) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate similarity
|
|
102
|
+
const similarity = cosineSimilarity(memA.embedding, memB.embedding);
|
|
103
|
+
|
|
104
|
+
if (similarity > threshold) {
|
|
105
|
+
duplicates.push({
|
|
106
|
+
memory1: memA,
|
|
107
|
+
memory2: memB,
|
|
108
|
+
similarity
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.debug('Duplicates found', { count: duplicates.length });
|
|
115
|
+
|
|
116
|
+
return duplicates;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge duplicate memories
|
|
121
|
+
* @param {Database} db - SQLite database instance
|
|
122
|
+
* @param {Array} duplicates - Array of duplicate pairs
|
|
123
|
+
* @returns {Promise<number>} Number of duplicates removed
|
|
124
|
+
*/
|
|
125
|
+
async function mergeDuplicates(db, duplicates) {
|
|
126
|
+
let removed = 0;
|
|
127
|
+
|
|
128
|
+
for (const { memory1, memory2, similarity } of duplicates) {
|
|
129
|
+
try {
|
|
130
|
+
// Determine which memory to keep (higher confidence, more accesses, or more recent)
|
|
131
|
+
let keepMemory, removeMemory;
|
|
132
|
+
|
|
133
|
+
if (memory1.confidence > memory2.confidence) {
|
|
134
|
+
keepMemory = memory1;
|
|
135
|
+
removeMemory = memory2;
|
|
136
|
+
} else if (memory1.confidence < memory2.confidence) {
|
|
137
|
+
keepMemory = memory2;
|
|
138
|
+
removeMemory = memory1;
|
|
139
|
+
} else if (memory1.access_count > memory2.access_count) {
|
|
140
|
+
keepMemory = memory1;
|
|
141
|
+
removeMemory = memory2;
|
|
142
|
+
} else if (memory1.access_count < memory2.access_count) {
|
|
143
|
+
keepMemory = memory2;
|
|
144
|
+
removeMemory = memory1;
|
|
145
|
+
} else if (memory1.updated_at > memory2.updated_at) {
|
|
146
|
+
keepMemory = memory1;
|
|
147
|
+
removeMemory = memory2;
|
|
148
|
+
} else {
|
|
149
|
+
keepMemory = memory2;
|
|
150
|
+
removeMemory = memory1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Merge access counts
|
|
154
|
+
const mergedAccessCount = keepMemory.access_count + removeMemory.access_count;
|
|
155
|
+
|
|
156
|
+
// Update the kept memory with merged stats
|
|
157
|
+
updateMemory(db, keepMemory.id, {
|
|
158
|
+
access_count: mergedAccessCount
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Delete the duplicate
|
|
162
|
+
deleteMemory(db, removeMemory.id);
|
|
163
|
+
|
|
164
|
+
logger.debug('Duplicate merged', {
|
|
165
|
+
kept: keepMemory.id.substring(0, 8),
|
|
166
|
+
removed: removeMemory.id.substring(0, 8),
|
|
167
|
+
similarity: similarity.toFixed(3)
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
removed++;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.warn('Failed to merge duplicate', { error: error.message });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return removed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find contradictory memories
|
|
181
|
+
* @param {Database} db - SQLite database instance
|
|
182
|
+
* @returns {Promise<Array>} Array of contradiction pairs
|
|
183
|
+
*/
|
|
184
|
+
async function findContradictions(db) {
|
|
185
|
+
const memories = getMemoriesWithEmbeddings(db);
|
|
186
|
+
const contradictions = [];
|
|
187
|
+
|
|
188
|
+
// Group memories by entity
|
|
189
|
+
const byEntity = new Map();
|
|
190
|
+
for (const memory of memories) {
|
|
191
|
+
if (!memory.entity) continue;
|
|
192
|
+
|
|
193
|
+
if (!byEntity.has(memory.entity)) {
|
|
194
|
+
byEntity.set(memory.entity, []);
|
|
195
|
+
}
|
|
196
|
+
byEntity.get(memory.entity).push(memory);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Look for contradictions within each entity group
|
|
200
|
+
for (const [entity, entityMemories] of byEntity.entries()) {
|
|
201
|
+
if (entityMemories.length < 2) continue;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < entityMemories.length; i++) {
|
|
204
|
+
for (let j = i + 1; j < entityMemories.length; j++) {
|
|
205
|
+
const memA = entityMemories[i];
|
|
206
|
+
const memB = entityMemories[j];
|
|
207
|
+
|
|
208
|
+
// Skip if in different namespaces
|
|
209
|
+
if (memA.namespace !== memB.namespace) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if contents suggest contradiction
|
|
214
|
+
if (seemsContradictory(memA.content, memB.content)) {
|
|
215
|
+
contradictions.push({
|
|
216
|
+
memory1: memA,
|
|
217
|
+
memory2: memB,
|
|
218
|
+
entity
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return contradictions;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if two memory contents seem contradictory
|
|
230
|
+
* @param {string} contentA - First memory content
|
|
231
|
+
* @param {string} contentB - Second memory content
|
|
232
|
+
* @returns {boolean} True if potentially contradictory
|
|
233
|
+
*/
|
|
234
|
+
function seemsContradictory(contentA, contentB) {
|
|
235
|
+
const lowerA = contentA.toLowerCase();
|
|
236
|
+
const lowerB = contentB.toLowerCase();
|
|
237
|
+
|
|
238
|
+
// Check for explicit contradictions
|
|
239
|
+
const contradictionPatterns = [
|
|
240
|
+
// Preferences
|
|
241
|
+
{ a: /prefer.*?(\w+)/, b: /prefer.*?(\w+)/ },
|
|
242
|
+
{ a: /uses?\s+(\w+)/, b: /uses?\s+(\w+)/ },
|
|
243
|
+
{ a: /instead of\s+(\w+)/, b: /instead of\s+(\w+)/ },
|
|
244
|
+
|
|
245
|
+
// Facts
|
|
246
|
+
{ a: /is\s+(\w+)/, b: /is\s+(\w+)/ },
|
|
247
|
+
{ a: /version\s+(\d+)/, b: /version\s+(\d+)/ },
|
|
248
|
+
|
|
249
|
+
// Negations
|
|
250
|
+
{ a: /never|not|doesn't|don't/, b: /always|does|do/ }
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
// Simple heuristic: if one contains "not"/"never" and the other doesn't
|
|
254
|
+
const hasNegationA = /\b(not|never|doesn't|don't|avoid|dislike)\b/i.test(lowerA);
|
|
255
|
+
const hasNegationB = /\b(not|never|doesn't|don't|avoid|dislike)\b/i.test(lowerB);
|
|
256
|
+
|
|
257
|
+
if (hasNegationA !== hasNegationB) {
|
|
258
|
+
// Remove negation words and check similarity
|
|
259
|
+
const normalizedA = lowerA.replace(/\b(not|never|doesn't|don't|avoid|dislike)\b/gi, '');
|
|
260
|
+
const normalizedB = lowerB.replace(/\b(not|never|doesn't|don't|avoid|dislike)\b/gi, '');
|
|
261
|
+
|
|
262
|
+
// If the rest is similar, it's likely a contradiction
|
|
263
|
+
const similarity = simpleSimilarity(normalizedA, normalizedB);
|
|
264
|
+
if (similarity > 0.6) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Simple text similarity calculation
|
|
274
|
+
* @param {string} a - First text
|
|
275
|
+
* @param {string} b - Second text
|
|
276
|
+
* @returns {number} Similarity score (0-1)
|
|
277
|
+
*/
|
|
278
|
+
function simpleSimilarity(a, b) {
|
|
279
|
+
const wordsA = new Set(a.toLowerCase().match(/\b\w+\b/g) || []);
|
|
280
|
+
const wordsB = new Set(b.toLowerCase().match(/\b\w+\b/g) || []);
|
|
281
|
+
|
|
282
|
+
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
|
|
283
|
+
const union = new Set([...wordsA, ...wordsB]);
|
|
284
|
+
|
|
285
|
+
return union.size > 0 ? intersection.size / union.size : 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Flag contradictions for user review
|
|
290
|
+
* @param {Database} db - SQLite database instance
|
|
291
|
+
* @param {Array} contradictions - Array of contradiction pairs
|
|
292
|
+
* @returns {Promise<number>} Number of contradictions flagged
|
|
293
|
+
*/
|
|
294
|
+
async function flagContradictions(db, contradictions) {
|
|
295
|
+
let flagged = 0;
|
|
296
|
+
|
|
297
|
+
for (const { memory1, memory2 } of contradictions) {
|
|
298
|
+
try {
|
|
299
|
+
// Generate a conflict ID for this pair
|
|
300
|
+
const conflictId = `conflict_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
301
|
+
|
|
302
|
+
// Add conflict tag to both memories
|
|
303
|
+
const tags1 = memory1.tags || [];
|
|
304
|
+
const tags2 = memory2.tags || [];
|
|
305
|
+
|
|
306
|
+
if (!tags1.includes(conflictId)) {
|
|
307
|
+
updateMemory(db, memory1.id, {
|
|
308
|
+
tags: [...tags1, conflictId, 'has-conflict']
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!tags2.includes(conflictId)) {
|
|
313
|
+
updateMemory(db, memory2.id, {
|
|
314
|
+
tags: [...tags2, conflictId, 'has-conflict']
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
flagged++;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
logger.warn('Failed to flag contradiction', { error: error.message });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return flagged;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Apply confidence decay to memories based on age and access
|
|
329
|
+
* @param {Database} db - SQLite database instance
|
|
330
|
+
* @returns {Promise<number>} Number of memories decayed
|
|
331
|
+
*/
|
|
332
|
+
async function applyConfidenceDecay(db) {
|
|
333
|
+
const memories = listMemories(db, { limit: 10000 }); // Get all memories
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
let decayed = 0;
|
|
336
|
+
|
|
337
|
+
for (const memory of memories) {
|
|
338
|
+
if (memory.decay_rate === 0) {
|
|
339
|
+
continue; // Skip memories that never decay
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const lastAccessed = memory.last_accessed || memory.created_at;
|
|
343
|
+
const daysSinceAccess = (now - lastAccessed) / (1000 * 60 * 60 * 24);
|
|
344
|
+
|
|
345
|
+
// Calculate decay
|
|
346
|
+
const decay = memory.decay_rate * daysSinceAccess;
|
|
347
|
+
const newConfidence = Math.max(0.1, memory.confidence * (1 - decay));
|
|
348
|
+
|
|
349
|
+
// Only update if confidence changed significantly
|
|
350
|
+
if (Math.abs(newConfidence - memory.confidence) > 0.01) {
|
|
351
|
+
updateMemory(db, memory.id, { confidence: newConfidence });
|
|
352
|
+
decayed++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return decayed;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Clean up stale memories
|
|
361
|
+
* @param {Database} db - SQLite database instance
|
|
362
|
+
* @returns {Promise<number>} Number of memories cleaned
|
|
363
|
+
*/
|
|
364
|
+
async function cleanupStaleMemories(db) {
|
|
365
|
+
const memories = listMemories(db, { limit: 10000 });
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
let cleaned = 0;
|
|
368
|
+
|
|
369
|
+
for (const memory of memories) {
|
|
370
|
+
// Criteria for stale: confidence < 0.15, no accesses, and > 90 days old
|
|
371
|
+
const age = (now - memory.created_at) / (1000 * 60 * 60 * 24);
|
|
372
|
+
|
|
373
|
+
if (memory.confidence < 0.15 && memory.access_count === 0 && age > 90) {
|
|
374
|
+
// Mark as stale (add tag) instead of deleting
|
|
375
|
+
const tags = memory.tags || [];
|
|
376
|
+
if (!tags.includes('stale')) {
|
|
377
|
+
updateMemory(db, memory.id, {
|
|
378
|
+
tags: [...tags, 'stale']
|
|
379
|
+
});
|
|
380
|
+
cleaned++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return cleaned;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get memories flagged with contradictions
|
|
390
|
+
* @param {Database} db - SQLite database instance
|
|
391
|
+
* @returns {Object[]} Array of memories with conflict tags
|
|
392
|
+
*/
|
|
393
|
+
export function getConflicts(db) {
|
|
394
|
+
const memories = listMemories(db, { limit: 10000 });
|
|
395
|
+
|
|
396
|
+
// Find all memories with conflict tags
|
|
397
|
+
const conflicts = memories.filter(m =>
|
|
398
|
+
m.tags && m.tags.some(tag => tag.startsWith('conflict_') || tag === 'has-conflict')
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Group by conflict ID
|
|
402
|
+
const grouped = new Map();
|
|
403
|
+
|
|
404
|
+
for (const memory of conflicts) {
|
|
405
|
+
const conflictTags = memory.tags.filter(tag => tag.startsWith('conflict_'));
|
|
406
|
+
|
|
407
|
+
for (const conflictId of conflictTags) {
|
|
408
|
+
if (!grouped.has(conflictId)) {
|
|
409
|
+
grouped.set(conflictId, []);
|
|
410
|
+
}
|
|
411
|
+
grouped.get(conflictId).push(memory);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Convert to array of conflict pairs
|
|
416
|
+
return Array.from(grouped.entries()).map(([conflictId, memories]) => ({
|
|
417
|
+
conflictId,
|
|
418
|
+
memories
|
|
419
|
+
}));
|
|
420
|
+
}
|