memorix 1.6.3 → 1.7.1
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/CHANGELOG.md +31 -0
- package/dist/cli/index.js +1029 -643
- package/dist/cli/memcode.js +0 -0
- package/dist/dashboard/static/app.js +298 -37
- package/dist/dashboard/static/index.html +17 -0
- package/dist/dashboard/static/style.css +328 -0
- package/dist/index.js +902 -394
- package/dist/memcode-runtime/CHANGELOG.md +31 -0
- package/dist/sdk.js +901 -393
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/DASHBOARD.md +54 -0
- package/docs/README.md +2 -0
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/cleanup.ts +13 -114
- package/src/cli/commands/memory.ts +12 -43
- package/src/dashboard/maintenance.ts +303 -0
- package/src/dashboard/server.ts +100 -2
- package/src/dashboard/static/index.html +17 -0
- package/src/dashboard/static/style.css +328 -0
- package/src/memory/cleanup.ts +143 -0
- package/src/memory/deduplication.ts +178 -0
- package/src/server.ts +12 -46
package/dist/cli/memcode.js
CHANGED
|
File without changes
|
|
@@ -49,6 +49,38 @@ const i18n = {
|
|
|
49
49
|
deleteObs: 'Delete',
|
|
50
50
|
deleteConfirm: 'Delete observation #%id%?',
|
|
51
51
|
batchCleanup: 'Cleanup',
|
|
52
|
+
selectRecords: 'Select',
|
|
53
|
+
memoryMaintenance: 'Memory Maintenance',
|
|
54
|
+
previewRequired: 'Preview required',
|
|
55
|
+
cleanupAction: 'Cleanup',
|
|
56
|
+
consolidateAction: 'Consolidate',
|
|
57
|
+
deduplicateAction: 'Deduplicate',
|
|
58
|
+
archiveAction: 'Archive',
|
|
59
|
+
previewAction: 'Preview',
|
|
60
|
+
executeAction: 'Execute',
|
|
61
|
+
closeAction: 'Close',
|
|
62
|
+
cleanupNoise: 'Include test and demo noise',
|
|
63
|
+
maintenancePreview: 'Maintenance preview',
|
|
64
|
+
maintenanceEmpty: 'No candidates found',
|
|
65
|
+
maintenanceUnavailable: 'Action unavailable',
|
|
66
|
+
maintenanceChanged: 'Memory changed. Preview again before executing.',
|
|
67
|
+
maintenanceSuccess: 'Maintenance completed',
|
|
68
|
+
maintenanceFailed: 'Maintenance failed',
|
|
69
|
+
willDelete: 'Delete',
|
|
70
|
+
willArchive: 'Archive',
|
|
71
|
+
willResolve: 'Resolve',
|
|
72
|
+
willMerge: 'Merge',
|
|
73
|
+
canonicalRecord: 'Keep',
|
|
74
|
+
affectedRecords: 'Affected records',
|
|
75
|
+
cleanupLowQuality: 'Low quality',
|
|
76
|
+
cleanupDuplicates: 'Exact duplicates',
|
|
77
|
+
cleanupNoiseCount: 'Noise',
|
|
78
|
+
scannedRecords: 'Scanned',
|
|
79
|
+
comparisonFailures: 'Comparison failures',
|
|
80
|
+
confirmCleanup: 'Execute cleanup',
|
|
81
|
+
confirmConsolidate: 'Merge candidates',
|
|
82
|
+
confirmDeduplicate: 'Resolve duplicates',
|
|
83
|
+
confirmArchive: 'Archive candidates',
|
|
52
84
|
selected: 'selected',
|
|
53
85
|
cancel: 'Cancel',
|
|
54
86
|
deleteSelected: 'Delete Selected',
|
|
@@ -481,6 +513,38 @@ const i18n = {
|
|
|
481
513
|
deleteObs: '删除',
|
|
482
514
|
deleteConfirm: '确认删除观察 #%id%?',
|
|
483
515
|
batchCleanup: '清理',
|
|
516
|
+
selectRecords: '选择',
|
|
517
|
+
memoryMaintenance: '记忆维护',
|
|
518
|
+
previewRequired: '执行前需预览',
|
|
519
|
+
cleanupAction: '清理',
|
|
520
|
+
consolidateAction: '合并',
|
|
521
|
+
deduplicateAction: '智能去重',
|
|
522
|
+
archiveAction: '归档',
|
|
523
|
+
previewAction: '预览',
|
|
524
|
+
executeAction: '执行',
|
|
525
|
+
closeAction: '关闭',
|
|
526
|
+
cleanupNoise: '包含测试和演示噪声',
|
|
527
|
+
maintenancePreview: '维护预览',
|
|
528
|
+
maintenanceEmpty: '没有发现候选项',
|
|
529
|
+
maintenanceUnavailable: '当前操作不可用',
|
|
530
|
+
maintenanceChanged: '记忆已发生变化,请重新预览后再执行。',
|
|
531
|
+
maintenanceSuccess: '维护完成',
|
|
532
|
+
maintenanceFailed: '维护失败',
|
|
533
|
+
willDelete: '删除',
|
|
534
|
+
willArchive: '归档',
|
|
535
|
+
willResolve: '设为已解决',
|
|
536
|
+
willMerge: '合并',
|
|
537
|
+
canonicalRecord: '保留',
|
|
538
|
+
affectedRecords: '受影响记录',
|
|
539
|
+
cleanupLowQuality: '低质量记录',
|
|
540
|
+
cleanupDuplicates: '完全重复',
|
|
541
|
+
cleanupNoiseCount: '噪声',
|
|
542
|
+
scannedRecords: '已扫描',
|
|
543
|
+
comparisonFailures: '比较失败',
|
|
544
|
+
confirmCleanup: '执行清理',
|
|
545
|
+
confirmConsolidate: '合并候选项',
|
|
546
|
+
confirmDeduplicate: '解决重复项',
|
|
547
|
+
confirmArchive: '归档候选项',
|
|
484
548
|
selected: '已选中',
|
|
485
549
|
cancel: '取消',
|
|
486
550
|
deleteSelected: '删除所选',
|
|
@@ -1071,6 +1135,209 @@ async function api(endpoint) {
|
|
|
1071
1135
|
}
|
|
1072
1136
|
}
|
|
1073
1137
|
|
|
1138
|
+
// ============================================================
|
|
1139
|
+
// Maintenance actions
|
|
1140
|
+
// ============================================================
|
|
1141
|
+
|
|
1142
|
+
let activeMaintenancePreview = null;
|
|
1143
|
+
|
|
1144
|
+
const maintenanceMeta = {
|
|
1145
|
+
cleanup: { title: 'cleanupAction', confirm: 'confirmCleanup', icon: 'lucide:eraser' },
|
|
1146
|
+
consolidate: { title: 'consolidateAction', confirm: 'confirmConsolidate', icon: 'lucide:combine' },
|
|
1147
|
+
deduplicate: { title: 'deduplicateAction', confirm: 'confirmDeduplicate', icon: 'lucide:copy-minus' },
|
|
1148
|
+
retention: { title: 'archiveAction', confirm: 'confirmArchive', icon: 'lucide:archive' },
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
async function maintenancePost(action, phase, body = {}) {
|
|
1152
|
+
const suffix = selectedProject ? `?project=${encodeURIComponent(selectedProject)}` : '';
|
|
1153
|
+
const response = await fetch(`/api/maintenance/${action}/${phase}${suffix}`, {
|
|
1154
|
+
method: 'POST',
|
|
1155
|
+
headers: { 'content-type': 'application/json' },
|
|
1156
|
+
body: JSON.stringify(body),
|
|
1157
|
+
});
|
|
1158
|
+
const data = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
1159
|
+
if (!response.ok) {
|
|
1160
|
+
const error = new Error(data.error || `HTTP ${response.status}`);
|
|
1161
|
+
error.status = response.status;
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
1164
|
+
return data;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function showDashboardToast(message, tone = 'success') {
|
|
1168
|
+
const toast = document.getElementById('dashboard-toast');
|
|
1169
|
+
if (!toast) return;
|
|
1170
|
+
toast.textContent = message;
|
|
1171
|
+
toast.dataset.tone = tone;
|
|
1172
|
+
toast.classList.add('visible');
|
|
1173
|
+
clearTimeout(showDashboardToast.timer);
|
|
1174
|
+
showDashboardToast.timer = setTimeout(() => toast.classList.remove('visible'), 3200);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function maintenanceSummaryEntries(action, summary = {}) {
|
|
1178
|
+
const mappings = {
|
|
1179
|
+
cleanup: [
|
|
1180
|
+
['delete', 'willDelete'], ['archive', 'willArchive'],
|
|
1181
|
+
['lowQuality', 'cleanupLowQuality'], ['duplicates', 'cleanupDuplicates'], ['noise', 'cleanupNoiseCount'],
|
|
1182
|
+
],
|
|
1183
|
+
consolidate: [['clusters', 'willMerge'], ['observations', 'affectedRecords']],
|
|
1184
|
+
deduplicate: [['resolve', 'willResolve'], ['scanned', 'scannedRecords'], ['failedComparisons', 'comparisonFailures']],
|
|
1185
|
+
retention: [['archive', 'willArchive']],
|
|
1186
|
+
};
|
|
1187
|
+
return (mappings[action] || [])
|
|
1188
|
+
.filter(([key]) => summary[key] !== undefined)
|
|
1189
|
+
.map(([key, label]) => ({ label: t(label), value: summary[key] }));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function maintenanceRecord(record, prefix = '') {
|
|
1193
|
+
return `
|
|
1194
|
+
<div class="maintenance-record">
|
|
1195
|
+
<span class="maintenance-record-id">#${record.id}</span>
|
|
1196
|
+
<span class="maintenance-record-title">${prefix}${escapeHtml(record.title || t('untitled'))}</span>
|
|
1197
|
+
<span class="maintenance-record-meta">${escapeHtml(record.entityName || '')}</span>
|
|
1198
|
+
</div>`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function maintenanceDetails(action, preview) {
|
|
1202
|
+
if (action === 'cleanup') {
|
|
1203
|
+
const lowQuality = (preview.lowQuality || []).map((item) => maintenanceRecord(item)).join('');
|
|
1204
|
+
const duplicateGroups = (preview.duplicateGroups || []).map((group) => `
|
|
1205
|
+
<div class="maintenance-group">
|
|
1206
|
+
${maintenanceRecord(group.canonical, `${t('canonicalRecord')}: `)}
|
|
1207
|
+
${(group.duplicates || []).map((item) => maintenanceRecord(item, `${t('willDelete')}: `)).join('')}
|
|
1208
|
+
</div>`).join('');
|
|
1209
|
+
const noise = (preview.noise || []).map((item) => maintenanceRecord(item, `${t('willArchive')}: `)).join('');
|
|
1210
|
+
const records = `${lowQuality}${duplicateGroups}${noise}`;
|
|
1211
|
+
return `
|
|
1212
|
+
<label class="maintenance-toggle">
|
|
1213
|
+
<input id="maintenance-noise-toggle" type="checkbox" ${preview.includeNoise ? 'checked' : ''}>
|
|
1214
|
+
<span>${t('cleanupNoise')}</span>
|
|
1215
|
+
</label>
|
|
1216
|
+
${records || `<div class="maintenance-empty">${t('maintenanceEmpty')}</div>`}`;
|
|
1217
|
+
}
|
|
1218
|
+
if (action === 'consolidate') {
|
|
1219
|
+
return (preview.clusters || []).map((cluster, index) => `
|
|
1220
|
+
<div class="maintenance-group">
|
|
1221
|
+
<div class="maintenance-group-title">${t('willMerge')} ${index + 1} · ${escapeHtml(cluster.entityName)} · ${Math.round(cluster.similarity * 100)}%</div>
|
|
1222
|
+
${(cluster.ids || []).map((id, itemIndex) => maintenanceRecord({
|
|
1223
|
+
id,
|
|
1224
|
+
title: (cluster.titles || [])[itemIndex] || '',
|
|
1225
|
+
entityName: cluster.type || '',
|
|
1226
|
+
})).join('')}
|
|
1227
|
+
</div>`).join('');
|
|
1228
|
+
}
|
|
1229
|
+
if (action === 'deduplicate') {
|
|
1230
|
+
return (preview.actions || []).map((item) => `
|
|
1231
|
+
<div class="maintenance-group">
|
|
1232
|
+
${maintenanceRecord({ id: item.keepId, title: item.keepTitle, entityName: item.entityName }, `${t('canonicalRecord')}: `)}
|
|
1233
|
+
${maintenanceRecord({ id: item.resolveId, title: item.resolveTitle, entityName: item.reason }, `${t('willResolve')}: `)}
|
|
1234
|
+
</div>`).join('');
|
|
1235
|
+
}
|
|
1236
|
+
return (preview.candidates || []).map((item) => maintenanceRecord({
|
|
1237
|
+
...item,
|
|
1238
|
+
entityName: `${item.score} · ${item.ageHours}h`,
|
|
1239
|
+
})).join('');
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function renderMaintenanceDialog(action, preview) {
|
|
1243
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1244
|
+
const body = document.getElementById('maintenance-dialog-body');
|
|
1245
|
+
const footer = document.getElementById('maintenance-dialog-footer');
|
|
1246
|
+
const meta = maintenanceMeta[action];
|
|
1247
|
+
document.getElementById('maintenance-dialog-kicker').textContent = t('maintenancePreview');
|
|
1248
|
+
document.getElementById('maintenance-dialog-title').textContent = t(meta.title);
|
|
1249
|
+
|
|
1250
|
+
if (preview.available === false) {
|
|
1251
|
+
body.innerHTML = `<div class="maintenance-empty"><strong>${t('maintenanceUnavailable')}</strong><span>${escapeHtml(preview.reason || '')}</span></div>`;
|
|
1252
|
+
footer.innerHTML = `<button class="maintenance-secondary" type="button" data-maintenance-close>${t('closeAction')}</button>`;
|
|
1253
|
+
} else {
|
|
1254
|
+
const summary = maintenanceSummaryEntries(action, preview.summary);
|
|
1255
|
+
const hasChanges = action === 'cleanup'
|
|
1256
|
+
? (preview.summary.delete + preview.summary.archive) > 0
|
|
1257
|
+
: action === 'consolidate'
|
|
1258
|
+
? preview.summary.clusters > 0
|
|
1259
|
+
: action === 'deduplicate'
|
|
1260
|
+
? preview.summary.resolve > 0
|
|
1261
|
+
: preview.summary.archive > 0;
|
|
1262
|
+
body.innerHTML = `
|
|
1263
|
+
<div class="maintenance-summary">
|
|
1264
|
+
${summary.map((item) => `<div class="maintenance-metric"><span>${escapeHtml(item.label)}</span><strong>${item.value}</strong></div>`).join('')}
|
|
1265
|
+
</div>
|
|
1266
|
+
<div class="maintenance-records">
|
|
1267
|
+
${action === 'cleanup' || hasChanges ? maintenanceDetails(action, preview) : `<div class="maintenance-empty">${t('maintenanceEmpty')}</div>`}
|
|
1268
|
+
</div>`;
|
|
1269
|
+
footer.innerHTML = `
|
|
1270
|
+
<button class="maintenance-secondary" type="button" data-maintenance-close>${t('cancel')}</button>
|
|
1271
|
+
<button class="maintenance-primary" id="maintenance-execute" type="button" ${hasChanges ? '' : 'disabled'}>
|
|
1272
|
+
<span class="iconify" data-icon="${meta.icon}"></span>${t(meta.confirm)}
|
|
1273
|
+
</button>`;
|
|
1274
|
+
document.getElementById('maintenance-execute')?.addEventListener('click', executeMaintenancePreview);
|
|
1275
|
+
document.getElementById('maintenance-noise-toggle')?.addEventListener('change', (event) => {
|
|
1276
|
+
openMaintenancePreview('cleanup', { includeNoise: event.target.checked });
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
footer.querySelector('[data-maintenance-close]')?.addEventListener('click', closeMaintenanceDialog);
|
|
1280
|
+
if (!dialog.open) dialog.showModal();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async function openMaintenancePreview(action, options = {}) {
|
|
1284
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1285
|
+
const meta = maintenanceMeta[action];
|
|
1286
|
+
document.getElementById('maintenance-dialog-kicker').textContent = t('maintenancePreview');
|
|
1287
|
+
document.getElementById('maintenance-dialog-title').textContent = t(meta.title);
|
|
1288
|
+
document.getElementById('maintenance-dialog-body').innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
1289
|
+
document.getElementById('maintenance-dialog-footer').innerHTML = '';
|
|
1290
|
+
if (!dialog.open) dialog.showModal();
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
const preview = await maintenancePost(action, 'preview', action === 'cleanup' ? { includeNoise: options.includeNoise === true } : {});
|
|
1294
|
+
activeMaintenancePreview = { action, preview };
|
|
1295
|
+
renderMaintenanceDialog(action, preview);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
activeMaintenancePreview = null;
|
|
1298
|
+
document.getElementById('maintenance-dialog-body').innerHTML = `
|
|
1299
|
+
<div class="maintenance-empty"><strong>${t('maintenanceFailed')}</strong><span>${escapeHtml(error.message)}</span></div>`;
|
|
1300
|
+
document.getElementById('maintenance-dialog-footer').innerHTML = `
|
|
1301
|
+
<button class="maintenance-secondary" type="button" data-maintenance-close>${t('closeAction')}</button>`;
|
|
1302
|
+
document.querySelector('[data-maintenance-close]')?.addEventListener('click', closeMaintenanceDialog);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function executeMaintenancePreview() {
|
|
1307
|
+
if (!activeMaintenancePreview) return;
|
|
1308
|
+
const { action, preview } = activeMaintenancePreview;
|
|
1309
|
+
const button = document.getElementById('maintenance-execute');
|
|
1310
|
+
button.disabled = true;
|
|
1311
|
+
button.classList.add('busy');
|
|
1312
|
+
try {
|
|
1313
|
+
await maintenancePost(action, 'execute', { payload: preview.payload, token: preview.token });
|
|
1314
|
+
closeMaintenanceDialog();
|
|
1315
|
+
showDashboardToast(t('maintenanceSuccess'));
|
|
1316
|
+
if (currentPage === 'retention') await loadRetention();
|
|
1317
|
+
else await loadObservations();
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
button.disabled = false;
|
|
1320
|
+
button.classList.remove('busy');
|
|
1321
|
+
const message = error.status === 409 ? t('maintenanceChanged') : `${t('maintenanceFailed')}: ${error.message}`;
|
|
1322
|
+
showDashboardToast(message, 'error');
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function closeMaintenanceDialog() {
|
|
1327
|
+
activeMaintenancePreview = null;
|
|
1328
|
+
document.getElementById('maintenance-dialog')?.close();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
onDomReady(() => {
|
|
1332
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1333
|
+
dialog?.querySelector('.maintenance-dialog-close')?.addEventListener('click', closeMaintenanceDialog);
|
|
1334
|
+
dialog?.addEventListener('click', (event) => {
|
|
1335
|
+
if (event.target === dialog) closeMaintenanceDialog();
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
window.openMaintenancePreview = openMaintenancePreview;
|
|
1340
|
+
|
|
1074
1341
|
// ============================================================
|
|
1075
1342
|
// Project Switcher — Custom Dropdown
|
|
1076
1343
|
// ============================================================
|
|
@@ -2110,20 +2377,21 @@ function renderSemanticGraph(kg) {
|
|
|
2110
2377
|
backgroundColor: 'transparent',
|
|
2111
2378
|
tooltip: {
|
|
2112
2379
|
trigger: 'item',
|
|
2380
|
+
confine: true,
|
|
2113
2381
|
backgroundColor: tooltipBg,
|
|
2114
2382
|
borderColor: tooltipBorder,
|
|
2115
2383
|
borderWidth: 1,
|
|
2116
2384
|
textStyle: { color: labelColor, fontSize: 12, fontFamily: 'Inter, system-ui, sans-serif' },
|
|
2117
|
-
extraCssText: 'box-shadow: 0 4px 16px rgba(0,0,0,0.18); border-radius: 8px; padding: 10px 12px; max-width:
|
|
2385
|
+
extraCssText: 'box-shadow: 0 4px 16px rgba(0,0,0,0.18); border-radius: 8px; padding: 10px 12px; width: min(320px, calc(100vw - 32px)); max-width: calc(100vw - 32px); box-sizing: border-box; white-space: normal; overflow-wrap: anywhere; word-break: break-word;',
|
|
2118
2386
|
formatter: (params) => {
|
|
2119
2387
|
if (params.dataType === 'node') {
|
|
2120
2388
|
const d = params.data;
|
|
2121
2389
|
const summary = d.summary ? String(d.summary).slice(0, 160) : '';
|
|
2122
2390
|
return (
|
|
2123
|
-
`<div style="font-weight:600;font-size:13px;margin-bottom:4px;line-height:1.35;">${wrap(d.fullLabel || d.name)}</div>` +
|
|
2391
|
+
`<div style="font-weight:600;font-size:13px;margin-bottom:4px;line-height:1.35;white-space:normal;overflow-wrap:anywhere;word-break:break-word;">${wrap(d.fullLabel || d.name)}</div>` +
|
|
2124
2392
|
`<div style="font-size:11px;color:${labelMutedColor};margin-bottom:6px;">${wrap(d.nodeType || '')}${d.entityName ? ' \u00b7 ' + wrap(d.entityName) : ''}</div>` +
|
|
2125
|
-
`<div style="font-size:11px;color:${labelMutedColor};">${d.evidenceCount || 0} ${wrap(t('kgInspectorEvidence'))}</div>` +
|
|
2126
|
-
(summary ? `<div style="font-size:11.5px;line-height:1.5;margin-top:8px;color:${labelColor};opacity:0.85;">${wrap(summary)}\u2026</div>` : '')
|
|
2393
|
+
`<div style="font-size:11px;color:${labelMutedColor};white-space:normal;overflow-wrap:anywhere;word-break:break-word;">${d.evidenceCount || 0} ${wrap(t('kgInspectorEvidence'))}</div>` +
|
|
2394
|
+
(summary ? `<div style="font-size:11.5px;line-height:1.5;margin-top:8px;color:${labelColor};opacity:0.85;white-space:normal;overflow-wrap:anywhere;word-break:break-word;">${wrap(summary)}\u2026</div>` : '')
|
|
2127
2395
|
);
|
|
2128
2396
|
}
|
|
2129
2397
|
if (params.dataType === 'edge') {
|
|
@@ -3214,20 +3482,6 @@ let obsTypeFilter = '';
|
|
|
3214
3482
|
let batchMode = false;
|
|
3215
3483
|
let selectedIds = new Set();
|
|
3216
3484
|
|
|
3217
|
-
// Low quality detection (same patterns as CLI cleanup)
|
|
3218
|
-
const LOW_QUALITY_OBS_PATTERNS = [
|
|
3219
|
-
/^Session activity/i,
|
|
3220
|
-
/^Updated \S+\.\w+$/i,
|
|
3221
|
-
/^Created \S+\.\w+$/i,
|
|
3222
|
-
/^Deleted \S+\.\w+$/i,
|
|
3223
|
-
/^Modified \S+\.\w+$/i,
|
|
3224
|
-
/^Ran command:/i,
|
|
3225
|
-
/^Read file:/i,
|
|
3226
|
-
];
|
|
3227
|
-
function isLowQualityObs(title) {
|
|
3228
|
-
return LOW_QUALITY_OBS_PATTERNS.some(p => p.test(title.trim()));
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
3485
|
function renderBatchToolbar() {
|
|
3232
3486
|
const slot = document.getElementById('batch-toolbar-slot');
|
|
3233
3487
|
if (!slot) return;
|
|
@@ -3314,8 +3568,8 @@ async function loadObservations() {
|
|
|
3314
3568
|
<p class="page-subtitle">${allObservations.length} ${t('observationsStored')}</p>
|
|
3315
3569
|
</div>
|
|
3316
3570
|
<div style="display:flex;gap:8px;">
|
|
3317
|
-
<button class="export-btn" id="btn-batch-
|
|
3318
|
-
|
|
3571
|
+
<button class="export-btn" id="btn-batch-select" title="${t('selectRecords')}">
|
|
3572
|
+
<span class="iconify" data-icon="lucide:list-checks"></span> ${t('selectRecords')}
|
|
3319
3573
|
</button>
|
|
3320
3574
|
<button class="export-btn" id="btn-export" title="${t('exportData')}">
|
|
3321
3575
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
|
|
@@ -3324,6 +3578,18 @@ async function loadObservations() {
|
|
|
3324
3578
|
</div>
|
|
3325
3579
|
</div>
|
|
3326
3580
|
|
|
3581
|
+
<section class="maintenance-strip" aria-labelledby="memory-maintenance-title">
|
|
3582
|
+
<div class="maintenance-strip-heading">
|
|
3583
|
+
<span id="memory-maintenance-title">${t('memoryMaintenance')}</span>
|
|
3584
|
+
<small>${t('previewRequired')}</small>
|
|
3585
|
+
</div>
|
|
3586
|
+
<div class="maintenance-actions">
|
|
3587
|
+
<button type="button" onclick="openMaintenancePreview('cleanup')"><span class="iconify" data-icon="lucide:eraser"></span>${t('cleanupAction')}</button>
|
|
3588
|
+
<button type="button" onclick="openMaintenancePreview('consolidate')"><span class="iconify" data-icon="lucide:combine"></span>${t('consolidateAction')}</button>
|
|
3589
|
+
<button type="button" onclick="openMaintenancePreview('deduplicate')"><span class="iconify" data-icon="lucide:copy-minus"></span>${t('deduplicateAction')}</button>
|
|
3590
|
+
</div>
|
|
3591
|
+
</section>
|
|
3592
|
+
|
|
3327
3593
|
<div id="batch-toolbar-slot"></div>
|
|
3328
3594
|
|
|
3329
3595
|
<div class="search-bar">
|
|
@@ -3341,18 +3607,10 @@ async function loadObservations() {
|
|
|
3341
3607
|
window.open(`/api/export${sep}`, '_blank');
|
|
3342
3608
|
});
|
|
3343
3609
|
|
|
3344
|
-
//
|
|
3345
|
-
document.getElementById('btn-batch-
|
|
3610
|
+
// Manual selection remains separate from the shared cleanup pipeline.
|
|
3611
|
+
document.getElementById('btn-batch-select').addEventListener('click', () => {
|
|
3346
3612
|
batchMode = !batchMode;
|
|
3347
|
-
|
|
3348
|
-
// Auto-select low quality ones
|
|
3349
|
-
selectedIds.clear();
|
|
3350
|
-
allObservations.forEach(obs => {
|
|
3351
|
-
if (isLowQualityObs(obs.title || '')) selectedIds.add(obs.id);
|
|
3352
|
-
});
|
|
3353
|
-
} else {
|
|
3354
|
-
selectedIds.clear();
|
|
3355
|
-
}
|
|
3613
|
+
selectedIds.clear();
|
|
3356
3614
|
renderObsList();
|
|
3357
3615
|
renderBatchToolbar();
|
|
3358
3616
|
});
|
|
@@ -3405,18 +3663,16 @@ function renderObsList() {
|
|
|
3405
3663
|
}
|
|
3406
3664
|
|
|
3407
3665
|
list.innerHTML = filtered.map(obs => {
|
|
3408
|
-
const isLow = isLowQualityObs(obs.title || '');
|
|
3409
3666
|
const isSelected = selectedIds.has(obs.id);
|
|
3410
3667
|
const hl = (text) => obsFilter ? escapeHtml(text).replace(new RegExp(`(${escapeHtml(obsFilter).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>') : escapeHtml(text);
|
|
3411
3668
|
return `
|
|
3412
|
-
<div class="obs-card
|
|
3669
|
+
<div class="obs-card" data-obs-id="${obs.id}" onclick="toggleObsDetail(${obs.id})" style="cursor:pointer;">
|
|
3413
3670
|
<div class="obs-card-header">
|
|
3414
3671
|
${batchMode ? `<input type="checkbox" class="obs-checkbox" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); toggleObsSelect(${obs.id});" />` : ''}
|
|
3415
3672
|
<span class="obs-card-id">#${obs.id}</span>
|
|
3416
3673
|
<span class="type-badge" data-type="${obs.type || 'unknown'}">
|
|
3417
3674
|
${typeIcons[obs.type] || '[UNKNOWN]'} ${obs.type || t('unknown')}
|
|
3418
3675
|
</span>
|
|
3419
|
-
${isLow ? '<span class="low-quality-badge">' + t('lowQuality') + '</span>' : ''}
|
|
3420
3676
|
<span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
|
|
3421
3677
|
<span class="obs-expand-icon">▼</span>
|
|
3422
3678
|
</div>
|
|
@@ -3461,9 +3717,14 @@ async function loadRetention() {
|
|
|
3461
3717
|
const { summary, items } = data;
|
|
3462
3718
|
|
|
3463
3719
|
container.innerHTML = `
|
|
3464
|
-
<div class="page-header">
|
|
3465
|
-
<
|
|
3466
|
-
|
|
3720
|
+
<div class="page-header page-header-actions">
|
|
3721
|
+
<div>
|
|
3722
|
+
<h1 class="page-title">${t('memoryRetention')}</h1>
|
|
3723
|
+
<p class="page-subtitle">${t('retentionSubtitle')}</p>
|
|
3724
|
+
</div>
|
|
3725
|
+
<button class="export-btn" type="button" onclick="openMaintenancePreview('retention')" ${summary.archive > 0 ? '' : 'disabled'}>
|
|
3726
|
+
<span class="iconify" data-icon="lucide:archive"></span>${t('archiveAction')}
|
|
3727
|
+
</button>
|
|
3467
3728
|
</div>
|
|
3468
3729
|
|
|
3469
3730
|
<div class="retention-summary">
|
|
@@ -182,6 +182,23 @@
|
|
|
182
182
|
<div id="page-team" class="page"></div>
|
|
183
183
|
</main>
|
|
184
184
|
|
|
185
|
+
<dialog id="maintenance-dialog" class="maintenance-dialog" aria-labelledby="maintenance-dialog-title">
|
|
186
|
+
<div class="maintenance-dialog-shell">
|
|
187
|
+
<header class="maintenance-dialog-header">
|
|
188
|
+
<div>
|
|
189
|
+
<div class="maintenance-dialog-kicker" id="maintenance-dialog-kicker"></div>
|
|
190
|
+
<h2 id="maintenance-dialog-title"></h2>
|
|
191
|
+
</div>
|
|
192
|
+
<button class="icon-btn maintenance-dialog-close" type="button" aria-label="Close" title="Close">
|
|
193
|
+
<span class="iconify" data-icon="lucide:x"></span>
|
|
194
|
+
</button>
|
|
195
|
+
</header>
|
|
196
|
+
<div id="maintenance-dialog-body" class="maintenance-dialog-body"></div>
|
|
197
|
+
<footer id="maintenance-dialog-footer" class="maintenance-dialog-footer"></footer>
|
|
198
|
+
</div>
|
|
199
|
+
</dialog>
|
|
200
|
+
<div id="dashboard-toast" class="dashboard-toast" role="status" aria-live="polite"></div>
|
|
201
|
+
|
|
185
202
|
<!-- Apache ECharts 5 — Knowledge Graph renderer -->
|
|
186
203
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
|
187
204
|
<!-- Cytoscape.js + Dagre — legacy entity-graph fallback -->
|