shieldcortex 3.1.0 → 3.2.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 +16 -0
- 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/page_client-reference-manifest.js +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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- 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 +2 -2
- 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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
- 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 +3 -3
- 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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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/98e2c181d5c4349f.js +1 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +9 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/a56c497e02afd4ba.css +3 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/a90355d73183a5e6.js +1 -0
- package/dist/api/routes/memories.js +366 -1
- package/dist/api/routes/recall.js +53 -0
- package/dist/cloud/graph-sync.js +6 -3
- package/dist/cloud/memory-sync.d.ts +1 -0
- package/dist/cloud/memory-sync.js +3 -0
- package/dist/database/init.js +29 -0
- package/dist/memory/search.d.ts +1 -0
- package/dist/memory/search.js +4 -0
- package/dist/memory/store.js +146 -28
- package/dist/memory/types.d.ts +31 -0
- package/dist/tools/context.d.ts +4 -4
- package/dist/tools/forget.d.ts +4 -4
- package/dist/tools/recall.d.ts +8 -8
- package/dist/tools/remember.d.ts +19 -4
- package/dist/tools/remember.js +17 -1
- package/hooks/openclaw/cortex-memory/handler.ts +8 -0
- package/package.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/0a69eb25d08447ee.js +0 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/3cc7e8d4f73cf5d2.js +0 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/97537d3db46c8467.css +0 -3
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/aa6e9b8a52353969.js +0 -9
- /package/dashboard/.next/standalone/dashboard/.next/static/{RnvqrTXo_jN8SuMdaNcIj → ctp9eCBcHDpTWtUYMwJK7}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{RnvqrTXo_jN8SuMdaNcIj → ctp9eCBcHDpTWtUYMwJK7}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{RnvqrTXo_jN8SuMdaNcIj → ctp9eCBcHDpTWtUYMwJK7}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
1
4
|
import { getDatabase } from '../../database/init.js';
|
|
2
5
|
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
|
|
3
6
|
import { calculateDecayedScore } from '../../memory/decay.js';
|
|
@@ -6,6 +9,185 @@ import { getActivationStats, getActiveMemories } from '../../memory/activation.j
|
|
|
6
9
|
import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
|
|
7
10
|
import { emitConsolidation } from '../events.js';
|
|
8
11
|
export function registerMemoryRoutes(app, requireNotLocked) {
|
|
12
|
+
app.get('/api/capture/openclaw/sessions', requireNotLocked, (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
15
|
+
const auditDir = join(homedir(), '.shieldcortex', 'audit');
|
|
16
|
+
const db = getDatabase();
|
|
17
|
+
const rows = db.prepare(`
|
|
18
|
+
SELECT * FROM memories
|
|
19
|
+
WHERE source_kind IN ('hook', 'plugin') OR source LIKE 'hook:openclaw%' OR source LIKE 'agent:openclaw-plugin%'
|
|
20
|
+
ORDER BY updated_at DESC
|
|
21
|
+
LIMIT 500
|
|
22
|
+
`).all();
|
|
23
|
+
const openClawMemories = rows.map(rowToMemory);
|
|
24
|
+
const sessionMap = new Map();
|
|
25
|
+
const getSession = (sessionId) => {
|
|
26
|
+
let session = sessionMap.get(sessionId);
|
|
27
|
+
if (!session) {
|
|
28
|
+
session = {
|
|
29
|
+
sessionId,
|
|
30
|
+
firstSeenAt: new Date().toISOString(),
|
|
31
|
+
lastSeenAt: new Date(0).toISOString(),
|
|
32
|
+
storedMemoryCount: 0,
|
|
33
|
+
loggedSaved: 0,
|
|
34
|
+
skipped: 0,
|
|
35
|
+
threats: 0,
|
|
36
|
+
blocked: 0,
|
|
37
|
+
quarantined: 0,
|
|
38
|
+
autoExtracted: 0,
|
|
39
|
+
keywordTriggered: 0,
|
|
40
|
+
pinned: 0,
|
|
41
|
+
suppressed: 0,
|
|
42
|
+
hooks: new Set(),
|
|
43
|
+
models: new Set(),
|
|
44
|
+
agentIds: new Set(),
|
|
45
|
+
memoryIds: new Set(),
|
|
46
|
+
previews: [],
|
|
47
|
+
events: [],
|
|
48
|
+
};
|
|
49
|
+
sessionMap.set(sessionId, session);
|
|
50
|
+
}
|
|
51
|
+
return session;
|
|
52
|
+
};
|
|
53
|
+
for (const memory of openClawMemories) {
|
|
54
|
+
const sessionId = typeof memory.metadata?.sessionId === 'string'
|
|
55
|
+
? memory.metadata.sessionId
|
|
56
|
+
: memory.source?.startsWith('agent:openclaw-plugin:')
|
|
57
|
+
? memory.source.slice('agent:openclaw-plugin:'.length)
|
|
58
|
+
: null;
|
|
59
|
+
if (!sessionId)
|
|
60
|
+
continue;
|
|
61
|
+
const session = getSession(sessionId);
|
|
62
|
+
const createdAt = memory.createdAt.toISOString();
|
|
63
|
+
if (createdAt < session.firstSeenAt)
|
|
64
|
+
session.firstSeenAt = createdAt;
|
|
65
|
+
if (createdAt > session.lastSeenAt)
|
|
66
|
+
session.lastSeenAt = createdAt;
|
|
67
|
+
session.storedMemoryCount += 1;
|
|
68
|
+
session.memoryIds.add(memory.id);
|
|
69
|
+
if (typeof memory.metadata?.agentId === 'string')
|
|
70
|
+
session.agentIds.add(memory.metadata.agentId);
|
|
71
|
+
if (memory.captureMethod === 'auto' || memory.captureMethod === 'plugin' || memory.captureMethod === 'hook') {
|
|
72
|
+
session.autoExtracted += 1;
|
|
73
|
+
}
|
|
74
|
+
if (typeof memory.metadata?.trigger === 'string' || memory.tags.includes('keyword_trigger')) {
|
|
75
|
+
session.keywordTriggered += 1;
|
|
76
|
+
}
|
|
77
|
+
if (memory.pinned)
|
|
78
|
+
session.pinned += 1;
|
|
79
|
+
if (memory.status === 'suppressed')
|
|
80
|
+
session.suppressed += 1;
|
|
81
|
+
}
|
|
82
|
+
if (existsSync(auditDir)) {
|
|
83
|
+
const files = readdirSync(auditDir)
|
|
84
|
+
.filter((file) => /^realtime-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file))
|
|
85
|
+
.sort()
|
|
86
|
+
.slice(-14);
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const raw = readFileSync(join(auditDir, file), 'utf-8');
|
|
89
|
+
for (const line of raw.split('\n')) {
|
|
90
|
+
if (!line.trim())
|
|
91
|
+
continue;
|
|
92
|
+
try {
|
|
93
|
+
const entry = JSON.parse(line);
|
|
94
|
+
const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId : null;
|
|
95
|
+
if (!sessionId)
|
|
96
|
+
continue;
|
|
97
|
+
const session = getSession(sessionId);
|
|
98
|
+
const ts = typeof entry.ts === 'string' ? entry.ts : new Date().toISOString();
|
|
99
|
+
if (ts < session.firstSeenAt)
|
|
100
|
+
session.firstSeenAt = ts;
|
|
101
|
+
if (ts > session.lastSeenAt)
|
|
102
|
+
session.lastSeenAt = ts;
|
|
103
|
+
if (typeof entry.hook === 'string')
|
|
104
|
+
session.hooks.add(entry.hook);
|
|
105
|
+
if (typeof entry.model === 'string')
|
|
106
|
+
session.models.add(entry.model);
|
|
107
|
+
if (entry.type === 'memory') {
|
|
108
|
+
const count = Number(entry.count ?? 0);
|
|
109
|
+
const skipped = Number(entry.skipped ?? 0);
|
|
110
|
+
session.loggedSaved += count;
|
|
111
|
+
session.skipped += skipped;
|
|
112
|
+
session.events.push({
|
|
113
|
+
ts,
|
|
114
|
+
type: 'memory',
|
|
115
|
+
hook: typeof entry.hook === 'string' ? entry.hook : undefined,
|
|
116
|
+
model: typeof entry.model === 'string' ? entry.model : undefined,
|
|
117
|
+
preview: typeof entry.preview === 'string' ? entry.preview : undefined,
|
|
118
|
+
count,
|
|
119
|
+
skipped,
|
|
120
|
+
});
|
|
121
|
+
if (typeof entry.preview === 'string' && session.previews.length < 6) {
|
|
122
|
+
session.previews.push(entry.preview);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (entry.type === 'threat') {
|
|
126
|
+
session.threats += 1;
|
|
127
|
+
const preview = typeof entry.preview === 'string' ? entry.preview : undefined;
|
|
128
|
+
const lower = preview?.toLowerCase() ?? '';
|
|
129
|
+
if (lower.includes('quarantine'))
|
|
130
|
+
session.quarantined += 1;
|
|
131
|
+
if (lower.includes('block'))
|
|
132
|
+
session.blocked += 1;
|
|
133
|
+
session.events.push({
|
|
134
|
+
ts,
|
|
135
|
+
type: lower.includes('quarantine') ? 'quarantine' : lower.includes('block') ? 'blocked' : 'threat',
|
|
136
|
+
hook: typeof entry.hook === 'string' ? entry.hook : undefined,
|
|
137
|
+
model: typeof entry.model === 'string' ? entry.model : undefined,
|
|
138
|
+
preview,
|
|
139
|
+
});
|
|
140
|
+
if (preview && session.previews.length < 6) {
|
|
141
|
+
session.previews.push(preview);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore malformed lines
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const sessions = Array.from(sessionMap.values())
|
|
152
|
+
.map((session) => ({
|
|
153
|
+
sessionId: session.sessionId,
|
|
154
|
+
firstSeenAt: session.firstSeenAt,
|
|
155
|
+
lastSeenAt: session.lastSeenAt,
|
|
156
|
+
saved: Math.max(session.storedMemoryCount, session.loggedSaved),
|
|
157
|
+
skipped: session.skipped,
|
|
158
|
+
threats: session.threats,
|
|
159
|
+
blocked: session.blocked,
|
|
160
|
+
quarantined: session.quarantined,
|
|
161
|
+
autoExtracted: session.autoExtracted,
|
|
162
|
+
keywordTriggered: session.keywordTriggered,
|
|
163
|
+
pinned: session.pinned,
|
|
164
|
+
suppressed: session.suppressed,
|
|
165
|
+
hooks: Array.from(session.hooks),
|
|
166
|
+
models: Array.from(session.models),
|
|
167
|
+
agentIds: Array.from(session.agentIds),
|
|
168
|
+
previews: session.previews,
|
|
169
|
+
events: session.events
|
|
170
|
+
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
|
|
171
|
+
.slice(0, 24),
|
|
172
|
+
memories: openClawMemories
|
|
173
|
+
.filter((memory) => session.memoryIds.has(memory.id))
|
|
174
|
+
.sort((a, b) => new Date(b.updatedAt ?? b.createdAt).getTime() - new Date(a.updatedAt ?? a.createdAt).getTime())
|
|
175
|
+
.slice(0, 12),
|
|
176
|
+
}))
|
|
177
|
+
.sort((a, b) => new Date(b.lastSeenAt).getTime() - new Date(a.lastSeenAt).getTime())
|
|
178
|
+
.slice(0, limit);
|
|
179
|
+
const summary = {
|
|
180
|
+
sessions: sessions.length,
|
|
181
|
+
saved: sessions.reduce((sum, session) => sum + session.saved, 0),
|
|
182
|
+
skipped: sessions.reduce((sum, session) => sum + session.skipped, 0),
|
|
183
|
+
threats: sessions.reduce((sum, session) => sum + session.threats, 0),
|
|
184
|
+
};
|
|
185
|
+
res.json({ summary, sessions });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
res.status(500).json({ error: error.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
9
191
|
app.get('/api/memories', requireNotLocked, async (req, res) => {
|
|
10
192
|
try {
|
|
11
193
|
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
@@ -110,11 +292,127 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
110
292
|
JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
|
|
111
293
|
${project ? 'WHERE m1.project = ?' : ''}
|
|
112
294
|
LIMIT 50
|
|
295
|
+
`).all(...params);
|
|
296
|
+
const lowTrust = db.prepare(`
|
|
297
|
+
SELECT id, title, category, project, trust_score, source_kind, capture_method
|
|
298
|
+
FROM memories WHERE trust_score < 0.7 ${projectFilter}
|
|
299
|
+
ORDER BY trust_score ASC, updated_at DESC LIMIT 50
|
|
300
|
+
`).all(...params);
|
|
301
|
+
const noisyAutoExtracted = db.prepare(`
|
|
302
|
+
SELECT id, title, category, project, source_kind, capture_method, tags, trust_score
|
|
303
|
+
FROM memories
|
|
304
|
+
WHERE (capture_method = 'auto' OR tags LIKE '%auto-extracted%') ${projectFilter}
|
|
305
|
+
ORDER BY updated_at DESC LIMIT 50
|
|
306
|
+
`).all(...params);
|
|
307
|
+
const projectless = db.prepare(`
|
|
308
|
+
SELECT id, title, category, scope, source_kind, capture_method
|
|
309
|
+
FROM memories
|
|
310
|
+
WHERE (project IS NULL OR project = '') AND scope != 'global'
|
|
311
|
+
ORDER BY updated_at DESC LIMIT 50
|
|
312
|
+
`).all();
|
|
313
|
+
const statusCounts = db.prepare(`
|
|
314
|
+
SELECT status, COUNT(*) as count
|
|
315
|
+
FROM memories
|
|
316
|
+
${project ? 'WHERE project = ?' : ''}
|
|
317
|
+
GROUP BY status
|
|
113
318
|
`).all(...params);
|
|
114
319
|
res.json({
|
|
115
320
|
neverAccessed: { count: neverAccessed.length, items: neverAccessed },
|
|
116
321
|
stale: { count: stale.length, items: stale },
|
|
117
322
|
duplicates: { count: duplicates.length, items: duplicates },
|
|
323
|
+
lowTrust: { count: lowTrust.length, items: lowTrust },
|
|
324
|
+
noisyAutoExtracted: { count: noisyAutoExtracted.length, items: noisyAutoExtracted },
|
|
325
|
+
projectless: { count: projectless.length, items: projectless },
|
|
326
|
+
status: statusCounts.reduce((acc, row) => {
|
|
327
|
+
acc[row.status] = row.count;
|
|
328
|
+
return acc;
|
|
329
|
+
}, {}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
res.status(500).json({ error: error.message });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
app.get('/api/review/queue', requireNotLocked, (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
339
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
340
|
+
const db = getDatabase();
|
|
341
|
+
const projectFilter = project ? 'AND project = ?' : '';
|
|
342
|
+
const params = project ? [project] : [];
|
|
343
|
+
const stale = db.prepare(`
|
|
344
|
+
SELECT * FROM memories
|
|
345
|
+
WHERE decayed_score < 0.3 ${projectFilter}
|
|
346
|
+
AND last_accessed < datetime('now', '-30 days')
|
|
347
|
+
ORDER BY decayed_score ASC LIMIT ?
|
|
348
|
+
`).all(...params, limit);
|
|
349
|
+
const neverUsed = db.prepare(`
|
|
350
|
+
SELECT * FROM memories
|
|
351
|
+
WHERE access_count = 0 ${projectFilter}
|
|
352
|
+
AND created_at < datetime('now', '-1 day')
|
|
353
|
+
ORDER BY created_at DESC LIMIT ?
|
|
354
|
+
`).all(...params, limit);
|
|
355
|
+
const lowTrust = db.prepare(`
|
|
356
|
+
SELECT * FROM memories
|
|
357
|
+
WHERE trust_score < 0.7 ${projectFilter}
|
|
358
|
+
ORDER BY trust_score ASC, updated_at DESC LIMIT ?
|
|
359
|
+
`).all(...params, limit);
|
|
360
|
+
const noisyAutoExtracted = db.prepare(`
|
|
361
|
+
SELECT * FROM memories
|
|
362
|
+
WHERE (capture_method = 'auto' OR tags LIKE '%auto-extracted%') ${projectFilter}
|
|
363
|
+
ORDER BY updated_at DESC LIMIT ?
|
|
364
|
+
`).all(...params, limit);
|
|
365
|
+
const projectless = db.prepare(`
|
|
366
|
+
SELECT * FROM memories
|
|
367
|
+
WHERE (project IS NULL OR project = '') AND scope != 'global'
|
|
368
|
+
ORDER BY updated_at DESC LIMIT ?
|
|
369
|
+
`).all(limit);
|
|
370
|
+
const openClawSummary = db.prepare(`
|
|
371
|
+
SELECT
|
|
372
|
+
COUNT(*) as total,
|
|
373
|
+
SUM(CASE WHEN capture_method = 'auto' THEN 1 ELSE 0 END) as auto_count,
|
|
374
|
+
SUM(CASE WHEN tags LIKE '%keyword-trigger%' THEN 1 ELSE 0 END) as keyword_count,
|
|
375
|
+
SUM(CASE WHEN status = 'suppressed' THEN 1 ELSE 0 END) as suppressed_count,
|
|
376
|
+
SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned_count
|
|
377
|
+
FROM memories
|
|
378
|
+
WHERE (source_kind IN ('hook', 'plugin') OR tags LIKE '%openclaw%')
|
|
379
|
+
${project ? 'AND project = ?' : ''}
|
|
380
|
+
`).get(...params);
|
|
381
|
+
const contradictions = detectContradictions({
|
|
382
|
+
project,
|
|
383
|
+
minScore: 0.4,
|
|
384
|
+
limit,
|
|
385
|
+
});
|
|
386
|
+
res.json({
|
|
387
|
+
summary: {
|
|
388
|
+
stale: stale.length,
|
|
389
|
+
neverUsed: neverUsed.length,
|
|
390
|
+
lowTrust: lowTrust.length,
|
|
391
|
+
noisyAutoExtracted: noisyAutoExtracted.length,
|
|
392
|
+
projectless: projectless.length,
|
|
393
|
+
contradictions: contradictions.length,
|
|
394
|
+
},
|
|
395
|
+
openClaw: {
|
|
396
|
+
total: openClawSummary.total ?? 0,
|
|
397
|
+
autoExtracted: openClawSummary.auto_count ?? 0,
|
|
398
|
+
keywordTriggered: openClawSummary.keyword_count ?? 0,
|
|
399
|
+
suppressed: openClawSummary.suppressed_count ?? 0,
|
|
400
|
+
pinned: openClawSummary.pinned_count ?? 0,
|
|
401
|
+
},
|
|
402
|
+
sections: {
|
|
403
|
+
stale: stale.map(rowToMemory),
|
|
404
|
+
neverUsed: neverUsed.map(rowToMemory),
|
|
405
|
+
lowTrust: lowTrust.map(rowToMemory),
|
|
406
|
+
noisyAutoExtracted: noisyAutoExtracted.map(rowToMemory),
|
|
407
|
+
projectless: projectless.map(rowToMemory),
|
|
408
|
+
contradictions: contradictions.map((item) => ({
|
|
409
|
+
memoryA: item.memoryA,
|
|
410
|
+
memoryB: item.memoryB,
|
|
411
|
+
score: item.score,
|
|
412
|
+
reason: item.reason,
|
|
413
|
+
sharedTopics: item.sharedTopics,
|
|
414
|
+
})),
|
|
415
|
+
},
|
|
118
416
|
});
|
|
119
417
|
}
|
|
120
418
|
catch (error) {
|
|
@@ -585,7 +883,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
585
883
|
app.patch('/api/memories/:id', requireNotLocked, (req, res) => {
|
|
586
884
|
try {
|
|
587
885
|
const id = parseInt(req.params.id, 10);
|
|
588
|
-
const { title, content, category, tags, importance } = req.body;
|
|
886
|
+
const { title, content, category, tags, importance, status, pinned, reviewedBy, cloudExcluded, scope, project } = req.body;
|
|
589
887
|
if (title !== undefined && (typeof title !== 'string' || title.trim().length === 0)) {
|
|
590
888
|
return res.status(400).json({ error: 'Title must be a non-empty string' });
|
|
591
889
|
}
|
|
@@ -602,6 +900,24 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
602
900
|
if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
|
|
603
901
|
return res.status(400).json({ error: 'Importance must be a number between 0 and 1' });
|
|
604
902
|
}
|
|
903
|
+
if (status !== undefined && !['active', 'archived', 'suppressed', 'canonical'].includes(status)) {
|
|
904
|
+
return res.status(400).json({ error: 'Invalid review status' });
|
|
905
|
+
}
|
|
906
|
+
if (pinned !== undefined && typeof pinned !== 'boolean') {
|
|
907
|
+
return res.status(400).json({ error: 'Pinned must be boolean' });
|
|
908
|
+
}
|
|
909
|
+
if (cloudExcluded !== undefined && typeof cloudExcluded !== 'boolean') {
|
|
910
|
+
return res.status(400).json({ error: 'cloudExcluded must be boolean' });
|
|
911
|
+
}
|
|
912
|
+
if (reviewedBy !== undefined && reviewedBy !== null && typeof reviewedBy !== 'string') {
|
|
913
|
+
return res.status(400).json({ error: 'reviewedBy must be string or null' });
|
|
914
|
+
}
|
|
915
|
+
if (scope !== undefined && !['project', 'global'].includes(scope)) {
|
|
916
|
+
return res.status(400).json({ error: 'scope must be project or global' });
|
|
917
|
+
}
|
|
918
|
+
if (project !== undefined && project !== null && typeof project !== 'string') {
|
|
919
|
+
return res.status(400).json({ error: 'project must be string or null' });
|
|
920
|
+
}
|
|
605
921
|
const updates = {};
|
|
606
922
|
if (title !== undefined)
|
|
607
923
|
updates.title = title.trim();
|
|
@@ -613,6 +929,55 @@ export function registerMemoryRoutes(app, requireNotLocked) {
|
|
|
613
929
|
updates.tags = tags;
|
|
614
930
|
if (importance !== undefined)
|
|
615
931
|
updates.salience = importance;
|
|
932
|
+
if (status !== undefined)
|
|
933
|
+
updates.status = status;
|
|
934
|
+
if (pinned !== undefined)
|
|
935
|
+
updates.pinned = pinned;
|
|
936
|
+
if (reviewedBy !== undefined)
|
|
937
|
+
updates.reviewedBy = reviewedBy;
|
|
938
|
+
if (cloudExcluded !== undefined)
|
|
939
|
+
updates.cloudExcluded = cloudExcluded;
|
|
940
|
+
if (scope !== undefined)
|
|
941
|
+
updates.scope = scope;
|
|
942
|
+
if (project !== undefined)
|
|
943
|
+
updates.project = project;
|
|
944
|
+
const updated = updateMemory(id, updates);
|
|
945
|
+
if (!updated) {
|
|
946
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
947
|
+
}
|
|
948
|
+
res.json(updated);
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
res.status(500).json({ error: error.message });
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
app.patch('/api/memories/:id/review', requireNotLocked, (req, res) => {
|
|
955
|
+
try {
|
|
956
|
+
const id = parseInt(req.params.id, 10);
|
|
957
|
+
const { action, reviewedBy, project, scope } = req.body;
|
|
958
|
+
if (Number.isNaN(id)) {
|
|
959
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
960
|
+
}
|
|
961
|
+
const reviewActor = typeof reviewedBy === 'string' && reviewedBy.trim() ? reviewedBy.trim() : 'dashboard';
|
|
962
|
+
const actionMap = {
|
|
963
|
+
archive: { status: 'archived', reviewedBy: reviewActor },
|
|
964
|
+
suppress: { status: 'suppressed', reviewedBy: reviewActor },
|
|
965
|
+
restore: { status: 'active', reviewedBy: reviewActor },
|
|
966
|
+
pin: { pinned: true, reviewedBy: reviewActor },
|
|
967
|
+
unpin: { pinned: false, reviewedBy: reviewActor },
|
|
968
|
+
canonicalize: { status: 'canonical', pinned: true, reviewedBy: reviewActor },
|
|
969
|
+
excludeCloud: { cloudExcluded: true, reviewedBy: reviewActor },
|
|
970
|
+
includeCloud: { cloudExcluded: false, reviewedBy: reviewActor },
|
|
971
|
+
rescopeProject: { scope: 'project', project: project ?? null, reviewedBy: reviewActor },
|
|
972
|
+
rescopeGlobal: { scope: 'global', project: null, reviewedBy: reviewActor },
|
|
973
|
+
};
|
|
974
|
+
if (!action || !actionMap[action]) {
|
|
975
|
+
return res.status(400).json({ error: 'Unsupported review action' });
|
|
976
|
+
}
|
|
977
|
+
const updates = {
|
|
978
|
+
...actionMap[action],
|
|
979
|
+
...(scope ? { scope } : {}),
|
|
980
|
+
};
|
|
616
981
|
const updated = updateMemory(id, updates);
|
|
617
982
|
if (!updated) {
|
|
618
983
|
return res.status(404).json({ error: 'Memory not found' });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { searchMemoriesExplained } from '../../memory/store.js';
|
|
2
|
+
import { getMemoryById, getRecentMemories } from '../../memory/store.js';
|
|
2
3
|
export function registerRecallRoutes(app, requireNotLocked) {
|
|
3
4
|
app.get('/api/recall/explain', requireNotLocked, async (req, res) => {
|
|
4
5
|
try {
|
|
@@ -12,6 +13,9 @@ export function registerRecallRoutes(app, requireNotLocked) {
|
|
|
12
13
|
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
|
|
13
14
|
const includeDecayed = req.query.includeDecayed === 'true';
|
|
14
15
|
const includeGlobal = req.query.includeGlobal !== 'false';
|
|
16
|
+
const includeArchived = req.query.includeArchived === 'true';
|
|
17
|
+
const includeSuppressed = req.query.includeSuppressed === 'true';
|
|
18
|
+
const expectedId = typeof req.query.expectedId === 'string' ? parseInt(req.query.expectedId, 10) : null;
|
|
15
19
|
const results = await searchMemoriesExplained({
|
|
16
20
|
query,
|
|
17
21
|
project,
|
|
@@ -20,13 +24,62 @@ export function registerRecallRoutes(app, requireNotLocked) {
|
|
|
20
24
|
limit,
|
|
21
25
|
includeDecayed,
|
|
22
26
|
includeGlobal,
|
|
27
|
+
includeArchived,
|
|
28
|
+
includeSuppressed,
|
|
23
29
|
});
|
|
30
|
+
let expectedMemory = null;
|
|
31
|
+
if (expectedId && Number.isFinite(expectedId)) {
|
|
32
|
+
const memory = getMemoryById(expectedId);
|
|
33
|
+
if (memory) {
|
|
34
|
+
const foundIndex = results.findIndex((result) => result.memory.id === expectedId);
|
|
35
|
+
expectedMemory = {
|
|
36
|
+
id: memory.id,
|
|
37
|
+
title: memory.title,
|
|
38
|
+
status: memory.status,
|
|
39
|
+
pinned: memory.pinned,
|
|
40
|
+
cloudExcluded: memory.cloudExcluded,
|
|
41
|
+
trustScore: memory.trustScore,
|
|
42
|
+
captureMethod: memory.captureMethod,
|
|
43
|
+
sourceKind: memory.sourceKind,
|
|
44
|
+
rank: foundIndex >= 0 ? foundIndex + 1 : null,
|
|
45
|
+
eligible: memory.status !== 'archived' && memory.status !== 'suppressed',
|
|
46
|
+
reasons: [
|
|
47
|
+
...(memory.status === 'archived' ? ['Archived memories are excluded from normal recall'] : []),
|
|
48
|
+
...(memory.status === 'suppressed' ? ['Suppressed memories are excluded from normal recall'] : []),
|
|
49
|
+
...(memory.trustScore < 0.7 ? [`Low trust source (${memory.trustScore.toFixed(2)})`] : []),
|
|
50
|
+
...(memory.cloudExcluded ? ['Excluded from cloud sync'] : []),
|
|
51
|
+
...(foundIndex === -1 ? ['Did not rank in the current result window'] : []),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const misses = getRecentMemories(200, project).filter((memory) => {
|
|
57
|
+
if (results.some((result) => result.memory.id === memory.id))
|
|
58
|
+
return false;
|
|
59
|
+
if (memory.status === 'archived' || memory.status === 'suppressed')
|
|
60
|
+
return false;
|
|
61
|
+
return memory.salience >= 0.65 || memory.pinned;
|
|
62
|
+
}).slice(0, 5).map((memory) => ({
|
|
63
|
+
id: memory.id,
|
|
64
|
+
title: memory.title,
|
|
65
|
+
status: memory.status,
|
|
66
|
+
salience: memory.salience,
|
|
67
|
+
captureMethod: memory.captureMethod,
|
|
68
|
+
sourceKind: memory.sourceKind,
|
|
69
|
+
whyNotRecalled: [
|
|
70
|
+
'Lower relevance for this query than the returned set',
|
|
71
|
+
...(memory.pinned ? ['Pinned memories are still query-sensitive, not guaranteed results'] : []),
|
|
72
|
+
...(memory.trustScore < 0.7 ? [`Low trust source (${memory.trustScore.toFixed(2)})`] : []),
|
|
73
|
+
],
|
|
74
|
+
}));
|
|
24
75
|
res.json({
|
|
25
76
|
query,
|
|
26
77
|
project: project ?? null,
|
|
27
78
|
total: results.length,
|
|
28
79
|
sideEffects: 'disabled',
|
|
29
80
|
results,
|
|
81
|
+
expectedMemory,
|
|
82
|
+
misses,
|
|
30
83
|
});
|
|
31
84
|
}
|
|
32
85
|
catch (error) {
|
package/dist/cloud/graph-sync.js
CHANGED
|
@@ -19,13 +19,15 @@ function tripleExternalId(id) {
|
|
|
19
19
|
}
|
|
20
20
|
function getAllowedMemoryExternalIds() {
|
|
21
21
|
const db = getDatabase();
|
|
22
|
-
const rows = db.prepare('SELECT uuid, project, sensitivity_level FROM memories').all();
|
|
22
|
+
const rows = db.prepare('SELECT uuid, project, sensitivity_level, cloud_excluded FROM memories').all();
|
|
23
23
|
return buildAllowedMemoryExternalIdSet(rows);
|
|
24
24
|
}
|
|
25
25
|
function buildAllowedMemoryExternalIdSet(rows) {
|
|
26
26
|
const controls = getCloudSyncControls();
|
|
27
27
|
return new Set(rows
|
|
28
28
|
.filter((row) => {
|
|
29
|
+
if (row.cloud_excluded)
|
|
30
|
+
return false;
|
|
29
31
|
if (!shouldSyncProject(row.project, controls))
|
|
30
32
|
return false;
|
|
31
33
|
if (controls.excludeSensitive && isSensitiveLevel(row.sensitivity_level))
|
|
@@ -114,11 +116,12 @@ function mapMemoryEntityRow(row) {
|
|
|
114
116
|
}
|
|
115
117
|
function getMemoryScopedEnvelope(memoryId) {
|
|
116
118
|
const db = getDatabase();
|
|
117
|
-
const memory = db.prepare('SELECT id, uuid, project, sensitivity_level FROM memories WHERE id = ?').get(memoryId);
|
|
119
|
+
const memory = db.prepare('SELECT id, uuid, project, sensitivity_level, cloud_excluded FROM memories WHERE id = ?').get(memoryId);
|
|
118
120
|
if (!memory)
|
|
119
121
|
return null;
|
|
120
122
|
const controls = getCloudSyncControls();
|
|
121
|
-
const memoryAllowed =
|
|
123
|
+
const memoryAllowed = !memory.cloud_excluded &&
|
|
124
|
+
shouldSyncProject(memory.project, controls) &&
|
|
122
125
|
!(controls.excludeSensitive && isSensitiveLevel(memory.sensitivity_level));
|
|
123
126
|
if (!memoryAllowed) {
|
|
124
127
|
return buildEnvelope([], [], [], [memory.uuid]);
|
|
@@ -28,6 +28,7 @@ function rowToSyncRecord(row) {
|
|
|
28
28
|
sensitivity_level: row.sensitivity_level ?? null,
|
|
29
29
|
source: row.source ?? null,
|
|
30
30
|
metadata: safeJsonParse(row.metadata, {}),
|
|
31
|
+
cloud_excluded: Boolean(row.cloud_excluded),
|
|
31
32
|
created_at: new Date(row.created_at ?? Date.now()).toISOString(),
|
|
32
33
|
updated_at: new Date(row.updated_at ?? row.created_at ?? Date.now()).toISOString(),
|
|
33
34
|
deleted_at: null,
|
|
@@ -44,6 +45,8 @@ function buildEnvelope(records) {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
function shouldSyncRecord(record) {
|
|
48
|
+
if (record.cloud_excluded)
|
|
49
|
+
return false;
|
|
47
50
|
const controls = getCloudSyncControls();
|
|
48
51
|
if (!shouldSyncProject(record.project, controls))
|
|
49
52
|
return false;
|
package/dist/database/init.js
CHANGED
|
@@ -261,6 +261,27 @@ function runMigrations(database) {
|
|
|
261
261
|
if (!columnNames.has('source')) {
|
|
262
262
|
database.exec("ALTER TABLE memories ADD COLUMN source TEXT DEFAULT 'user:direct'");
|
|
263
263
|
}
|
|
264
|
+
if (!columnNames.has('status')) {
|
|
265
|
+
database.exec("ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'");
|
|
266
|
+
}
|
|
267
|
+
if (!columnNames.has('pinned')) {
|
|
268
|
+
database.exec('ALTER TABLE memories ADD COLUMN pinned INTEGER DEFAULT 0');
|
|
269
|
+
}
|
|
270
|
+
if (!columnNames.has('reviewed_at')) {
|
|
271
|
+
database.exec('ALTER TABLE memories ADD COLUMN reviewed_at TIMESTAMP');
|
|
272
|
+
}
|
|
273
|
+
if (!columnNames.has('reviewed_by')) {
|
|
274
|
+
database.exec('ALTER TABLE memories ADD COLUMN reviewed_by TEXT');
|
|
275
|
+
}
|
|
276
|
+
if (!columnNames.has('source_kind')) {
|
|
277
|
+
database.exec("ALTER TABLE memories ADD COLUMN source_kind TEXT DEFAULT 'user'");
|
|
278
|
+
}
|
|
279
|
+
if (!columnNames.has('capture_method')) {
|
|
280
|
+
database.exec("ALTER TABLE memories ADD COLUMN capture_method TEXT DEFAULT 'manual'");
|
|
281
|
+
}
|
|
282
|
+
if (!columnNames.has('cloud_excluded')) {
|
|
283
|
+
database.exec('ALTER TABLE memories ADD COLUMN cloud_excluded INTEGER DEFAULT 0');
|
|
284
|
+
}
|
|
264
285
|
if (!columnNames.has('uuid')) {
|
|
265
286
|
database.exec("ALTER TABLE memories ADD COLUMN uuid TEXT");
|
|
266
287
|
}
|
|
@@ -274,8 +295,16 @@ function runMigrations(database) {
|
|
|
274
295
|
setUuid.run(randomUUID(), row.id);
|
|
275
296
|
}
|
|
276
297
|
database.exec('UPDATE memories SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL');
|
|
298
|
+
database.exec("UPDATE memories SET status = COALESCE(status, 'active') WHERE status IS NULL OR status = ''");
|
|
299
|
+
database.exec('UPDATE memories SET pinned = COALESCE(pinned, 0) WHERE pinned IS NULL');
|
|
300
|
+
database.exec("UPDATE memories SET source_kind = CASE WHEN source LIKE 'hook:%' THEN 'hook' WHEN source LIKE 'api:%' THEN 'api' WHEN source LIKE 'agent:%' THEN 'agent' WHEN source LIKE 'cli:%' THEN 'cli' ELSE COALESCE(source_kind, 'user') END WHERE source_kind IS NULL OR source_kind = ''");
|
|
301
|
+
database.exec("UPDATE memories SET capture_method = CASE WHEN tags LIKE '%auto-extracted%' THEN 'auto' WHEN source_kind = 'hook' THEN 'hook' WHEN source_kind = 'api' THEN 'api' WHEN source_kind = 'agent' THEN 'plugin' WHEN source_kind = 'cli' THEN 'manual' ELSE COALESCE(capture_method, 'manual') END WHERE capture_method IS NULL OR capture_method = ''");
|
|
302
|
+
database.exec('UPDATE memories SET cloud_excluded = COALESCE(cloud_excluded, 0) WHERE cloud_excluded IS NULL');
|
|
277
303
|
database.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_uuid ON memories(uuid)');
|
|
278
304
|
database.exec('CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC)');
|
|
305
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)');
|
|
306
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(pinned DESC)');
|
|
307
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_memories_source_kind ON memories(source_kind)');
|
|
279
308
|
}
|
|
280
309
|
catch {
|
|
281
310
|
// Safe to ignore on partially migrated databases
|
package/dist/memory/search.d.ts
CHANGED
package/dist/memory/search.js
CHANGED
|
@@ -118,6 +118,9 @@ export function buildSearchExplanation(memory, context, values) {
|
|
|
118
118
|
if (values.activationBoost > 0) {
|
|
119
119
|
reasons.push('Activated by recent recall activity');
|
|
120
120
|
}
|
|
121
|
+
if (values.contradictionPenalty > 0) {
|
|
122
|
+
reasons.push('Contradiction risk reduced its rank');
|
|
123
|
+
}
|
|
121
124
|
if (reasons.length === 0) {
|
|
122
125
|
reasons.push('Ranked by salience and base recall heuristics');
|
|
123
126
|
}
|
|
@@ -135,6 +138,7 @@ export function buildSearchExplanation(memory, context, values) {
|
|
|
135
138
|
linkBoost: values.linkBoost,
|
|
136
139
|
tagBoost: values.tagBoost,
|
|
137
140
|
activationBoost: values.activationBoost,
|
|
141
|
+
contradictionPenalty: values.contradictionPenalty,
|
|
138
142
|
finalScore: values.finalScore,
|
|
139
143
|
matchedTags,
|
|
140
144
|
matchedCategory: values.categoryBoost > 0 ? context.detectedCategory : null,
|