shieldcortex 3.3.0 → 3.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/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/ffdceca8b5f407c5.js +9 -0
- package/dist/api/routes/memories.js +24 -2
- package/dist/memory/consolidate.d.ts +13 -0
- package/dist/memory/consolidate.js +83 -72
- package/dist/memory/store.d.ts +3 -0
- package/dist/memory/store.js +98 -1
- package/dist/setup/codex.js +55 -18
- package/package.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +0 -9
- /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_ssgManifest.js +0 -0
|
@@ -2,9 +2,9 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
4
|
import { getDatabase } from '../../database/init.js';
|
|
5
|
-
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
|
|
5
|
+
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, mergeMemories, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
|
|
6
6
|
import { calculateDecayedScore } from '../../memory/decay.js';
|
|
7
|
-
import { consolidate, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
|
|
7
|
+
import { consolidate, findDuplicateMemoryPairs, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
|
|
8
8
|
import { getActivationStats, getActiveMemories } from '../../memory/activation.js';
|
|
9
9
|
import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
|
|
10
10
|
import { emitConsolidation } from '../events.js';
|
|
@@ -383,6 +383,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
383
383
|
minScore: 0.4,
|
|
384
384
|
limit,
|
|
385
385
|
});
|
|
386
|
+
const duplicates = findDuplicateMemoryPairs({ project, limit });
|
|
386
387
|
res.json({
|
|
387
388
|
summary: {
|
|
388
389
|
stale: stale.length,
|
|
@@ -391,6 +392,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
391
392
|
noisyAutoExtracted: noisyAutoExtracted.length,
|
|
392
393
|
projectless: projectless.length,
|
|
393
394
|
contradictions: contradictions.length,
|
|
395
|
+
duplicates: duplicates.length,
|
|
394
396
|
},
|
|
395
397
|
openClaw: {
|
|
396
398
|
total: openClawSummary.total ?? 0,
|
|
@@ -405,6 +407,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
405
407
|
lowTrust: lowTrust.map(rowToMemory),
|
|
406
408
|
noisyAutoExtracted: noisyAutoExtracted.map(rowToMemory),
|
|
407
409
|
projectless: projectless.map(rowToMemory),
|
|
410
|
+
duplicates,
|
|
408
411
|
contradictions: contradictions.map((item) => ({
|
|
409
412
|
memoryA: item.memoryA,
|
|
410
413
|
memoryB: item.memoryB,
|
|
@@ -419,6 +422,25 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
419
422
|
res.status(500).json({ error: error.message });
|
|
420
423
|
}
|
|
421
424
|
});
|
|
425
|
+
app.post('/api/memories/merge', requireNotLocked, (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { keptId, removedId, reviewedBy } = req.body;
|
|
428
|
+
if (!Number.isInteger(keptId) || !Number.isInteger(removedId)) {
|
|
429
|
+
return res.status(400).json({ error: 'keptId and removedId are required integers' });
|
|
430
|
+
}
|
|
431
|
+
if (keptId === removedId) {
|
|
432
|
+
return res.status(400).json({ error: 'keptId and removedId must be different' });
|
|
433
|
+
}
|
|
434
|
+
const merged = mergeMemories(keptId, removedId, { reviewedBy });
|
|
435
|
+
if (!merged) {
|
|
436
|
+
return res.status(404).json({ error: 'Unable to merge memories' });
|
|
437
|
+
}
|
|
438
|
+
res.json(merged);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
res.status(500).json({ error: error.message });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
422
444
|
app.get('/api/memories/:id', requireNotLocked, (req, res) => {
|
|
423
445
|
try {
|
|
424
446
|
const id = parseInt(req.params.id, 10);
|
|
@@ -13,6 +13,19 @@ import { Memory, MemoryConfig, ConsolidationResult, ContextSummary } from './typ
|
|
|
13
13
|
* This is like the brain's sleep consolidation - should be run periodically
|
|
14
14
|
*/
|
|
15
15
|
export declare function consolidate(config?: MemoryConfig): ConsolidationResult;
|
|
16
|
+
export interface DuplicateMemoryPair {
|
|
17
|
+
memoryA: Memory;
|
|
18
|
+
memoryB: Memory;
|
|
19
|
+
recommendedKeepId: number;
|
|
20
|
+
similarity: string;
|
|
21
|
+
titleSimilarity: number;
|
|
22
|
+
contentOverlap: number;
|
|
23
|
+
sharedWords: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function findDuplicateMemoryPairs(options?: {
|
|
26
|
+
project?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}): DuplicateMemoryPair[];
|
|
16
29
|
/**
|
|
17
30
|
* Find memories with very similar titles/content and merge them.
|
|
18
31
|
* Keeps the newer one, appends unique content from the older one.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { getDatabase, withTransaction } from '../database/init.js';
|
|
11
11
|
import { DEFAULT_CONFIG, } from './types.js';
|
|
12
|
-
import { getMemoriesByType, getRecentMemories, promoteMemory, deleteMemory, searchMemories, getMemoryStats, updateDecayScores, addMemory, } from './store.js';
|
|
12
|
+
import { getMemoriesByType, getRecentMemories, promoteMemory, deleteMemory, searchMemories, getMemoryStats, updateDecayScores, addMemory, rowToMemory, } from './store.js';
|
|
13
13
|
import { processDecay, } from './decay.js';
|
|
14
14
|
import { detectContradictions, linkContradictions, } from './contradiction.js';
|
|
15
15
|
import { jaccardSimilarity } from './similarity.js';
|
|
@@ -191,6 +191,67 @@ function sharedWordCount(a, b) {
|
|
|
191
191
|
}
|
|
192
192
|
return count;
|
|
193
193
|
}
|
|
194
|
+
export function findDuplicateMemoryPairs(options) {
|
|
195
|
+
const db = getDatabase();
|
|
196
|
+
const limit = options?.limit ?? 20;
|
|
197
|
+
const rows = options?.project
|
|
198
|
+
? db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND project = ? ORDER BY created_at ASC").all(options.project)
|
|
199
|
+
: db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY created_at ASC").all();
|
|
200
|
+
const groups = new Map();
|
|
201
|
+
for (const mem of rows) {
|
|
202
|
+
const cat = mem.category || 'note';
|
|
203
|
+
if (!groups.has(cat))
|
|
204
|
+
groups.set(cat, []);
|
|
205
|
+
groups.get(cat).push(mem);
|
|
206
|
+
}
|
|
207
|
+
const pairs = [];
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
for (const [, group] of groups) {
|
|
210
|
+
if (group.length < 2)
|
|
211
|
+
continue;
|
|
212
|
+
for (let i = 0; i < group.length && pairs.length < limit; i++) {
|
|
213
|
+
const memA = group[i];
|
|
214
|
+
for (let j = i + 1; j < group.length && pairs.length < limit; j++) {
|
|
215
|
+
const memB = group[j];
|
|
216
|
+
const idA = memA.id;
|
|
217
|
+
const idB = memB.id;
|
|
218
|
+
const pairKey = `${Math.min(idA, idB)}:${Math.max(idA, idB)}`;
|
|
219
|
+
if (seen.has(pairKey))
|
|
220
|
+
continue;
|
|
221
|
+
const titleA = memA.title || '';
|
|
222
|
+
const titleB = memB.title || '';
|
|
223
|
+
const contentA = memA.content || '';
|
|
224
|
+
const contentB = memB.content || '';
|
|
225
|
+
const titleSimilarity = levenshteinSimilarity(titleA.toLowerCase(), titleB.toLowerCase());
|
|
226
|
+
const sharedWords = sharedWordCount(titleA, titleB);
|
|
227
|
+
const titlesMatch = titleSimilarity > 0.7 || sharedWords >= 3 || titleA.toLowerCase() === titleB.toLowerCase();
|
|
228
|
+
if (!titlesMatch)
|
|
229
|
+
continue;
|
|
230
|
+
const contentOverlap = wordOverlap(contentA, contentB);
|
|
231
|
+
if (contentOverlap <= 0.5)
|
|
232
|
+
continue;
|
|
233
|
+
const salienceA = memA.salience || 0;
|
|
234
|
+
const salienceB = memB.salience || 0;
|
|
235
|
+
const createdA = new Date(memA.created_at || 0).getTime();
|
|
236
|
+
const createdB = new Date(memB.created_at || 0).getTime();
|
|
237
|
+
const recommendedKeepId = salienceA > salienceB || (salienceA === salienceB && createdA >= createdB)
|
|
238
|
+
? idA
|
|
239
|
+
: idB;
|
|
240
|
+
pairs.push({
|
|
241
|
+
memoryA: rowToMemory(memA),
|
|
242
|
+
memoryB: rowToMemory(memB),
|
|
243
|
+
recommendedKeepId,
|
|
244
|
+
similarity: `title=${titleSimilarity.toFixed(2)}, content=${contentOverlap.toFixed(2)}`,
|
|
245
|
+
titleSimilarity,
|
|
246
|
+
contentOverlap,
|
|
247
|
+
sharedWords,
|
|
248
|
+
});
|
|
249
|
+
seen.add(pairKey);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return pairs;
|
|
254
|
+
}
|
|
194
255
|
/**
|
|
195
256
|
* Find memories with very similar titles/content and merge them.
|
|
196
257
|
* Keeps the newer one, appends unique content from the older one.
|
|
@@ -200,81 +261,31 @@ export function deduplicateMemories(options) {
|
|
|
200
261
|
return withTransaction(() => {
|
|
201
262
|
const db = getDatabase();
|
|
202
263
|
const pairs = [];
|
|
203
|
-
// Query all LTM memories
|
|
204
|
-
const ltmMemories = db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY created_at ASC").all();
|
|
205
|
-
// Group by category
|
|
206
|
-
const groups = new Map();
|
|
207
|
-
for (const mem of ltmMemories) {
|
|
208
|
-
const cat = mem.category || 'note';
|
|
209
|
-
if (!groups.has(cat))
|
|
210
|
-
groups.set(cat, []);
|
|
211
|
-
groups.get(cat).push(mem);
|
|
212
|
-
}
|
|
213
264
|
const removed = new Set();
|
|
214
|
-
for (const
|
|
215
|
-
if (
|
|
265
|
+
for (const pair of findDuplicateMemoryPairs()) {
|
|
266
|
+
if (removed.has(pair.memoryA.id) || removed.has(pair.memoryB.id))
|
|
216
267
|
continue;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
// Check title similarity: Levenshtein > 0.7 OR 3+ shared words
|
|
230
|
-
const titleSim = levenshteinSimilarity(titleA.toLowerCase(), titleB.toLowerCase());
|
|
231
|
-
const titleSharedWords = sharedWordCount(titleA, titleB);
|
|
232
|
-
const titlesMatch = titleSim > 0.7 || titleSharedWords >= 3;
|
|
233
|
-
if (!titlesMatch)
|
|
234
|
-
continue;
|
|
235
|
-
// Check content overlap > 50%
|
|
236
|
-
const contentOverlap = wordOverlap(contentA, contentB);
|
|
237
|
-
if (contentOverlap <= 0.5)
|
|
238
|
-
continue;
|
|
239
|
-
// They are duplicates — keep the one with higher salience (or newer if equal)
|
|
240
|
-
const salienceA = memA.salience || 0;
|
|
241
|
-
const salienceB = memB.salience || 0;
|
|
242
|
-
const createdA = new Date(memA.created_at || 0).getTime();
|
|
243
|
-
const createdB = new Date(memB.created_at || 0).getTime();
|
|
244
|
-
let kept;
|
|
245
|
-
let discarded;
|
|
246
|
-
if (salienceA > salienceB || (salienceA === salienceB && createdA >= createdB)) {
|
|
247
|
-
kept = memA;
|
|
248
|
-
discarded = memB;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
kept = memB;
|
|
252
|
-
discarded = memA;
|
|
253
|
-
}
|
|
254
|
-
const similarity = `title=${titleSim.toFixed(2)}, content=${contentOverlap.toFixed(2)}`;
|
|
255
|
-
if (!dryRun) {
|
|
256
|
-
// Append unique sentences from discarded to kept
|
|
257
|
-
const keptContent = kept.content || '';
|
|
258
|
-
const discardedContent = discarded.content || '';
|
|
259
|
-
const keptSentences = new Set(keptContent.split(/[.!?\n]+/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0));
|
|
260
|
-
const uniqueSentences = discardedContent
|
|
261
|
-
.split(/[.!?\n]+/)
|
|
262
|
-
.map(s => s.trim())
|
|
263
|
-
.filter(s => s.length > 0 && !keptSentences.has(s.toLowerCase()));
|
|
264
|
-
if (uniqueSentences.length > 0) {
|
|
265
|
-
const mergedContent = keptContent + '\n\nMerged from duplicate:\n' + uniqueSentences.join('. ') + '.';
|
|
266
|
-
db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(mergedContent, kept.id);
|
|
267
|
-
}
|
|
268
|
-
deleteMemory(discarded.id);
|
|
269
|
-
}
|
|
270
|
-
removed.add(discarded.id);
|
|
271
|
-
pairs.push({
|
|
272
|
-
kept: kept.id,
|
|
273
|
-
removed: discarded.id,
|
|
274
|
-
similarity,
|
|
275
|
-
});
|
|
268
|
+
const removedId = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB.id : pair.memoryA.id;
|
|
269
|
+
if (!dryRun) {
|
|
270
|
+
const kept = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryA : pair.memoryB;
|
|
271
|
+
const discarded = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB : pair.memoryA;
|
|
272
|
+
const keptSentences = new Set(kept.content.split(/[.!?\n]+/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0));
|
|
273
|
+
const uniqueSentences = discarded.content
|
|
274
|
+
.split(/[.!?\n]+/)
|
|
275
|
+
.map(s => s.trim())
|
|
276
|
+
.filter(s => s.length > 0 && !keptSentences.has(s.toLowerCase()));
|
|
277
|
+
if (uniqueSentences.length > 0) {
|
|
278
|
+
const mergedContent = kept.content + '\n\nMerged from duplicate:\n' + uniqueSentences.join('. ') + '.';
|
|
279
|
+
db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(mergedContent, kept.id);
|
|
276
280
|
}
|
|
281
|
+
deleteMemory(removedId);
|
|
277
282
|
}
|
|
283
|
+
removed.add(removedId);
|
|
284
|
+
pairs.push({
|
|
285
|
+
kept: pair.recommendedKeepId,
|
|
286
|
+
removed: removedId,
|
|
287
|
+
similarity: pair.similarity,
|
|
288
|
+
});
|
|
278
289
|
}
|
|
279
290
|
return { merged: pairs.length, pairs };
|
|
280
291
|
});
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ export declare function getMemoryById(id: number, source?: DefenceSource): Memor
|
|
|
42
42
|
* Update a memory
|
|
43
43
|
*/
|
|
44
44
|
export declare function updateMemory(id: number, updates: Partial<MemoryInput>): Memory | null;
|
|
45
|
+
export declare function mergeMemories(keptId: number, removedId: number, options?: {
|
|
46
|
+
reviewedBy?: string | null;
|
|
47
|
+
}): Memory | null;
|
|
45
48
|
/**
|
|
46
49
|
* Delete a memory
|
|
47
50
|
*/
|
package/dist/memory/store.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Handles storage, retrieval, and management of memories.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
|
-
import { getDatabase, isDatabaseInitialized } from '../database/init.js';
|
|
8
|
+
import { getDatabase, isDatabaseInitialized, withTransaction } from '../database/init.js';
|
|
9
9
|
import { DEFAULT_CONFIG, } from './types.js';
|
|
10
10
|
import { calculateSalience, suggestCategory, extractTags, } from './salience.js';
|
|
11
11
|
import { calculateDecayedScore, calculateReinforcementBoost, calculatePriority, } from './decay.js';
|
|
@@ -655,6 +655,103 @@ export function updateMemory(id, updates) {
|
|
|
655
655
|
}
|
|
656
656
|
return updatedMemory;
|
|
657
657
|
}
|
|
658
|
+
function uniqueSentencesFrom(source, existing) {
|
|
659
|
+
const existingSentences = new Set(existing
|
|
660
|
+
.split(/[.!?\n]+/)
|
|
661
|
+
.map((sentence) => sentence.trim().toLowerCase())
|
|
662
|
+
.filter(Boolean));
|
|
663
|
+
return source
|
|
664
|
+
.split(/[.!?\n]+/)
|
|
665
|
+
.map((sentence) => sentence.trim())
|
|
666
|
+
.filter((sentence) => sentence.length > 0 && !existingSentences.has(sentence.toLowerCase()));
|
|
667
|
+
}
|
|
668
|
+
export function mergeMemories(keptId, removedId, options) {
|
|
669
|
+
if (keptId === removedId)
|
|
670
|
+
return getMemoryById(keptId);
|
|
671
|
+
return withTransaction(() => {
|
|
672
|
+
const db = getDatabase();
|
|
673
|
+
const kept = getMemoryById(keptId);
|
|
674
|
+
const removed = getMemoryById(removedId);
|
|
675
|
+
if (!kept || !removed)
|
|
676
|
+
return null;
|
|
677
|
+
const mergedSnippets = uniqueSentencesFrom(removed.content, kept.content);
|
|
678
|
+
const mergedContent = mergedSnippets.length > 0
|
|
679
|
+
? `${kept.content}\n\nMerged from duplicate (${removed.title}):\n${mergedSnippets.join('. ')}.`
|
|
680
|
+
: kept.content;
|
|
681
|
+
const mergedTags = Array.from(new Set([...(kept.tags ?? []), ...(removed.tags ?? [])]));
|
|
682
|
+
const mergedFrom = Array.isArray(kept.metadata?.mergedFrom)
|
|
683
|
+
? [...kept.metadata.mergedFrom]
|
|
684
|
+
: [];
|
|
685
|
+
mergedFrom.push({
|
|
686
|
+
id: removed.id,
|
|
687
|
+
uuid: removed.uuid,
|
|
688
|
+
title: removed.title,
|
|
689
|
+
mergedAt: new Date().toISOString(),
|
|
690
|
+
});
|
|
691
|
+
const mergedMetadata = {
|
|
692
|
+
...kept.metadata,
|
|
693
|
+
mergedFrom,
|
|
694
|
+
mergedFromCount: mergedFrom.length,
|
|
695
|
+
};
|
|
696
|
+
const mergedStatus = kept.status === 'canonical' || removed.status === 'canonical'
|
|
697
|
+
? 'canonical'
|
|
698
|
+
: kept.status === 'active' || removed.status === 'active'
|
|
699
|
+
? 'active'
|
|
700
|
+
: kept.status;
|
|
701
|
+
const mergedPinned = kept.pinned || removed.pinned || mergedStatus === 'canonical';
|
|
702
|
+
const mergedCloudExcluded = kept.cloudExcluded || removed.cloudExcluded;
|
|
703
|
+
const mergedScope = kept.scope === 'global' || removed.scope === 'global' ? 'global' : 'project';
|
|
704
|
+
const mergedProject = kept.project ?? removed.project ?? null;
|
|
705
|
+
const mergedTransferable = kept.transferable || removed.transferable;
|
|
706
|
+
const mergedSalience = Math.max(kept.salience, removed.salience);
|
|
707
|
+
const mergedTrustScore = Math.max(kept.trustScore ?? 0, removed.trustScore ?? 0);
|
|
708
|
+
const mergedAccessCount = kept.accessCount + removed.accessCount;
|
|
709
|
+
const mergedLastAccessed = new Date(Math.max(new Date(kept.lastAccessed).getTime(), new Date(removed.lastAccessed).getTime())).toISOString();
|
|
710
|
+
const mergedReviewedBy = options?.reviewedBy ?? kept.reviewedBy ?? 'review-merge';
|
|
711
|
+
const mergedSensitivity = kept.sensitivityLevel === 'SECRET' || removed.sensitivityLevel === 'SECRET'
|
|
712
|
+
? 'SECRET'
|
|
713
|
+
: kept.sensitivityLevel === 'CONFIDENTIAL' || removed.sensitivityLevel === 'CONFIDENTIAL'
|
|
714
|
+
? 'CONFIDENTIAL'
|
|
715
|
+
: kept.sensitivityLevel ?? removed.sensitivityLevel ?? 'INTERNAL';
|
|
716
|
+
db.prepare(`
|
|
717
|
+
UPDATE memories
|
|
718
|
+
SET content = ?,
|
|
719
|
+
tags = ?,
|
|
720
|
+
salience = ?,
|
|
721
|
+
project = ?,
|
|
722
|
+
metadata = ?,
|
|
723
|
+
scope = ?,
|
|
724
|
+
transferable = ?,
|
|
725
|
+
status = ?,
|
|
726
|
+
pinned = ?,
|
|
727
|
+
reviewed_by = ?,
|
|
728
|
+
reviewed_at = ?,
|
|
729
|
+
trust_score = ?,
|
|
730
|
+
sensitivity_level = ?,
|
|
731
|
+
cloud_excluded = ?,
|
|
732
|
+
access_count = ?,
|
|
733
|
+
last_accessed = ?,
|
|
734
|
+
updated_at = CURRENT_TIMESTAMP
|
|
735
|
+
WHERE id = ?
|
|
736
|
+
`).run(mergedContent, JSON.stringify(mergedTags), mergedSalience, mergedProject, JSON.stringify(mergedMetadata), mergedScope, mergedTransferable ? 1 : 0, mergedStatus, mergedPinned ? 1 : 0, mergedReviewedBy, new Date().toISOString(), mergedTrustScore, mergedSensitivity, mergedCloudExcluded ? 1 : 0, mergedAccessCount, mergedLastAccessed, kept.id);
|
|
737
|
+
const updatedMemory = getMemoryById(kept.id);
|
|
738
|
+
try {
|
|
739
|
+
const extraction = extractFromMemory(updatedMemory.title, updatedMemory.content, updatedMemory.category);
|
|
740
|
+
replaceMemoryGraph(updatedMemory.id, extraction);
|
|
741
|
+
}
|
|
742
|
+
catch (e) {
|
|
743
|
+
console.error('[shieldcortex] Entity extraction refresh failed after merge:', e);
|
|
744
|
+
}
|
|
745
|
+
emitMemoryUpdated(updatedMemory);
|
|
746
|
+
persistEvent('memory_updated', { memory: updatedMemory, mergedFromId: removed.id });
|
|
747
|
+
if (isFeatureEnabled('cloud_sync')) {
|
|
748
|
+
syncMemoryUpsertToCloud(updatedMemory);
|
|
749
|
+
syncGraphForMemoryToCloud(updatedMemory.id);
|
|
750
|
+
}
|
|
751
|
+
deleteMemory(removed.id);
|
|
752
|
+
return updatedMemory;
|
|
753
|
+
});
|
|
754
|
+
}
|
|
658
755
|
/**
|
|
659
756
|
* Delete a memory
|
|
660
757
|
*/
|
package/dist/setup/codex.js
CHANGED
|
@@ -13,6 +13,9 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const SERVER_NAME = 'shieldcortex-memory';
|
|
15
15
|
const MCP_ENTRY = path.resolve(__dirname, '..', 'index.js');
|
|
16
|
+
function escapeRegExp(value) {
|
|
17
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
}
|
|
16
19
|
function getCodexConfigPath() {
|
|
17
20
|
return path.join(os.homedir(), '.codex', 'config.toml');
|
|
18
21
|
}
|
|
@@ -38,32 +41,66 @@ function buildServerBlock() {
|
|
|
38
41
|
'',
|
|
39
42
|
].join('\n');
|
|
40
43
|
}
|
|
44
|
+
function parseSections(content) {
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
const sections = [];
|
|
47
|
+
let current = { header: null, lines: [] };
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const headerMatch = line.match(/^\[([^\]]+)\]\s*$/);
|
|
50
|
+
if (headerMatch) {
|
|
51
|
+
sections.push(current);
|
|
52
|
+
current = { header: headerMatch[1], lines: [line] };
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
current.lines.push(line);
|
|
56
|
+
}
|
|
57
|
+
sections.push(current);
|
|
58
|
+
return sections;
|
|
59
|
+
}
|
|
60
|
+
function normalizeContent(content) {
|
|
61
|
+
const trimmed = content.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
62
|
+
return trimmed.length > 0 ? `${trimmed}\n` : '';
|
|
63
|
+
}
|
|
41
64
|
function hasServerBlock(content) {
|
|
42
|
-
return new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]
|
|
65
|
+
return new RegExp(`^\\[mcp_servers\\.${escapeRegExp(SERVER_NAME)}\\]\\r?$`, 'm').test(content);
|
|
43
66
|
}
|
|
44
67
|
function upsertServerBlock(content) {
|
|
45
|
-
const block = buildServerBlock();
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
const block = buildServerBlock().trimEnd();
|
|
69
|
+
const sections = parseSections(content);
|
|
70
|
+
const output = [];
|
|
71
|
+
let insertionIndex = -1;
|
|
72
|
+
let found = false;
|
|
73
|
+
for (const section of sections) {
|
|
74
|
+
if (section.header === `mcp_servers.${SERVER_NAME}`) {
|
|
75
|
+
if (insertionIndex === -1)
|
|
76
|
+
insertionIndex = output.length;
|
|
77
|
+
found = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const rendered = section.lines.join('\n').trimEnd();
|
|
81
|
+
if (rendered.length > 0)
|
|
82
|
+
output.push(rendered);
|
|
83
|
+
}
|
|
84
|
+
if (insertionIndex === -1)
|
|
85
|
+
insertionIndex = output.length;
|
|
86
|
+
output.splice(insertionIndex, 0, block);
|
|
87
|
+
const normalized = normalizeContent(output.join('\n\n'));
|
|
88
|
+
if (found) {
|
|
89
|
+
return { content: normalized, changed: normalized !== content };
|
|
50
90
|
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
content: trimmed.length > 0 ? `${trimmed}\n\n${block}` : block,
|
|
54
|
-
changed: true,
|
|
55
|
-
};
|
|
91
|
+
return { content: normalized, changed: normalized !== content };
|
|
56
92
|
}
|
|
57
93
|
function removeServerBlock(content) {
|
|
58
|
-
const
|
|
59
|
-
|
|
94
|
+
const sections = parseSections(content);
|
|
95
|
+
const filtered = sections.filter((section) => section.header !== `mcp_servers.${SERVER_NAME}`);
|
|
96
|
+
if (filtered.length === sections.length) {
|
|
60
97
|
return { content, changed: false };
|
|
61
98
|
}
|
|
62
|
-
const
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
return { content:
|
|
99
|
+
const normalized = normalizeContent(filtered
|
|
100
|
+
.map((section) => section.lines.join('\n').trimEnd())
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join('\n\n'));
|
|
103
|
+
return { content: normalized, changed: true };
|
|
67
104
|
}
|
|
68
105
|
export async function installCodex() {
|
|
69
106
|
if (!fs.existsSync(MCP_ENTRY)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shieldcortex",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|