claude-memory-layer 1.0.17 → 1.0.19
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/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +372 -74
- package/dist/cli/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +6 -0
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +6 -0
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +29 -13
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +6 -0
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +245 -31
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +329 -31
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +336 -38
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +6 -0
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +236 -4
- package/dist/ui/index.html +51 -0
- package/dist/ui/style.css +34 -0
- package/memory/_index.md +4 -0
- package/memory/agent_response/uncategorized/2026-02-26.md +151 -1
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-02-26.md +13 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-02-26.md +9 -1
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/memory/user_prompt/uncategorized/2026-02-26.md +9 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/hooks/session-start.ts +9 -3
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +1 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +7 -0
- package/src/ui/app.js +236 -4
- package/src/ui/index.html +51 -0
- package/src/ui/style.css +34 -0
package/dist/ui/app.js
CHANGED
|
@@ -12,6 +12,8 @@ const state = {
|
|
|
12
12
|
mostAccessed: null,
|
|
13
13
|
helpfulness: null,
|
|
14
14
|
retrievalTraces: null,
|
|
15
|
+
adherenceSummary: null,
|
|
16
|
+
adherenceWindow: '24h',
|
|
15
17
|
currentLevel: 'L0',
|
|
16
18
|
currentSort: 'recent',
|
|
17
19
|
currentView: 'overview',
|
|
@@ -20,6 +22,9 @@ const state = {
|
|
|
20
22
|
events: [],
|
|
21
23
|
isLoading: false,
|
|
22
24
|
chartInstance: null,
|
|
25
|
+
kpiChartInstance: null,
|
|
26
|
+
kpiWindow: '7d',
|
|
27
|
+
kpi: null,
|
|
23
28
|
chatMessages: [],
|
|
24
29
|
isChatOpen: false,
|
|
25
30
|
isChatStreaming: false,
|
|
@@ -102,13 +107,42 @@ function setupEventListeners() {
|
|
|
102
107
|
});
|
|
103
108
|
|
|
104
109
|
// Sort buttons
|
|
105
|
-
document.querySelectorAll('.sort-btn').forEach(btn => {
|
|
110
|
+
document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
|
|
106
111
|
btn.addEventListener('click', (e) => {
|
|
107
112
|
const sort = e.currentTarget.dataset.sort;
|
|
108
113
|
if (sort) selectSort(sort);
|
|
109
114
|
});
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
// Adherence window controls
|
|
118
|
+
document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
|
|
119
|
+
btn.addEventListener('click', async (e) => {
|
|
120
|
+
const window = e.currentTarget.dataset.adhWindow;
|
|
121
|
+
if (!window || state.adherenceWindow === window) return;
|
|
122
|
+
state.adherenceWindow = window;
|
|
123
|
+
document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
|
|
124
|
+
b.classList.toggle('active', b.dataset.adhWindow === window);
|
|
125
|
+
});
|
|
126
|
+
state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
|
|
127
|
+
updateAdherenceSummaryUI();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// KPI window controls
|
|
132
|
+
document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
|
|
133
|
+
btn.addEventListener('click', async (e) => {
|
|
134
|
+
const window = e.currentTarget.dataset.kpiWindow;
|
|
135
|
+
if (!window || state.kpiWindow === window) return;
|
|
136
|
+
state.kpiWindow = window;
|
|
137
|
+
document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
|
|
138
|
+
b.classList.toggle('active', b.dataset.kpiWindow === window);
|
|
139
|
+
});
|
|
140
|
+
await loadKpiData();
|
|
141
|
+
updateKpiCardsUI();
|
|
142
|
+
renderKpiTrendChart();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
112
146
|
// Search
|
|
113
147
|
const searchInput = document.getElementById('search-input');
|
|
114
148
|
if (searchInput) {
|
|
@@ -125,6 +159,10 @@ function setupEventListeners() {
|
|
|
125
159
|
state.chartInstance.destroy();
|
|
126
160
|
state.chartInstance = null;
|
|
127
161
|
}
|
|
162
|
+
if (state.kpiChartInstance) {
|
|
163
|
+
state.kpiChartInstance.destroy();
|
|
164
|
+
state.kpiChartInstance = null;
|
|
165
|
+
}
|
|
128
166
|
await initActivityChart();
|
|
129
167
|
// Reload current view if not overview
|
|
130
168
|
if (state.currentView !== 'overview') {
|
|
@@ -231,17 +269,24 @@ function setupEventListeners() {
|
|
|
231
269
|
|
|
232
270
|
// --- Data Fetching ---
|
|
233
271
|
|
|
272
|
+
async function loadKpiData() {
|
|
273
|
+
state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
|
|
274
|
+
.then(r => r.json())
|
|
275
|
+
.catch(() => null);
|
|
276
|
+
}
|
|
277
|
+
|
|
234
278
|
async function refreshData() {
|
|
235
279
|
const btn = document.getElementById('refresh-btn');
|
|
236
280
|
if(btn) btn.classList.add('loading');
|
|
237
281
|
|
|
238
282
|
try {
|
|
239
|
-
const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
|
|
283
|
+
const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
|
|
240
284
|
fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
|
|
241
285
|
fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
|
|
242
286
|
fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
|
|
243
287
|
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
|
|
244
|
-
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
|
|
288
|
+
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
|
|
289
|
+
fetchAdherenceSummary().catch(() => null)
|
|
245
290
|
]);
|
|
246
291
|
|
|
247
292
|
state.stats = stats;
|
|
@@ -249,10 +294,15 @@ async function refreshData() {
|
|
|
249
294
|
state.mostAccessed = mostAccessed;
|
|
250
295
|
state.helpfulness = helpfulness;
|
|
251
296
|
state.retrievalTraces = retrievalTraces;
|
|
297
|
+
state.adherenceSummary = adherenceSummary;
|
|
298
|
+
|
|
299
|
+
await loadKpiData();
|
|
252
300
|
|
|
253
301
|
updateStatsUI();
|
|
254
302
|
updateSharedUI();
|
|
255
303
|
updateMemoryUsageUI();
|
|
304
|
+
updateKpiCardsUI();
|
|
305
|
+
renderKpiTrendChart();
|
|
256
306
|
await loadLevelEvents(state.currentLevel);
|
|
257
307
|
|
|
258
308
|
checkEndlessStatus();
|
|
@@ -327,6 +377,98 @@ function updateSharedUI() {
|
|
|
327
377
|
document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
|
|
328
378
|
}
|
|
329
379
|
|
|
380
|
+
function percentText(v) {
|
|
381
|
+
return `${((v || 0) * 100).toFixed(1)}%`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
|
|
385
|
+
const el = document.getElementById(id);
|
|
386
|
+
if (!el) return;
|
|
387
|
+
const v = Number(value || 0);
|
|
388
|
+
const sign = v > 0 ? '+' : '';
|
|
389
|
+
const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
|
|
390
|
+
|
|
391
|
+
let positive = v > 0;
|
|
392
|
+
if (lowerIsBetter) positive = v < 0;
|
|
393
|
+
const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
|
|
394
|
+
const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
|
|
395
|
+
|
|
396
|
+
el.className = `kpi-delta ${cls}`;
|
|
397
|
+
el.textContent = `${arrow} ${text} vs prev`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function updateKpiCardsUI() {
|
|
401
|
+
const m = state.kpi?.metrics;
|
|
402
|
+
const d = state.kpi?.deltas;
|
|
403
|
+
if (!m) return;
|
|
404
|
+
const set = (id, value) => {
|
|
405
|
+
const el = document.getElementById(id);
|
|
406
|
+
if (el) el.textContent = value;
|
|
407
|
+
};
|
|
408
|
+
set('kpi-useful-recall', percentText(m.usefulRecallRate));
|
|
409
|
+
set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
|
|
410
|
+
set('kpi-rework-rate', percentText(m.reworkRate));
|
|
411
|
+
set('kpi-failure-rate', percentText(m.postChangeFailureRate));
|
|
412
|
+
|
|
413
|
+
if (d) {
|
|
414
|
+
renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
|
|
415
|
+
renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
|
|
416
|
+
renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
|
|
417
|
+
renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const alertsEl = document.getElementById('kpi-alerts');
|
|
421
|
+
if (alertsEl) {
|
|
422
|
+
const alerts = state.kpi?.alerts || [];
|
|
423
|
+
if (alerts.length === 0) {
|
|
424
|
+
alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
|
|
425
|
+
} else {
|
|
426
|
+
alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderKpiTrendChart() {
|
|
432
|
+
const chartEl = document.querySelector('#kpi-trend-chart');
|
|
433
|
+
if (!chartEl) return;
|
|
434
|
+
|
|
435
|
+
const daily = state.kpi?.trend?.daily || [];
|
|
436
|
+
const categories = daily.map(d => d.date);
|
|
437
|
+
const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
|
|
438
|
+
const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
|
|
439
|
+
const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
|
|
440
|
+
|
|
441
|
+
if (state.kpiChartInstance) {
|
|
442
|
+
state.kpiChartInstance.destroy();
|
|
443
|
+
state.kpiChartInstance = null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const options = {
|
|
447
|
+
series: [
|
|
448
|
+
{ name: 'Useful Recall %', data: useful },
|
|
449
|
+
{ name: 'Rework %', data: rework },
|
|
450
|
+
{ name: 'Failure %', data: fail }
|
|
451
|
+
],
|
|
452
|
+
chart: {
|
|
453
|
+
type: 'line',
|
|
454
|
+
height: 240,
|
|
455
|
+
background: 'transparent',
|
|
456
|
+
toolbar: { show: false },
|
|
457
|
+
fontFamily: 'Outfit, sans-serif'
|
|
458
|
+
},
|
|
459
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
460
|
+
dataLabels: { enabled: false },
|
|
461
|
+
xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
|
|
462
|
+
yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
|
|
463
|
+
theme: { mode: 'dark' },
|
|
464
|
+
grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
|
|
465
|
+
colors: ['#34D399', '#FEB019', '#FF4560']
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
state.kpiChartInstance = new ApexCharts(chartEl, options);
|
|
469
|
+
state.kpiChartInstance.render();
|
|
470
|
+
}
|
|
471
|
+
|
|
330
472
|
function selectLevel(level) {
|
|
331
473
|
state.currentLevel = level;
|
|
332
474
|
|
|
@@ -347,6 +489,23 @@ function selectSort(sort) {
|
|
|
347
489
|
loadLevelEvents(state.currentLevel, sort);
|
|
348
490
|
}
|
|
349
491
|
|
|
492
|
+
function getAdherenceInfo(event) {
|
|
493
|
+
const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
|
|
494
|
+
if (!adherence || typeof adherence !== 'object') return null;
|
|
495
|
+
const reason = adherence.reason || 'unknown';
|
|
496
|
+
const checked = Boolean(adherence.checked);
|
|
497
|
+
const turn = adherence.turn;
|
|
498
|
+
return { reason, checked, turn };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderAdherenceBadge(event) {
|
|
502
|
+
const info = getAdherenceInfo(event);
|
|
503
|
+
if (!info) return '';
|
|
504
|
+
const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
|
|
505
|
+
const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
|
|
506
|
+
return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
|
|
507
|
+
}
|
|
508
|
+
|
|
350
509
|
function updateEventsListUI() {
|
|
351
510
|
const container = document.getElementById('event-list-container');
|
|
352
511
|
container.innerHTML = '';
|
|
@@ -374,13 +533,17 @@ function updateEventsListUI() {
|
|
|
374
533
|
const accessBadge = event.accessCount > 0
|
|
375
534
|
? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
|
|
376
535
|
: '';
|
|
536
|
+
const adherenceBadge = renderAdherenceBadge(event);
|
|
377
537
|
const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
|
|
378
538
|
? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
|
|
379
539
|
: '';
|
|
380
540
|
|
|
381
541
|
el.innerHTML = `
|
|
382
542
|
<div class="event-header">
|
|
383
|
-
<
|
|
543
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
544
|
+
<span class="event-type-badge ${typeClass}">${eventType}</span>
|
|
545
|
+
${adherenceBadge}
|
|
546
|
+
</div>
|
|
384
547
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
385
548
|
${accessBadge}
|
|
386
549
|
${lastUsed}
|
|
@@ -400,9 +563,72 @@ function updateMemoryUsageUI() {
|
|
|
400
563
|
updateGraduationBars();
|
|
401
564
|
updateHelpfulnessUI();
|
|
402
565
|
updateMostHelpfulList();
|
|
566
|
+
updateAdherenceSummaryUI();
|
|
403
567
|
updateRetrievalTraceUI();
|
|
404
568
|
}
|
|
405
569
|
|
|
570
|
+
function adherenceWindowToMs(window) {
|
|
571
|
+
if (window === '24h') return 24 * 60 * 60 * 1000;
|
|
572
|
+
if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
|
|
573
|
+
if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
|
|
574
|
+
return 24 * 60 * 60 * 1000;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function fetchAdherenceSummary() {
|
|
578
|
+
const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
|
|
579
|
+
if (!res.ok) return null;
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
const events = data.events || [];
|
|
582
|
+
|
|
583
|
+
const counts = {};
|
|
584
|
+
let checked = 0;
|
|
585
|
+
let skipped = 0;
|
|
586
|
+
let total = 0;
|
|
587
|
+
|
|
588
|
+
const now = Date.now();
|
|
589
|
+
const windowMs = adherenceWindowToMs(state.adherenceWindow);
|
|
590
|
+
|
|
591
|
+
for (const e of events) {
|
|
592
|
+
const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
|
|
593
|
+
if (!ts || now - ts > windowMs) continue;
|
|
594
|
+
|
|
595
|
+
const adherence = e?.metadata?.adherence || e?.meta?.adherence;
|
|
596
|
+
if (!adherence) continue;
|
|
597
|
+
total++;
|
|
598
|
+
const reason = adherence.reason || 'unknown';
|
|
599
|
+
counts[reason] = (counts[reason] || 0) + 1;
|
|
600
|
+
if (adherence.checked) checked++; else skipped++;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return { total, checked, skipped, counts, window: state.adherenceWindow };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function updateAdherenceSummaryUI() {
|
|
607
|
+
const el = document.getElementById('adherence-summary');
|
|
608
|
+
if (!el) return;
|
|
609
|
+
|
|
610
|
+
const s = state.adherenceSummary;
|
|
611
|
+
if (!s || !s.total) {
|
|
612
|
+
el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const top = Object.entries(s.counts || {})
|
|
617
|
+
.sort((a, b) => b[1] - a[1])
|
|
618
|
+
.slice(0, 5)
|
|
619
|
+
.map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
|
|
620
|
+
.join('');
|
|
621
|
+
|
|
622
|
+
el.innerHTML = `
|
|
623
|
+
<div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
|
|
624
|
+
<span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
|
|
625
|
+
<span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
|
|
626
|
+
<span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
|
|
627
|
+
</div>
|
|
628
|
+
<div>${top}</div>
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
|
|
406
632
|
function updateGraduationBars() {
|
|
407
633
|
const container = document.getElementById('graduation-bars');
|
|
408
634
|
if (!container || !state.stats?.levelStats) return;
|
|
@@ -691,6 +917,7 @@ async function openDetailModal(eventId) {
|
|
|
691
917
|
const eventType = evt.eventType || 'unknown';
|
|
692
918
|
const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
|
|
693
919
|
const time = new Date(evt.timestamp).toLocaleString();
|
|
920
|
+
const adherenceBadge = renderAdherenceBadge(evt);
|
|
694
921
|
|
|
695
922
|
let contextHtml = '';
|
|
696
923
|
if (ctx.length > 0) {
|
|
@@ -716,6 +943,7 @@ async function openDetailModal(eventId) {
|
|
|
716
943
|
<i class="ri-price-tag-3-line"></i>
|
|
717
944
|
<span class="event-type-badge ${typeClass}">${eventType}</span>
|
|
718
945
|
</div>
|
|
946
|
+
${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
|
|
719
947
|
<div class="modal-meta-item">
|
|
720
948
|
<i class="ri-time-line"></i>
|
|
721
949
|
${time}
|
|
@@ -763,11 +991,13 @@ async function showEventsListModal() {
|
|
|
763
991
|
|
|
764
992
|
body.innerHTML = events.map(e => {
|
|
765
993
|
const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
|
|
994
|
+
const adherenceBadge = renderAdherenceBadge(e);
|
|
766
995
|
return `
|
|
767
996
|
<div class="modal-list-item" onclick="openDetailModal('${e.id}')">
|
|
768
997
|
<div class="modal-list-info">
|
|
769
998
|
<div class="title">
|
|
770
999
|
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
1000
|
+
${adherenceBadge}
|
|
771
1001
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
772
1002
|
</div>
|
|
773
1003
|
<div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
|
|
@@ -847,11 +1077,13 @@ async function showSessionDetailInModal(sessionId) {
|
|
|
847
1077
|
<div class="modal-section-title">Events</div>
|
|
848
1078
|
${events.map(e => {
|
|
849
1079
|
const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
|
|
1080
|
+
const adherenceBadge = renderAdherenceBadge(e);
|
|
850
1081
|
return `
|
|
851
1082
|
<div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
|
|
852
1083
|
<div class="modal-list-info">
|
|
853
1084
|
<div class="title">
|
|
854
1085
|
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
1086
|
+
${adherenceBadge}
|
|
855
1087
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
856
1088
|
</div>
|
|
857
1089
|
<div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
|
package/dist/ui/index.html
CHANGED
|
@@ -120,6 +120,45 @@
|
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
122
122
|
|
|
123
|
+
<!-- KPI Cards -->
|
|
124
|
+
<div class="stats-grid kpi-grid" style="margin-top:-4px;">
|
|
125
|
+
<div class="stat-card kpi-card">
|
|
126
|
+
<div class="stat-value" id="kpi-useful-recall">-</div>
|
|
127
|
+
<div class="kpi-delta" id="kpi-useful-recall-delta">-</div>
|
|
128
|
+
<div class="stat-label"><i class="ri-thumb-up-line"></i> Useful Recall Rate</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="stat-card kpi-card">
|
|
131
|
+
<div class="stat-value" id="kpi-completion-turns">-</div>
|
|
132
|
+
<div class="kpi-delta" id="kpi-completion-turns-delta">-</div>
|
|
133
|
+
<div class="stat-label"><i class="ri-repeat-line"></i> Avg Completion Turns</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="stat-card kpi-card">
|
|
136
|
+
<div class="stat-value" id="kpi-rework-rate">-</div>
|
|
137
|
+
<div class="kpi-delta" id="kpi-rework-rate-delta">-</div>
|
|
138
|
+
<div class="stat-label"><i class="ri-hammer-line"></i> Rework Rate</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="stat-card kpi-card">
|
|
141
|
+
<div class="stat-value" id="kpi-failure-rate">-</div>
|
|
142
|
+
<div class="kpi-delta" id="kpi-failure-rate-delta">-</div>
|
|
143
|
+
<div class="stat-label"><i class="ri-error-warning-line"></i> Post-change Failure</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="card" style="margin-bottom:24px;">
|
|
148
|
+
<div class="card-header">
|
|
149
|
+
<div class="card-title">
|
|
150
|
+
<i class="ri-line-chart-line"></i>
|
|
151
|
+
<span>KPI Trend</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div style="display:flex; gap:8px;">
|
|
154
|
+
<button class="sort-btn active" data-kpi-window="7d">7d</button>
|
|
155
|
+
<button class="sort-btn" data-kpi-window="30d">30d</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="kpi-alerts" style="font-size:12px; color:var(--text-muted); margin-bottom:10px;">Loading...</div>
|
|
159
|
+
<div id="kpi-trend-chart" style="height:240px;"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
123
162
|
<!-- Main Grid -->
|
|
124
163
|
<div class="dashboard-grid">
|
|
125
164
|
|
|
@@ -272,6 +311,18 @@
|
|
|
272
311
|
</div>
|
|
273
312
|
</div>
|
|
274
313
|
|
|
314
|
+
<div style="margin-top:20px;">
|
|
315
|
+
<div class="section-label" style="display:flex; justify-content:space-between; align-items:center;">
|
|
316
|
+
<span>Adherence Reasons</span>
|
|
317
|
+
<span id="adherence-window-controls" style="display:flex; gap:6px;">
|
|
318
|
+
<button class="sort-btn active" data-adh-window="24h">24h</button>
|
|
319
|
+
<button class="sort-btn" data-adh-window="7d">7d</button>
|
|
320
|
+
<button class="sort-btn" data-adh-window="30d">30d</button>
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
275
326
|
<div style="margin-top:20px;">
|
|
276
327
|
<div class="section-label">Retrieval Trace (1:1)</div>
|
|
277
328
|
<div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
package/dist/ui/style.css
CHANGED
|
@@ -329,6 +329,23 @@ body {
|
|
|
329
329
|
opacity: 0.5;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
.kpi-grid .stat-card .stat-value {
|
|
333
|
+
font-size: 28px;
|
|
334
|
+
}
|
|
335
|
+
.kpi-grid .stat-card .stat-label {
|
|
336
|
+
font-size: 12px;
|
|
337
|
+
}
|
|
338
|
+
.kpi-delta {
|
|
339
|
+
font-size: 11px;
|
|
340
|
+
margin-top: -4px;
|
|
341
|
+
margin-bottom: 8px;
|
|
342
|
+
color: var(--text-muted);
|
|
343
|
+
font-weight: 600;
|
|
344
|
+
}
|
|
345
|
+
.kpi-delta.good { color: var(--success); }
|
|
346
|
+
.kpi-delta.bad { color: var(--error); }
|
|
347
|
+
.kpi-delta.neutral { color: var(--text-muted); }
|
|
348
|
+
|
|
332
349
|
.stat-value {
|
|
333
350
|
font-size: 36px;
|
|
334
351
|
font-weight: 700;
|
|
@@ -594,6 +611,23 @@ body {
|
|
|
594
611
|
font-weight: 600;
|
|
595
612
|
}
|
|
596
613
|
|
|
614
|
+
.adherence-badge {
|
|
615
|
+
font-size: 10px;
|
|
616
|
+
padding: 3px 7px;
|
|
617
|
+
border-radius: 6px;
|
|
618
|
+
font-weight: 700;
|
|
619
|
+
text-transform: uppercase;
|
|
620
|
+
letter-spacing: 0.2px;
|
|
621
|
+
}
|
|
622
|
+
.adherence-checked {
|
|
623
|
+
background: rgba(52, 211, 153, 0.14);
|
|
624
|
+
color: #34D399;
|
|
625
|
+
}
|
|
626
|
+
.adherence-skipped {
|
|
627
|
+
background: rgba(148, 163, 184, 0.14);
|
|
628
|
+
color: #94A3B8;
|
|
629
|
+
}
|
|
630
|
+
|
|
597
631
|
/* Section Label */
|
|
598
632
|
.section-label {
|
|
599
633
|
font-size: 12px;
|
package/memory/_index.md
CHANGED
|
@@ -4,6 +4,7 @@ Generated automatically by MarkdownMirror.
|
|
|
4
4
|
|
|
5
5
|
- memory/.claude-plugin/commands/2026-02-25.md
|
|
6
6
|
- memory/agent_response/uncategorized/2026-02-26.md
|
|
7
|
+
- memory/agent_response/uncategorized/2026-03-03.md
|
|
7
8
|
- memory/default/uncategorized/2026-02-25.md
|
|
8
9
|
- memory/node_modules/abbrev/2026-02-25.md
|
|
9
10
|
- memory/node_modules/abort-controller/2026-02-25.md
|
|
@@ -392,6 +393,8 @@ Generated automatically by MarkdownMirror.
|
|
|
392
393
|
- memory/node_modules/yallist/2026-02-25.md
|
|
393
394
|
- memory/node_modules/yocto-queue/2026-02-25.md
|
|
394
395
|
- memory/node_modules/zod/2026-02-25.md
|
|
396
|
+
- memory/session_summary/uncategorized/2026-02-26.md
|
|
397
|
+
- memory/session_summary/uncategorized/2026-03-03.md
|
|
395
398
|
- memory/specs/20260207-dashboard-upgrade/2026-02-25.md
|
|
396
399
|
- memory/specs/citations-system/2026-02-25.md
|
|
397
400
|
- memory/specs/endless-mode/2026-02-25.md
|
|
@@ -405,4 +408,5 @@ Generated automatically by MarkdownMirror.
|
|
|
405
408
|
- memory/specs/vector-outbox-v2/2026-02-25.md
|
|
406
409
|
- memory/specs/web-viewer-ui/2026-02-25.md
|
|
407
410
|
- memory/tool_observation/uncategorized/2026-02-26.md
|
|
411
|
+
- memory/tool_observation/uncategorized/2026-03-03.md
|
|
408
412
|
- memory/user_prompt/uncategorized/2026-02-26.md
|
|
@@ -23,4 +23,154 @@
|
|
|
23
23
|
- "챗봇이 제공할 수 없는 기능" 섹션 신규 추가 (재고, 쿠폰, 주문, 배송, 교환/반품, A/S, 회원, 색상/사이즈 옵션)
|
|
24
24
|
- 챗봇이 이 상품에 대해 할 수 있는 것 명시 (설명서 기반 정보, 리뷰 분석, 방송 일정)
|
|
25
25
|
|
|
26
|
-
이제 "각 색상·사이즈별 재고 가능 여부 확인해줘" 같은 제안이 생성되지 않습니다.
|
|
26
|
+
이제 "각 색상·사이즈별 재고 가능 여부 확인해줘" 같은 제안이 생성되지 않습니다.
|
|
27
|
+
## 2026-02-26T13:53:54.660Z | 798fb69b-1aed-469f-96e3-167b90a5e186
|
|
28
|
+
- type: agent_response
|
|
29
|
+
- session: bench-session-2
|
|
30
|
+
문제를 파악했습니다. `retrieval_traces` 테이블은 `sqlite-event-store.ts`에서 `CREATE TABLE IF NOT EXISTS`로 정의되어 있지만, API 엔드포인트에서 사용하는 DB 인스턴스가 해당 테이블을 초기화하지 못하고 있는 것 같습니다.
|
|
31
|
+
|
|
32
|
+
좀 더 정확한 원인을 확인하겠습니다.
|
|
33
|
+
## 2026-02-26T13:53:54.688Z | 8519d875-d5db-4b5c-8c6b-0542fbadf52c
|
|
34
|
+
- type: agent_response
|
|
35
|
+
- session: bench-session-2
|
|
36
|
+
원인을 찾았습니다. `sqlite-event-store.ts:56-60`에서:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// In read-only mode, skip schema creation
|
|
40
|
+
if (this.readOnly) {
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
API 대시보드는 `readOnly: true`로 서비스를 생성하므로 스키마 생성을 건너뜁니다. 만약 DB가 `retrieval_traces` 테이블이 추가되기 전에 생성되었다면, read-only 모드에서는 테이블을 만들 수 없어서 에러가 발생합니다.
|
|
47
|
+
|
|
48
|
+
`getRecentRetrievalTraces`와 `getRetrievalTraceStats`에서 테이블 미존재 시 빈 결과를 반환하도록 수정하겠습니다.
|
|
49
|
+
## 2026-02-26T13:53:54.697Z | cc73dce3-e4db-44f1-8a44-e6e1156f5c24
|
|
50
|
+
- type: agent_response
|
|
51
|
+
- session: bench-session-2
|
|
52
|
+
수정 완료했습니다. 빌드해서 확인하겠습니다.
|
|
53
|
+
## 2026-02-26T13:53:54.706Z | 66619324-dfef-4d55-bad6-406528e4cefa
|
|
54
|
+
- type: agent_response
|
|
55
|
+
- session: bench-session-2
|
|
56
|
+
빌드 성공했습니다.
|
|
57
|
+
|
|
58
|
+
**원인**: `getServiceFromQuery()`가 `readOnly: true`로 `MemoryService`를 생성 → `initialize()`에서 read-only 모드일 때 스키마 생성을 건너뜀 → 이전 버전 DB에 `retrieval_traces` 테이블이 없으면 쿼리 시 에러 발생
|
|
59
|
+
|
|
60
|
+
**수정 내용** (`sqlite-event-store.ts`):
|
|
61
|
+
- `getRecentRetrievalTraces()`: `no such table` 에러 시 빈 배열 `[]` 반환
|
|
62
|
+
- `getRetrievalTraceStats()`: `no such table` 에러 시 기본 빈 stats 객체 반환
|
|
63
|
+
- 다른 에러는 기존대로 throw
|
|
64
|
+
## 2026-02-26T13:53:54.714Z | 2ab64c5d-2185-43ab-9077-3907362a96de
|
|
65
|
+
- type: agent_response
|
|
66
|
+
- session: bench-session-2
|
|
67
|
+
`sqlite-event-store.ts:53-60`에서 보면:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
async initialize(): Promise<void> {
|
|
71
|
+
if (this.initialized) return;
|
|
72
|
+
|
|
73
|
+
// In read-only mode, skip schema creation
|
|
74
|
+
if (this.readOnly) {
|
|
75
|
+
this.initialized = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create all tables in a single exec...
|
|
80
|
+
sqliteExec(this.db, `CREATE TABLE IF NOT EXISTS ...`);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**스키마 생성 시점**: `MemoryService`가 **write 모드**로 `initialize()`를 처음 호출할 때입니다.
|
|
85
|
+
|
|
86
|
+
이건 실제로 MCP 플러그인이 Claude Code 세션에서 메모리를 기록할 때 발생합니다 (retrieve/store 호출 시).
|
|
87
|
+
|
|
88
|
+
**문제 상황**:
|
|
89
|
+
- 대시보드 API는 `readOnly: true`로 서비스를 생성 → 스키마 생성 안 함
|
|
90
|
+
- 해당 프로젝트 DB가 `retrieval_traces` 테이블 추가 **이전 버전**에서 만들어졌으면, write 모드 세션이 한 번도 다시 초기화되지 않은 상태
|
|
91
|
+
- 결과적으로 read-only API에서 존재하지 않는 테이블을 쿼리 → 에러
|
|
92
|
+
|
|
93
|
+
**정상 흐름**: 해당 프로젝트에서 Claude Code를 한번 사용하면 write 모드 초기화가 실행되고 `CREATE TABLE IF NOT EXISTS`로 테이블이 생성됩니다. 그 이후엔 대시보드에서도 정상 조회됩니다.
|
|
94
|
+
|
|
95
|
+
방금 수정은 그 사이 간극(테이블 없는 상태)에서 에러 대신 빈 결과를 반환하도록 한 것입니다.
|
|
96
|
+
## 2026-02-26T13:53:54.721Z | f362a4f6-a91c-4f25-89fa-4108e6418340
|
|
97
|
+
- type: agent_response
|
|
98
|
+
- session: bench-session-2
|
|
99
|
+
커밋 완료: `60d59c6`
|
|
100
|
+
|
|
101
|
+
`getRecentRetrievalTraces()`와 `getRetrievalTraceStats()`에서 `no such table` 에러 시 빈 결과를 반환하도록 수정했습니다.
|
|
102
|
+
## 2026-02-26T13:54:46.336Z | 08733485-243f-4a65-a81d-ec962bc9a1d5
|
|
103
|
+
- type: agent_response
|
|
104
|
+
- session: bench-session-3
|
|
105
|
+
문제를 파악했습니다. `retrieval_traces` 테이블은 `sqlite-event-store.ts`에서 `CREATE TABLE IF NOT EXISTS`로 정의되어 있지만, API 엔드포인트에서 사용하는 DB 인스턴스가 해당 테이블을 초기화하지 못하고 있는 것 같습니다.
|
|
106
|
+
|
|
107
|
+
좀 더 정확한 원인을 확인하겠습니다.
|
|
108
|
+
## 2026-02-26T13:54:46.356Z | acc9926e-20d3-477d-8fa0-e67983db0c0c
|
|
109
|
+
- type: agent_response
|
|
110
|
+
- session: bench-session-3
|
|
111
|
+
원인을 찾았습니다. `sqlite-event-store.ts:56-60`에서:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// In read-only mode, skip schema creation
|
|
115
|
+
if (this.readOnly) {
|
|
116
|
+
this.initialized = true;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
API 대시보드는 `readOnly: true`로 서비스를 생성하므로 스키마 생성을 건너뜁니다. 만약 DB가 `retrieval_traces` 테이블이 추가되기 전에 생성되었다면, read-only 모드에서는 테이블을 만들 수 없어서 에러가 발생합니다.
|
|
122
|
+
|
|
123
|
+
`getRecentRetrievalTraces`와 `getRetrievalTraceStats`에서 테이블 미존재 시 빈 결과를 반환하도록 수정하겠습니다.
|
|
124
|
+
## 2026-02-26T13:54:46.365Z | fca49e8b-4b39-48e7-a250-bd6100f96bab
|
|
125
|
+
- type: agent_response
|
|
126
|
+
- session: bench-session-3
|
|
127
|
+
수정 완료했습니다. 빌드해서 확인하겠습니다.
|
|
128
|
+
## 2026-02-26T13:54:46.374Z | 4e471294-7597-4a58-abc9-c220ac92b4bc
|
|
129
|
+
- type: agent_response
|
|
130
|
+
- session: bench-session-3
|
|
131
|
+
빌드 성공했습니다.
|
|
132
|
+
|
|
133
|
+
**원인**: `getServiceFromQuery()`가 `readOnly: true`로 `MemoryService`를 생성 → `initialize()`에서 read-only 모드일 때 스키마 생성을 건너뜀 → 이전 버전 DB에 `retrieval_traces` 테이블이 없으면 쿼리 시 에러 발생
|
|
134
|
+
|
|
135
|
+
**수정 내용** (`sqlite-event-store.ts`):
|
|
136
|
+
- `getRecentRetrievalTraces()`: `no such table` 에러 시 빈 배열 `[]` 반환
|
|
137
|
+
- `getRetrievalTraceStats()`: `no such table` 에러 시 기본 빈 stats 객체 반환
|
|
138
|
+
- 다른 에러는 기존대로 throw
|
|
139
|
+
## 2026-02-26T13:54:46.382Z | a2ece559-3e19-4553-a558-ec809b54b527
|
|
140
|
+
- type: agent_response
|
|
141
|
+
- session: bench-session-3
|
|
142
|
+
`sqlite-event-store.ts:53-60`에서 보면:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
async initialize(): Promise<void> {
|
|
146
|
+
if (this.initialized) return;
|
|
147
|
+
|
|
148
|
+
// In read-only mode, skip schema creation
|
|
149
|
+
if (this.readOnly) {
|
|
150
|
+
this.initialized = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create all tables in a single exec...
|
|
155
|
+
sqliteExec(this.db, `CREATE TABLE IF NOT EXISTS ...`);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**스키마 생성 시점**: `MemoryService`가 **write 모드**로 `initialize()`를 처음 호출할 때입니다.
|
|
160
|
+
|
|
161
|
+
이건 실제로 MCP 플러그인이 Claude Code 세션에서 메모리를 기록할 때 발생합니다 (retrieve/store 호출 시).
|
|
162
|
+
|
|
163
|
+
**문제 상황**:
|
|
164
|
+
- 대시보드 API는 `readOnly: true`로 서비스를 생성 → 스키마 생성 안 함
|
|
165
|
+
- 해당 프로젝트 DB가 `retrieval_traces` 테이블 추가 **이전 버전**에서 만들어졌으면, write 모드 세션이 한 번도 다시 초기화되지 않은 상태
|
|
166
|
+
- 결과적으로 read-only API에서 존재하지 않는 테이블을 쿼리 → 에러
|
|
167
|
+
|
|
168
|
+
**정상 흐름**: 해당 프로젝트에서 Claude Code를 한번 사용하면 write 모드 초기화가 실행되고 `CREATE TABLE IF NOT EXISTS`로 테이블이 생성됩니다. 그 이후엔 대시보드에서도 정상 조회됩니다.
|
|
169
|
+
|
|
170
|
+
방금 수정은 그 사이 간극(테이블 없는 상태)에서 에러 대신 빈 결과를 반환하도록 한 것입니다.
|
|
171
|
+
## 2026-02-26T13:54:46.390Z | e21b392c-87c3-44b0-a355-2fb753804ba6
|
|
172
|
+
- type: agent_response
|
|
173
|
+
- session: bench-session-3
|
|
174
|
+
커밋 완료: `60d59c6`
|
|
175
|
+
|
|
176
|
+
`getRecentRetrievalTraces()`와 `getRetrievalTraceStats()`에서 `no such table` 에러 시 빈 결과를 반환하도록 수정했습니다.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
## 2026-03-03T14:04:37.701Z | 55805b7d-13d2-4e6e-9c1d-bcdf1f1259f9
|
|
3
|
+
- type: agent_response
|
|
4
|
+
- session: 3252b490-16ca-40fb-b30c-1171456e2c89
|
|
5
|
+
커밋 완료했습니다.
|
|
6
|
+
|
|
7
|
+
**커밋 `b37aa13`** - `feat(cli): expand hook coverage to all lifecycle events`
|
|
8
|
+
|
|
9
|
+
변경 내용:
|
|
10
|
+
- `src/cli/index.ts`: `SessionStart`, `Stop`, `SessionEnd` 훅 추가, `makeHook` 헬퍼로 리팩토링, `REQUIRED_HOOK_FILES` 상수 및 `hasHook` 헬퍼 추가, install/uninstall/status 명령 업데이트
|
|
11
|
+
- `scripts/delete-unknown-projects.js`: 세션 레지스트리에 없는 프로젝트 디렉토리 정리 스크립트 (신규)
|
|
12
|
+
- `package.json` / `package-lock.json`: 버전 1.0.18, `ops:projects:clean-unknown` 스크립트 추가
|
|
13
|
+
|
|
14
|
+
제외된 파일 (커밋 불포함): `.npm-cache/`, `bootstrap-kb/`, `claude-memory-layer-1.0.14.tgz`, `d.sh`, `deploy.sh`, `memory/`, `yarn.lock` (빌드 아티팩트 및 로컬 파일)
|