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.
Files changed (40) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +372 -74
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/hooks/post-tool-use.js +6 -0
  5. package/dist/hooks/post-tool-use.js.map +2 -2
  6. package/dist/hooks/session-end.js +6 -0
  7. package/dist/hooks/session-end.js.map +2 -2
  8. package/dist/hooks/session-start.js +29 -13
  9. package/dist/hooks/session-start.js.map +2 -2
  10. package/dist/hooks/stop.js +6 -0
  11. package/dist/hooks/stop.js.map +2 -2
  12. package/dist/hooks/user-prompt-submit.js +245 -31
  13. package/dist/hooks/user-prompt-submit.js.map +3 -3
  14. package/dist/server/api/index.js +329 -31
  15. package/dist/server/api/index.js.map +3 -3
  16. package/dist/server/index.js +336 -38
  17. package/dist/server/index.js.map +3 -3
  18. package/dist/services/memory-service.js +6 -0
  19. package/dist/services/memory-service.js.map +2 -2
  20. package/dist/ui/app.js +236 -4
  21. package/dist/ui/index.html +51 -0
  22. package/dist/ui/style.css +34 -0
  23. package/memory/_index.md +4 -0
  24. package/memory/agent_response/uncategorized/2026-02-26.md +151 -1
  25. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  26. package/memory/session_summary/uncategorized/2026-02-26.md +13 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-02-26.md +9 -1
  29. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  30. package/memory/user_prompt/uncategorized/2026-02-26.md +9 -0
  31. package/package.json +3 -2
  32. package/scripts/delete-unknown-projects.js +154 -0
  33. package/src/hooks/session-start.ts +9 -3
  34. package/src/hooks/user-prompt-submit.ts +225 -29
  35. package/src/server/api/events.ts +1 -0
  36. package/src/server/api/stats.ts +346 -0
  37. package/src/services/memory-service.ts +7 -0
  38. package/src/ui/app.js +236 -4
  39. package/src/ui/index.html +51 -0
  40. 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
- <span class="event-type-badge ${typeClass}">${eventType}</span>
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>
@@ -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` (빌드 아티팩트 및 로컬 파일)