shieldcortex 3.0.2 → 3.0.4
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/prerender-manifest.json +3 -3
- 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/react-loadable-manifest.json +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/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +9 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/{fa5217550a8ab9a6.js → 49c1cec591af1460.js} +2 -2
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/{f69fd1c5e71fbbfd.js → ca21f348cb163905.js} +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +3 -0
- package/dist/api/routes/admin.d.ts +12 -0
- package/dist/api/routes/admin.js +502 -0
- package/dist/api/routes/graph.d.ts +4 -0
- package/dist/api/routes/graph.js +333 -0
- package/dist/api/routes/incidents.d.ts +2 -0
- package/dist/api/routes/incidents.js +32 -0
- package/dist/api/routes/memories.d.ts +4 -0
- package/dist/api/routes/memories.js +659 -0
- package/dist/api/routes/recall.d.ts +4 -0
- package/dist/api/routes/recall.js +36 -0
- package/dist/api/routes/system.d.ts +9 -0
- package/dist/api/routes/system.js +201 -0
- package/dist/api/visualization-server.js +31 -1913
- package/dist/memory/search.d.ts +37 -0
- package/dist/memory/search.js +143 -0
- package/dist/memory/store.js +15 -166
- package/dist/tools/forget.d.ts +2 -2
- package/dist/tools/recall.d.ts +2 -2
- package/hooks/openclaw/cortex-memory/handler.ts +5 -141
- package/hooks/openclaw/cortex-memory/runtime.mjs +129 -0
- package/package.json +8 -4
- package/plugins/openclaw/dist/index.js +5 -39
- package/scripts/run-jest.mjs +25 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/be6970da20a17c0b.js +0 -9
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/e63d2228780629dd.css +0 -3
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsc.js +0 -133818
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsserver.js +0 -659
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_typingsInstaller.js +0 -222
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsc.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserver.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserverlibrary.js +0 -21
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typesMap.json +0 -497
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typescript.js +0 -200276
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typingsInstaller.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/watchGuard.js +0 -53
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/package.json +0 -120
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/stop-dashboard.sh +0 -21
- /package/dashboard/.next/standalone/dashboard/.next/static/{wWaw0Bi8k5Hc3R8vWNUFY → BEvyMAX62LQMyt5iSb-F9}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{wWaw0Bi8k5Hc3R8vWNUFY → BEvyMAX62LQMyt5iSb-F9}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{wWaw0Bi8k5Hc3R8vWNUFY → BEvyMAX62LQMyt5iSb-F9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { getDatabase } from '../../database/init.js';
|
|
2
|
+
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
|
|
3
|
+
import { calculateDecayedScore } from '../../memory/decay.js';
|
|
4
|
+
import { consolidate, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
|
|
5
|
+
import { getActivationStats, getActiveMemories } from '../../memory/activation.js';
|
|
6
|
+
import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
|
|
7
|
+
import { emitConsolidation } from '../events.js';
|
|
8
|
+
export function registerMemoryRoutes(app, requireNotLocked) {
|
|
9
|
+
app.get('/api/memories', requireNotLocked, async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
12
|
+
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
|
13
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
14
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '50';
|
|
15
|
+
const offsetStr = typeof req.query.offset === 'string' ? req.query.offset : '0';
|
|
16
|
+
const mode = typeof req.query.mode === 'string' ? req.query.mode : 'recent';
|
|
17
|
+
const query = typeof req.query.query === 'string' ? req.query.query : undefined;
|
|
18
|
+
const limit = Math.min(parseInt(limitStr, 10) || 50, 1000);
|
|
19
|
+
const offset = parseInt(offsetStr, 10) || 0;
|
|
20
|
+
let memories;
|
|
21
|
+
if (mode === 'search' && query) {
|
|
22
|
+
const results = await searchMemories({
|
|
23
|
+
query,
|
|
24
|
+
project,
|
|
25
|
+
type: type,
|
|
26
|
+
category: category,
|
|
27
|
+
limit: limit + offset + 1,
|
|
28
|
+
});
|
|
29
|
+
memories = results.map((result) => result.memory);
|
|
30
|
+
}
|
|
31
|
+
else if (mode === 'important') {
|
|
32
|
+
memories = getHighPriorityMemories(limit + offset + 1, project);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
memories = getRecentMemories(limit + offset + 1, project);
|
|
36
|
+
}
|
|
37
|
+
if (type) {
|
|
38
|
+
memories = memories.filter((memory) => memory.type === type);
|
|
39
|
+
}
|
|
40
|
+
if (category) {
|
|
41
|
+
memories = memories.filter((memory) => memory.category === category);
|
|
42
|
+
}
|
|
43
|
+
const stats = getMemoryStats(project);
|
|
44
|
+
const total = stats.total;
|
|
45
|
+
const hasMore = memories.length > offset + limit;
|
|
46
|
+
const paginatedMemories = memories.slice(offset, offset + limit);
|
|
47
|
+
const memoriesWithDecay = paginatedMemories.map((memory) => ({
|
|
48
|
+
...memory,
|
|
49
|
+
decayedScore: calculateDecayedScore(memory),
|
|
50
|
+
}));
|
|
51
|
+
res.json({
|
|
52
|
+
memories: memoriesWithDecay,
|
|
53
|
+
pagination: {
|
|
54
|
+
offset,
|
|
55
|
+
limit,
|
|
56
|
+
total,
|
|
57
|
+
hasMore,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
res.status(500).json({ error: error.message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
app.get('/api/memories/activity', requireNotLocked, (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
68
|
+
const db = getDatabase();
|
|
69
|
+
const query = project
|
|
70
|
+
? `SELECT date(created_at) as date, COUNT(*) as count
|
|
71
|
+
FROM memories WHERE project = ?
|
|
72
|
+
GROUP BY date(created_at)
|
|
73
|
+
ORDER BY date DESC
|
|
74
|
+
LIMIT 365`
|
|
75
|
+
: `SELECT date(created_at) as date, COUNT(*) as count
|
|
76
|
+
FROM memories
|
|
77
|
+
GROUP BY date(created_at)
|
|
78
|
+
ORDER BY date DESC
|
|
79
|
+
LIMIT 365`;
|
|
80
|
+
const rows = project
|
|
81
|
+
? db.prepare(query).all(project)
|
|
82
|
+
: db.prepare(query).all();
|
|
83
|
+
res.json({ activity: rows });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
res.status(500).json({ error: error.message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
app.get('/api/memories/quality', requireNotLocked, (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
92
|
+
const db = getDatabase();
|
|
93
|
+
const projectFilter = project ? 'AND project = ?' : '';
|
|
94
|
+
const params = project ? [project] : [];
|
|
95
|
+
const neverAccessed = db.prepare(`
|
|
96
|
+
SELECT id, title, category, type, created_at, salience
|
|
97
|
+
FROM memories WHERE access_count = 0 ${projectFilter}
|
|
98
|
+
AND created_at < datetime('now', '-1 day')
|
|
99
|
+
ORDER BY created_at DESC LIMIT 50
|
|
100
|
+
`).all(...params);
|
|
101
|
+
const stale = db.prepare(`
|
|
102
|
+
SELECT id, title, category, type, last_accessed, decayed_score, salience
|
|
103
|
+
FROM memories WHERE decayed_score < 0.3 ${projectFilter}
|
|
104
|
+
AND last_accessed < datetime('now', '-30 days')
|
|
105
|
+
ORDER BY decayed_score ASC LIMIT 50
|
|
106
|
+
`).all(...params);
|
|
107
|
+
const duplicates = db.prepare(`
|
|
108
|
+
SELECT m1.id as id1, m1.title as title_a, m2.id as id2, m2.title as title_b
|
|
109
|
+
FROM memories m1
|
|
110
|
+
JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
|
|
111
|
+
${project ? 'WHERE m1.project = ?' : ''}
|
|
112
|
+
LIMIT 50
|
|
113
|
+
`).all(...params);
|
|
114
|
+
res.json({
|
|
115
|
+
neverAccessed: { count: neverAccessed.length, items: neverAccessed },
|
|
116
|
+
stale: { count: stale.length, items: stale },
|
|
117
|
+
duplicates: { count: duplicates.length, items: duplicates },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
res.status(500).json({ error: error.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
app.get('/api/memories/:id', requireNotLocked, (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
const id = parseInt(req.params.id, 10);
|
|
127
|
+
const memory = getMemoryById(id);
|
|
128
|
+
if (!memory) {
|
|
129
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
130
|
+
}
|
|
131
|
+
res.json({
|
|
132
|
+
...memory,
|
|
133
|
+
decayedScore: calculateDecayedScore(memory),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
res.status(500).json({ error: error.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
app.post('/api/memories', requireNotLocked, (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const { title, content, type, category, project, tags, salience } = req.body;
|
|
143
|
+
if (!title || !content) {
|
|
144
|
+
return res.status(400).json({ error: 'Title and content required' });
|
|
145
|
+
}
|
|
146
|
+
const memory = addMemory({
|
|
147
|
+
title,
|
|
148
|
+
content,
|
|
149
|
+
type: type || 'short_term',
|
|
150
|
+
category: category || 'note',
|
|
151
|
+
project,
|
|
152
|
+
tags: tags || [],
|
|
153
|
+
salience,
|
|
154
|
+
});
|
|
155
|
+
res.status(201).json(memory);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.name === 'MemoryPausedError') {
|
|
159
|
+
return res.status(503).json({
|
|
160
|
+
error: 'Memory creation is paused',
|
|
161
|
+
paused: true,
|
|
162
|
+
message: 'Use the dashboard control panel to resume memory creation.',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
res.status(500).json({ error: error.message });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
app.delete('/api/memories/:id', requireNotLocked, (req, res) => {
|
|
169
|
+
try {
|
|
170
|
+
const id = parseInt(req.params.id, 10);
|
|
171
|
+
const success = deleteMemory(id);
|
|
172
|
+
if (!success) {
|
|
173
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
174
|
+
}
|
|
175
|
+
res.json({ success: true });
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
res.status(500).json({ error: error.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
app.post('/api/memories/:id/access', requireNotLocked, (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const id = parseInt(req.params.id, 10);
|
|
184
|
+
const memory = accessMemory(id);
|
|
185
|
+
if (!memory) {
|
|
186
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
187
|
+
}
|
|
188
|
+
res.json({
|
|
189
|
+
...memory,
|
|
190
|
+
decayedScore: calculateDecayedScore(memory),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
res.status(500).json({ error: error.message });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
app.get('/api/stats', (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
200
|
+
const stats = getMemoryStats(project);
|
|
201
|
+
const db = getDatabase();
|
|
202
|
+
const rawRows = db.prepare(project
|
|
203
|
+
? 'SELECT * FROM memories WHERE project = ?'
|
|
204
|
+
: 'SELECT * FROM memories').all(project ? [project] : []);
|
|
205
|
+
const allMemories = rawRows.map(rowToMemory);
|
|
206
|
+
const decayDistribution = {
|
|
207
|
+
healthy: 0,
|
|
208
|
+
fading: 0,
|
|
209
|
+
critical: 0,
|
|
210
|
+
};
|
|
211
|
+
for (const memory of allMemories) {
|
|
212
|
+
const score = calculateDecayedScore(memory);
|
|
213
|
+
if (score > 0.35)
|
|
214
|
+
decayDistribution.healthy++;
|
|
215
|
+
else if (score > 0.2)
|
|
216
|
+
decayDistribution.fading++;
|
|
217
|
+
else
|
|
218
|
+
decayDistribution.critical++;
|
|
219
|
+
}
|
|
220
|
+
const activation = getActivationStats();
|
|
221
|
+
res.json({
|
|
222
|
+
...stats,
|
|
223
|
+
decayDistribution,
|
|
224
|
+
activation,
|
|
225
|
+
timestamp: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
res.status(500).json({ error: error.message });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
app.get('/api/health-score', requireNotLocked, (_req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const db = getDatabase();
|
|
235
|
+
const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
|
|
236
|
+
const freshCount = db.prepare('SELECT COUNT(*) as count FROM memories WHERE decayed_score > 0.3').get().count;
|
|
237
|
+
const freshnessScore = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
|
|
238
|
+
const freshPct = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
|
|
239
|
+
const linkedCount = db.prepare('SELECT COUNT(DISTINCT memory_id) as count FROM memory_entities').get().count;
|
|
240
|
+
const coverageScore = totalCount > 0 ? Math.round((linkedCount / totalCount) * 100) : 0;
|
|
241
|
+
const contradictionCount = db.prepare("SELECT COUNT(*) as count FROM memory_links WHERE relationship = 'contradicts'").get().count;
|
|
242
|
+
const consistencyScore = Math.max(0, 100 - (contradictionCount * 10));
|
|
243
|
+
const lastConsolidated = db.prepare("SELECT created_at FROM memories WHERE type = 'long_term' AND tags LIKE '%auto-consolidated%' ORDER BY created_at DESC LIMIT 1").get();
|
|
244
|
+
let consolidationScore = 25;
|
|
245
|
+
if (lastConsolidated) {
|
|
246
|
+
const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
|
|
247
|
+
if (hoursAgo <= 4)
|
|
248
|
+
consolidationScore = 100;
|
|
249
|
+
else if (hoursAgo <= 8)
|
|
250
|
+
consolidationScore = 75;
|
|
251
|
+
else if (hoursAgo <= 24)
|
|
252
|
+
consolidationScore = 50;
|
|
253
|
+
}
|
|
254
|
+
const overall = Math.round(freshnessScore * 0.3 +
|
|
255
|
+
coverageScore * 0.25 +
|
|
256
|
+
consistencyScore * 0.25 +
|
|
257
|
+
consolidationScore * 0.2);
|
|
258
|
+
let consolidationDetail = 'No consolidated memories found';
|
|
259
|
+
if (lastConsolidated) {
|
|
260
|
+
const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
|
|
261
|
+
consolidationDetail = hoursAgo < 1
|
|
262
|
+
? 'Last consolidated less than 1 hour ago'
|
|
263
|
+
: `Last consolidated ${Math.round(hoursAgo)} hours ago`;
|
|
264
|
+
}
|
|
265
|
+
res.json({
|
|
266
|
+
overall,
|
|
267
|
+
components: {
|
|
268
|
+
freshness: {
|
|
269
|
+
score: freshnessScore,
|
|
270
|
+
label: 'Memory Freshness',
|
|
271
|
+
detail: `${freshPct}% of memories above decay threshold`,
|
|
272
|
+
},
|
|
273
|
+
coverage: {
|
|
274
|
+
score: coverageScore,
|
|
275
|
+
label: 'Graph Coverage',
|
|
276
|
+
detail: `${coverageScore}% of memories have entity links`,
|
|
277
|
+
},
|
|
278
|
+
consistency: {
|
|
279
|
+
score: consistencyScore,
|
|
280
|
+
label: 'Consistency',
|
|
281
|
+
detail: `${contradictionCount} contradictions detected`,
|
|
282
|
+
},
|
|
283
|
+
consolidation: {
|
|
284
|
+
score: consolidationScore,
|
|
285
|
+
label: 'Consolidation',
|
|
286
|
+
detail: consolidationDetail,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
res.status(500).json({ error: error.message });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
app.get('/api/activation', requireNotLocked, (_req, res) => {
|
|
296
|
+
try {
|
|
297
|
+
res.json({
|
|
298
|
+
activeMemories: getActiveMemories(),
|
|
299
|
+
stats: getActivationStats(),
|
|
300
|
+
timestamp: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
res.status(500).json({ error: error.message });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
app.get('/api/contradictions', requireNotLocked, (req, res) => {
|
|
308
|
+
try {
|
|
309
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
310
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
311
|
+
const minScoreStr = typeof req.query.minScore === 'string' ? req.query.minScore : '0.4';
|
|
312
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '20';
|
|
313
|
+
const contradictions = detectContradictions({
|
|
314
|
+
project,
|
|
315
|
+
category: category,
|
|
316
|
+
minScore: parseFloat(minScoreStr) || 0.4,
|
|
317
|
+
limit: parseInt(limitStr, 10) || 20,
|
|
318
|
+
});
|
|
319
|
+
res.json({
|
|
320
|
+
contradictions: contradictions.map((item) => ({
|
|
321
|
+
memoryAId: item.memoryA.id,
|
|
322
|
+
memoryATitle: item.memoryA.title,
|
|
323
|
+
memoryBId: item.memoryB.id,
|
|
324
|
+
memoryBTitle: item.memoryB.title,
|
|
325
|
+
score: item.score,
|
|
326
|
+
reason: item.reason,
|
|
327
|
+
sharedTopics: item.sharedTopics,
|
|
328
|
+
})),
|
|
329
|
+
count: contradictions.length,
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
res.status(500).json({ error: error.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
app.get('/api/memories/:id/contradictions', requireNotLocked, (req, res) => {
|
|
338
|
+
try {
|
|
339
|
+
const id = parseInt(req.params.id, 10);
|
|
340
|
+
if (Number.isNaN(id)) {
|
|
341
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
342
|
+
}
|
|
343
|
+
const contradictions = getContradictionsFor(id);
|
|
344
|
+
res.json({
|
|
345
|
+
memoryId: id,
|
|
346
|
+
contradictions: contradictions.map((item) => ({
|
|
347
|
+
contradictingMemoryId: item.memoryB.id,
|
|
348
|
+
contradictingMemoryTitle: item.memoryB.title,
|
|
349
|
+
score: item.score,
|
|
350
|
+
reason: item.reason,
|
|
351
|
+
sharedTopics: item.sharedTopics,
|
|
352
|
+
})),
|
|
353
|
+
count: contradictions.length,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
res.status(500).json({ error: error.message });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
app.post('/api/memories/:id/enrich', requireNotLocked, (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const id = parseInt(req.params.id, 10);
|
|
363
|
+
if (Number.isNaN(id)) {
|
|
364
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
365
|
+
}
|
|
366
|
+
const { context, contextType } = req.body;
|
|
367
|
+
if (!context || typeof context !== 'string') {
|
|
368
|
+
return res.status(400).json({ error: 'Context string required in request body' });
|
|
369
|
+
}
|
|
370
|
+
const validTypes = ['search', 'access', 'related'];
|
|
371
|
+
const type = validTypes.includes(contextType) ? contextType : 'access';
|
|
372
|
+
res.json(enrichMemory(id, context, type));
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
res.status(500).json({ error: error.message });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
app.get('/api/projects', (_req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const db = getDatabase();
|
|
381
|
+
const projects = db.prepare(`
|
|
382
|
+
SELECT DISTINCT project, COUNT(*) as memory_count
|
|
383
|
+
FROM memories
|
|
384
|
+
WHERE project IS NOT NULL AND project != ''
|
|
385
|
+
GROUP BY project
|
|
386
|
+
ORDER BY memory_count DESC
|
|
387
|
+
`).all();
|
|
388
|
+
const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get();
|
|
389
|
+
res.json({
|
|
390
|
+
projects: [
|
|
391
|
+
{ project: null, memory_count: totalCount.count, label: 'All Projects' },
|
|
392
|
+
...projects.map((project) => ({ ...project, label: project.project })),
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
res.status(500).json({ error: error.message });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
app.get('/api/links', requireNotLocked, (req, res) => {
|
|
401
|
+
try {
|
|
402
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
403
|
+
const db = getDatabase();
|
|
404
|
+
const query = project
|
|
405
|
+
? `
|
|
406
|
+
SELECT
|
|
407
|
+
ml.*,
|
|
408
|
+
m1.title as source_title,
|
|
409
|
+
m1.category as source_category,
|
|
410
|
+
m1.type as source_type,
|
|
411
|
+
m2.title as target_title,
|
|
412
|
+
m2.category as target_category,
|
|
413
|
+
m2.type as target_type
|
|
414
|
+
FROM memory_links ml
|
|
415
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
416
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
417
|
+
WHERE m1.project = ? OR m2.project = ?
|
|
418
|
+
ORDER BY ml.created_at DESC
|
|
419
|
+
LIMIT 500
|
|
420
|
+
`
|
|
421
|
+
: `
|
|
422
|
+
SELECT
|
|
423
|
+
ml.*,
|
|
424
|
+
m1.title as source_title,
|
|
425
|
+
m1.category as source_category,
|
|
426
|
+
m1.type as source_type,
|
|
427
|
+
m2.title as target_title,
|
|
428
|
+
m2.category as target_category,
|
|
429
|
+
m2.type as target_type
|
|
430
|
+
FROM memory_links ml
|
|
431
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
432
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
433
|
+
ORDER BY ml.created_at DESC
|
|
434
|
+
LIMIT 500
|
|
435
|
+
`;
|
|
436
|
+
const links = project
|
|
437
|
+
? db.prepare(query).all(project, project)
|
|
438
|
+
: db.prepare(query).all();
|
|
439
|
+
res.json(links);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
res.status(500).json({ error: error.message });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
app.post('/api/consolidate', requireNotLocked, (_req, res) => {
|
|
446
|
+
try {
|
|
447
|
+
const result = consolidate();
|
|
448
|
+
emitConsolidation(result);
|
|
449
|
+
res.json({ success: true, ...result });
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
res.status(500).json({ error: error.message });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
app.get('/api/context', requireNotLocked, async (req, res) => {
|
|
456
|
+
try {
|
|
457
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
458
|
+
const summary = await generateContextSummary(project);
|
|
459
|
+
res.json({
|
|
460
|
+
summary,
|
|
461
|
+
formatted: formatContextSummary(summary),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
res.status(500).json({ error: error.message });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
app.get('/api/suggestions', requireNotLocked, (req, res) => {
|
|
469
|
+
try {
|
|
470
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
471
|
+
const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : 10;
|
|
472
|
+
if (!query || query.length < 2) {
|
|
473
|
+
return res.json({ suggestions: [] });
|
|
474
|
+
}
|
|
475
|
+
const db = getDatabase();
|
|
476
|
+
const suggestions = [];
|
|
477
|
+
const titleMatches = db.prepare(`
|
|
478
|
+
SELECT DISTINCT title, COUNT(*) as count
|
|
479
|
+
FROM memories
|
|
480
|
+
WHERE title LIKE ?
|
|
481
|
+
GROUP BY title
|
|
482
|
+
ORDER BY count DESC, last_accessed DESC
|
|
483
|
+
LIMIT ?
|
|
484
|
+
`).all(`%${query}%`, limit);
|
|
485
|
+
for (const match of titleMatches) {
|
|
486
|
+
suggestions.push({ text: match.title, type: 'title', count: match.count });
|
|
487
|
+
}
|
|
488
|
+
const categoryMatches = db.prepare(`
|
|
489
|
+
SELECT DISTINCT category, COUNT(*) as count
|
|
490
|
+
FROM memories
|
|
491
|
+
WHERE category LIKE ?
|
|
492
|
+
GROUP BY category
|
|
493
|
+
ORDER BY count DESC
|
|
494
|
+
LIMIT 5
|
|
495
|
+
`).all(`%${query}%`);
|
|
496
|
+
for (const match of categoryMatches) {
|
|
497
|
+
suggestions.push({ text: match.category, type: 'category', count: match.count });
|
|
498
|
+
}
|
|
499
|
+
const projectMatches = db.prepare(`
|
|
500
|
+
SELECT DISTINCT project, COUNT(*) as count
|
|
501
|
+
FROM memories
|
|
502
|
+
WHERE project IS NOT NULL AND project LIKE ?
|
|
503
|
+
GROUP BY project
|
|
504
|
+
ORDER BY count DESC
|
|
505
|
+
LIMIT 5
|
|
506
|
+
`).all(`%${query}%`);
|
|
507
|
+
for (const match of projectMatches) {
|
|
508
|
+
suggestions.push({ text: match.project, type: 'project', count: match.count });
|
|
509
|
+
}
|
|
510
|
+
const tagRows = db.prepare(`
|
|
511
|
+
SELECT tags FROM memories
|
|
512
|
+
WHERE tags IS NOT NULL AND tags != '[]'
|
|
513
|
+
LIMIT 500
|
|
514
|
+
`).all();
|
|
515
|
+
const tagCounts = new Map();
|
|
516
|
+
for (const row of tagRows) {
|
|
517
|
+
try {
|
|
518
|
+
const tags = JSON.parse(row.tags);
|
|
519
|
+
for (const tag of tags) {
|
|
520
|
+
if (tag.toLowerCase().includes(query.toLowerCase())) {
|
|
521
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// Ignore malformed tag rows.
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const tagMatches = Array.from(tagCounts.entries())
|
|
530
|
+
.map(([text, count]) => ({ text, type: 'tag', count }))
|
|
531
|
+
.sort((a, b) => b.count - a.count)
|
|
532
|
+
.slice(0, 5);
|
|
533
|
+
suggestions.push(...tagMatches);
|
|
534
|
+
const dedupedSuggestions = suggestions.filter((suggestion, index, all) => index === all.findIndex((other) => other.text.toLowerCase() === suggestion.text.toLowerCase() && other.type === suggestion.type));
|
|
535
|
+
const limitedSuggestions = dedupedSuggestions
|
|
536
|
+
.sort((a, b) => b.count - a.count)
|
|
537
|
+
.slice(0, limit);
|
|
538
|
+
res.json({ suggestions: limitedSuggestions });
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
res.status(500).json({ error: error.message });
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
app.post('/api/memories/:id/boost', requireNotLocked, (req, res) => {
|
|
545
|
+
try {
|
|
546
|
+
const id = parseInt(req.params.id, 10);
|
|
547
|
+
const memory = getMemoryById(id);
|
|
548
|
+
if (!memory) {
|
|
549
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
550
|
+
}
|
|
551
|
+
const updated = updateMemory(id, { salience: Math.min(1.0, (memory.salience ?? 0.5) + 0.15) });
|
|
552
|
+
res.json(updated);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
res.status(500).json({ error: error.message });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
app.post('/api/memories/:id/demote', requireNotLocked, (req, res) => {
|
|
559
|
+
try {
|
|
560
|
+
const id = parseInt(req.params.id, 10);
|
|
561
|
+
const memory = getMemoryById(id);
|
|
562
|
+
if (!memory) {
|
|
563
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
564
|
+
}
|
|
565
|
+
const updated = updateMemory(id, { salience: Math.max(0.05, (memory.salience ?? 0.5) - 0.15) });
|
|
566
|
+
res.json(updated);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
res.status(500).json({ error: error.message });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
app.post('/api/memories/:id/promote', requireNotLocked, (req, res) => {
|
|
573
|
+
try {
|
|
574
|
+
const id = parseInt(req.params.id, 10);
|
|
575
|
+
const memory = promoteMemory(id);
|
|
576
|
+
if (!memory) {
|
|
577
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
578
|
+
}
|
|
579
|
+
res.json(memory);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
res.status(500).json({ error: error.message });
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
app.patch('/api/memories/:id', requireNotLocked, (req, res) => {
|
|
586
|
+
try {
|
|
587
|
+
const id = parseInt(req.params.id, 10);
|
|
588
|
+
const { title, content, category, tags, importance } = req.body;
|
|
589
|
+
if (title !== undefined && (typeof title !== 'string' || title.trim().length === 0)) {
|
|
590
|
+
return res.status(400).json({ error: 'Title must be a non-empty string' });
|
|
591
|
+
}
|
|
592
|
+
if (content !== undefined && typeof content !== 'string') {
|
|
593
|
+
return res.status(400).json({ error: 'Content must be a string' });
|
|
594
|
+
}
|
|
595
|
+
const validCategories = ['architecture', 'pattern', 'preference', 'error', 'context', 'learning', 'todo', 'note', 'relationship', 'custom'];
|
|
596
|
+
if (category !== undefined && !validCategories.includes(category)) {
|
|
597
|
+
return res.status(400).json({ error: `Category must be one of: ${validCategories.join(', ')}` });
|
|
598
|
+
}
|
|
599
|
+
if (tags !== undefined && (!Array.isArray(tags) || !tags.every((tag) => typeof tag === 'string'))) {
|
|
600
|
+
return res.status(400).json({ error: 'Tags must be an array of strings' });
|
|
601
|
+
}
|
|
602
|
+
if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
|
|
603
|
+
return res.status(400).json({ error: 'Importance must be a number between 0 and 1' });
|
|
604
|
+
}
|
|
605
|
+
const updates = {};
|
|
606
|
+
if (title !== undefined)
|
|
607
|
+
updates.title = title.trim();
|
|
608
|
+
if (content !== undefined)
|
|
609
|
+
updates.content = content;
|
|
610
|
+
if (category !== undefined)
|
|
611
|
+
updates.category = category;
|
|
612
|
+
if (tags !== undefined)
|
|
613
|
+
updates.tags = tags;
|
|
614
|
+
if (importance !== undefined)
|
|
615
|
+
updates.salience = importance;
|
|
616
|
+
const updated = updateMemory(id, updates);
|
|
617
|
+
if (!updated) {
|
|
618
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
619
|
+
}
|
|
620
|
+
res.json(updated);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
res.status(500).json({ error: error.message });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
app.post('/api/memories/:id/quarantine', requireNotLocked, (req, res) => {
|
|
627
|
+
try {
|
|
628
|
+
const id = parseInt(req.params.id, 10);
|
|
629
|
+
const memory = getMemoryById(id);
|
|
630
|
+
if (!memory) {
|
|
631
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
632
|
+
}
|
|
633
|
+
const db = getDatabase();
|
|
634
|
+
db.prepare(`INSERT INTO quarantine (original_title, original_content, source_type, source_identifier, reason, project, status, created_at)
|
|
635
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`).run(memory.title, memory.content, 'dashboard', 'brain-control', req.body.reason || 'Manually quarantined from Brain dashboard', memory.project || null, new Date().toISOString());
|
|
636
|
+
deleteMemory(id);
|
|
637
|
+
res.json({ success: true, quarantined: id });
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
res.status(500).json({ error: error.message });
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
app.post('/api/links', requireNotLocked, (req, res) => {
|
|
644
|
+
try {
|
|
645
|
+
const { sourceId, targetId, relationship, strength } = req.body;
|
|
646
|
+
if (!sourceId || !targetId || !relationship) {
|
|
647
|
+
return res.status(400).json({ error: 'sourceId, targetId, and relationship are required' });
|
|
648
|
+
}
|
|
649
|
+
const link = createMemoryLink(sourceId, targetId, relationship, strength ?? 0.5);
|
|
650
|
+
if (!link) {
|
|
651
|
+
return res.status(404).json({ error: 'One or both memories not found, or self-link attempted' });
|
|
652
|
+
}
|
|
653
|
+
res.json(link);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
res.status(500).json({ error: error.message });
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|