claude-memory-layer 1.0.23 → 1.0.25

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 (58) hide show
  1. package/.claude/settings.local.json +25 -0
  2. package/README.md +2 -0
  3. package/dist/cli/index.js +229 -978
  4. package/dist/cli/index.js.map +4 -4
  5. package/dist/core/index.js +59 -71
  6. package/dist/core/index.js.map +3 -3
  7. package/dist/hooks/post-tool-use.js +287 -976
  8. package/dist/hooks/post-tool-use.js.map +4 -4
  9. package/dist/hooks/semantic-daemon.js +6520 -0
  10. package/dist/hooks/semantic-daemon.js.map +7 -0
  11. package/dist/hooks/session-end.js +209 -973
  12. package/dist/hooks/session-end.js.map +4 -4
  13. package/dist/hooks/session-start.js +293 -978
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +247 -975
  16. package/dist/hooks/stop.js.map +4 -4
  17. package/dist/hooks/user-prompt-submit.js +406 -1036
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +209 -973
  20. package/dist/server/api/index.js.map +4 -4
  21. package/dist/server/index.js +209 -973
  22. package/dist/server/index.js.map +4 -4
  23. package/dist/services/memory-service.js +209 -973
  24. package/dist/services/memory-service.js.map +4 -4
  25. package/dist/ui/app.js +48 -1
  26. package/dist/ui/index.html +11 -3
  27. package/memory/_index.md +1 -0
  28. package/memory/agent_response/uncategorized/2026-03-04.md +1314 -1
  29. package/memory/session_summary/uncategorized/2026-03-04.md +50 -0
  30. package/memory/tool_observation/uncategorized/2026-03-04.md +969 -1
  31. package/memory/user_prompt/uncategorized/2026-03-04.md +555 -1
  32. package/package.json +1 -2
  33. package/scripts/build.ts +2 -1
  34. package/specs/memory-utilization-improvements/context.md +145 -0
  35. package/specs/memory-utilization-improvements/plan.md +361 -0
  36. package/specs/memory-utilization-improvements/spec.md +308 -0
  37. package/specs/optional-duckdb/context.md +77 -0
  38. package/specs/optional-duckdb/plan.md +142 -0
  39. package/specs/optional-duckdb/spec.md +35 -0
  40. package/specs/selective-tool-observation/context.md +100 -0
  41. package/specs/selective-tool-observation/plan.md +158 -0
  42. package/specs/selective-tool-observation/spec.md +127 -0
  43. package/src/cli/index.ts +1 -0
  44. package/src/core/db-wrapper.ts +18 -73
  45. package/src/core/embedder.ts +13 -4
  46. package/src/core/sqlite-event-store.ts +40 -0
  47. package/src/core/turn-state.ts +48 -0
  48. package/src/core/types.ts +1 -0
  49. package/src/hooks/post-tool-use.ts +72 -2
  50. package/src/hooks/semantic-daemon-client.ts +208 -0
  51. package/src/hooks/semantic-daemon.ts +276 -0
  52. package/src/hooks/session-start.ts +11 -0
  53. package/src/hooks/stop.ts +33 -4
  54. package/src/hooks/user-prompt-submit.ts +48 -40
  55. package/src/services/memory-service.ts +112 -65
  56. package/src/services/session-history-importer.ts +18 -0
  57. package/src/ui/app.js +48 -1
  58. package/src/ui/index.html +11 -3
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * User Prompt Submit Hook
4
- * Called when user submits a prompt - retrieves relevant memories using fast keyword search
4
+ * Called when user submits a prompt - retrieves relevant memories.
5
5
  *
6
- * Uses SQLite FTS5 for fast keyword-based search (no ML model needed)
7
- * Much faster than vector search (~100ms vs 3-5s)
6
+ * Retrieval mode (CLAUDE_MEMORY_RETRIEVAL_MODE):
7
+ * - keyword (default-fast): SQLite FTS5 only, no ML model (~10ms)
8
+ * - semantic: vector search via long-running semantic daemon (~15-20ms warm)
9
+ * - hybrid: semantic first, keyword fallback (default)
10
+ *
11
+ * The semantic daemon keeps the embedding model in memory across hook invocations,
12
+ * avoiding per-request model initialization (~730ms cold start).
8
13
  *
9
14
  * Turn Grouping: Generates a turn_id and persists it to a state file
10
15
  * so PostToolUse and Stop hooks can associate their events with this turn.
@@ -14,8 +19,9 @@ import { randomUUID } from 'crypto';
14
19
  import * as fs from 'fs';
15
20
  import * as path from 'path';
16
21
  import * as os from 'os';
17
- import { getLightweightMemoryService, getMemoryServiceForSession } from '../services/memory-service.js';
18
- import { writeTurnState } from '../core/turn-state.js';
22
+ import { getLightweightMemoryService } from '../services/memory-service.js';
23
+ import { writeTurnState, readLastAssistantSnippet } from '../core/turn-state.js';
24
+ import { retrieveSemanticMemories } from './semantic-daemon-client.js';
19
25
  import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
20
26
 
21
27
  // Configuration
@@ -25,7 +31,7 @@ const BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.4');
25
31
  const FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || '0.3');
26
32
  const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
27
33
  const RETRIEVAL_MODE = (process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || 'hybrid') as 'keyword' | 'semantic' | 'hybrid';
28
- const SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || '1200');
34
+ const SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || '2000');
29
35
  const ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || '3');
30
36
 
31
37
  const ADHERENCE_STATE_DIR = path.join(os.homedir(), '.claude-code', 'memory');
@@ -59,21 +65,6 @@ function getDynamicMinScore(prompt: string): number {
59
65
  return BASE_MIN_SCORE;
60
66
  }
61
67
 
62
- function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
63
- return new Promise((resolve, reject) => {
64
- const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
65
- promise
66
- .then((result) => {
67
- clearTimeout(timer);
68
- resolve(result);
69
- })
70
- .catch((error) => {
71
- clearTimeout(timer);
72
- reject(error);
73
- });
74
- });
75
- }
76
-
77
68
  function formatMemoryContext(items: Array<{ type: string; content: string }>): string {
78
69
  if (items.length === 0) return '';
79
70
  const lines = items.map((m) => {
@@ -196,6 +187,12 @@ async function main(): Promise<void> {
196
187
  const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
197
188
  logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
198
189
 
190
+ // On first turn of a new session, backfill helpfulness for sessions
191
+ // that ended without Stop hook (crash, force-close, etc.)
192
+ if (currentTurn === 1) {
193
+ memoryService.evaluatePendingSessions(input.session_id).catch(() => {});
194
+ }
195
+
199
196
  // Store only non-trivial prompts (skip /commands, short inputs)
200
197
  if (shouldStorePrompt(input.prompt)) {
201
198
  await memoryService.storeUserPrompt(
@@ -214,32 +211,30 @@ async function main(): Promise<void> {
214
211
 
215
212
  // Search strategy: turn-1 always enforce adherence check,
216
213
  // then adaptively enforce on write-intent/topic-shift/interval
217
- if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
214
+ const isSlashCommand = input.prompt.trimStart().startsWith('/');
215
+ if (ENABLE_SEARCH && !isSlashCommand && input.prompt.length > 10 && adherenceDecision.run) {
218
216
  const minScore = getDynamicMinScore(input.prompt);
219
217
  let mergedMemories: Array<{ type: string; content: string; id?: string; score?: number }> = [];
220
218
 
219
+ // On turn 2+, enrich the retrieval query with the previous assistant response
220
+ // so short/ambiguous follow-ups ("그거 고쳐줘") resolve correctly.
221
+ const lastSnippet = currentTurn > 1 ? readLastAssistantSnippet(input.session_id) : null;
222
+ const retrievalQuery = lastSnippet
223
+ ? `${lastSnippet}\n\n${input.prompt}`
224
+ : input.prompt;
225
+
221
226
  const canUseSemantic = RETRIEVAL_MODE === 'semantic' || RETRIEVAL_MODE === 'hybrid';
222
227
  if (canUseSemantic) {
223
228
  try {
224
- const semanticService = getMemoryServiceForSession(input.session_id);
225
- const semantic = await withTimeout(
226
- semanticService.retrieveMemories(input.prompt, {
227
- topK: MAX_MEMORIES,
228
- minScore,
229
+ mergedMemories = await retrieveSemanticMemories(
230
+ {
229
231
  sessionId: input.session_id,
230
- intentRewrite: true,
231
- adaptiveRerank: true,
232
- projectScopeMode: 'strict'
233
- }),
232
+ prompt: retrievalQuery,
233
+ topK: MAX_MEMORIES,
234
+ minScore
235
+ },
234
236
  SEMANTIC_TIMEOUT_MS
235
237
  );
236
-
237
- mergedMemories = semantic.memories.map((m) => ({
238
- type: m.event.eventType,
239
- content: m.event.content,
240
- id: m.event.id,
241
- score: m.score
242
- }));
243
238
  } catch {
244
239
  // Semantic retrieval is best-effort; fallback below handles the rest
245
240
  }
@@ -251,14 +246,14 @@ async function main(): Promise<void> {
251
246
  mergedMemories.length === 0;
252
247
 
253
248
  if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
254
- let results = await memoryService.keywordSearch(input.prompt, {
249
+ let results = await memoryService.keywordSearch(retrievalQuery, {
255
250
  topK: MAX_MEMORIES,
256
251
  minScore
257
252
  });
258
253
 
259
254
  // recall rescue: if nothing found at tuned threshold, retry with fallback floor
260
255
  if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
261
- results = await memoryService.keywordSearch(input.prompt, {
256
+ results = await memoryService.keywordSearch(retrievalQuery, {
262
257
  topK: MAX_MEMORIES,
263
258
  minScore: FALLBACK_MIN_SCORE
264
259
  });
@@ -299,6 +294,19 @@ async function main(): Promise<void> {
299
294
 
300
295
  context = formatMemoryContext(mergedMemories);
301
296
  }
297
+
298
+ // Record query-level trace for dashboard stats (retrieval_traces table)
299
+ const allCandidateIds = mergedMemories.map((m) => m.id).filter((v): v is string => Boolean(v));
300
+ try {
301
+ await memoryService.recordQueryTrace({
302
+ sessionId: input.session_id,
303
+ queryText: retrievalQuery,
304
+ strategy: RETRIEVAL_MODE,
305
+ candidateEventIds: allCandidateIds,
306
+ selectedEventIds: allCandidateIds,
307
+ confidence: mergedMemories.length > 0 ? 'medium' : 'none'
308
+ });
309
+ } catch { /* non-critical */ }
302
310
  }
303
311
 
304
312
  writeAdherenceState({
@@ -10,7 +10,6 @@ import * as crypto from 'crypto';
10
10
 
11
11
  import { EventStore } from '../core/event-store.js';
12
12
  import { SQLiteEventStore } from '../core/sqlite-event-store.js';
13
- import { SyncWorker } from '../core/sync-worker.js';
14
13
  import { VectorStore } from '../core/vector-store.js';
15
14
  import { Embedder, getDefaultEmbedder } from '../core/embedder.js';
16
15
  import { VectorWorker, createVectorWorker } from '../core/vector-worker.js';
@@ -61,6 +60,8 @@ export interface MemoryServiceConfig {
61
60
  analyticsEnabled?: boolean;
62
61
  /** Lightweight mode for hooks - skip heavy initialization (default: false) */
63
62
  lightweightMode?: boolean;
63
+ /** Start only VectorWorker, skip GraduationWorker and SyncWorker (default: false) */
64
+ embeddingOnly?: boolean;
64
65
  }
65
66
 
66
67
  // ============================================================
@@ -180,9 +181,6 @@ export function getSessionProject(sessionId: string): SessionRegistryEntry | nul
180
181
  export class MemoryService {
181
182
  // Primary store: SQLite (WAL mode) - for hooks, always available
182
183
  private readonly sqliteStore: SQLiteEventStore;
183
- // Analytics store: DuckDB - for server reads (optional, synced from SQLite)
184
- private readonly analyticsStore: EventStore | null;
185
- private syncWorker: SyncWorker | null = null;
186
184
 
187
185
  private readonly vectorStore: VectorStore;
188
186
  private readonly embedder: Embedder;
@@ -212,6 +210,7 @@ export class MemoryService {
212
210
 
213
211
  private readonly readOnly: boolean;
214
212
  private readonly lightweightMode: boolean;
213
+ private readonly embeddingOnly: boolean;
215
214
  private readonly mdMirror: MarkdownMirror;
216
215
  private readonly storagePath: string;
217
216
 
@@ -220,6 +219,7 @@ export class MemoryService {
220
219
  this.storagePath = storagePath;
221
220
  this.readOnly = config.readOnly ?? false;
222
221
  this.lightweightMode = config.lightweightMode ?? false;
222
+ this.embeddingOnly = config.embeddingOnly ?? false;
223
223
  this.mdMirror = new MarkdownMirror(process.cwd());
224
224
 
225
225
  // Ensure storage directory exists (only if not read-only)
@@ -243,32 +243,6 @@ export class MemoryService {
243
243
  }
244
244
  );
245
245
 
246
- // Initialize ANALYTICS store: DuckDB (optional, for server reads)
247
- // Hooks set analyticsEnabled=false to avoid DuckDB lock conflicts
248
- const analyticsEnabled = config.analyticsEnabled ?? this.readOnly; // Default: enabled only for read-only (server)
249
-
250
- if (!analyticsEnabled) {
251
- // Hook mode: skip DuckDB entirely to avoid lock conflicts
252
- this.analyticsStore = null;
253
- } else if (this.readOnly) {
254
- // Server mode: try to use DuckDB for analytics, will fallback to SQLite
255
- try {
256
- this.analyticsStore = new EventStore(
257
- path.join(storagePath, 'analytics.duckdb'),
258
- { readOnly: true }
259
- );
260
- } catch {
261
- // DuckDB not available, will use SQLite for reads
262
- this.analyticsStore = null;
263
- }
264
- } else {
265
- // Writer mode with analytics: create DuckDB for sync target
266
- this.analyticsStore = new EventStore(
267
- path.join(storagePath, 'analytics.duckdb'),
268
- { readOnly: false }
269
- );
270
- }
271
-
272
246
  this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
273
247
  const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
274
248
  this.embedder = embeddingModel
@@ -302,16 +276,6 @@ export class MemoryService {
302
276
  return;
303
277
  }
304
278
 
305
- // Initialize analytics store if available (DuckDB)
306
- if (this.analyticsStore) {
307
- try {
308
- await this.analyticsStore.initialize();
309
- } catch (error) {
310
- console.warn('[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:', error);
311
- // Continue without analytics - SQLite will be used for reads
312
- }
313
- }
314
-
315
279
  await this.vectorStore.initialize();
316
280
  await this.embedder.initialize();
317
281
 
@@ -325,24 +289,17 @@ export class MemoryService {
325
289
  );
326
290
  this.vectorWorker.start();
327
291
 
328
- // Connect graduation pipeline to retriever for access tracking
329
- this.retriever.setGraduationPipeline(this.graduation);
292
+ if (!this.embeddingOnly) {
293
+ // Connect graduation pipeline to retriever for access tracking
294
+ this.retriever.setGraduationPipeline(this.graduation);
330
295
 
331
- // Start graduation worker for automatic level promotion
332
- this.graduationWorker = createGraduationWorker(
333
- this.sqliteStore as unknown as EventStore,
334
- this.graduation
335
- );
336
- this.graduationWorker.start();
337
-
338
- // Start sync worker (SQLite -> DuckDB) if analytics store is available
339
- if (this.analyticsStore) {
340
- this.syncWorker = new SyncWorker(
341
- this.sqliteStore,
342
- this.analyticsStore,
343
- { intervalMs: 30000, batchSize: 500 }
296
+ // Start graduation worker for automatic level promotion
297
+ this.graduationWorker = createGraduationWorker(
298
+ this.sqliteStore as unknown as EventStore,
299
+ this.graduation
344
300
  );
345
- this.syncWorker.start();
301
+ this.graduationWorker.start();
302
+
346
303
  }
347
304
 
348
305
  // Load endless mode setting
@@ -596,6 +553,67 @@ export class MemoryService {
596
553
  );
597
554
  }
598
555
 
556
+ /**
557
+ * Backfill session summaries for recent sessions that are missing them.
558
+ * Called from session-start hook to catch sessions that ended without Stop hook.
559
+ */
560
+ async backfillMissingSummaries(currentSessionId: string, limit = 5): Promise<void> {
561
+ await this.initialize();
562
+
563
+ // Get recent sessions that don't have a summary event
564
+ const recentSessionIds = await this.sqliteStore.getSessionsWithoutSummary(currentSessionId, limit);
565
+ for (const sid of recentSessionIds) {
566
+ try {
567
+ await this.generateSessionSummary(sid);
568
+ } catch {
569
+ // non-critical
570
+ }
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Generate a rule-based session summary from stored events.
576
+ * Called at session end (Stop hook) when no LLM-generated summary exists.
577
+ * Skips if a summary already exists for this session.
578
+ */
579
+ async generateSessionSummary(sessionId: string): Promise<void> {
580
+ await this.initialize();
581
+
582
+ const events = await this.sqliteStore.getSessionEvents(sessionId);
583
+ if (events.length < 3) return; // Too short to summarize
584
+
585
+ // Skip if summary already exists
586
+ const hasSummary = events.some((e) => e.eventType === 'session_summary');
587
+ if (hasSummary) return;
588
+
589
+ const prompts = events.filter((e) => e.eventType === 'user_prompt');
590
+ const toolObs = events.filter((e) => e.eventType === 'tool_observation');
591
+ const toolNames = [...new Set(
592
+ toolObs.map((e) => (e.metadata as Record<string, unknown>)?.toolName as string).filter(Boolean)
593
+ )];
594
+ const errorObs = toolObs.filter((e) => {
595
+ const meta = e.metadata as Record<string, unknown>;
596
+ return meta?.exitCode !== undefined && meta.exitCode !== 0;
597
+ });
598
+
599
+ const datePart = events[0].timestamp.toISOString().split('T')[0];
600
+ const parts: string[] = [`[${datePart}] ${prompts.length}턴 세션.`];
601
+
602
+ if (prompts.length > 0) {
603
+ const firstPrompt = prompts[0].content.slice(0, 120).replace(/\n/g, ' ');
604
+ parts.push(`주요 작업: ${firstPrompt}`);
605
+ }
606
+ if (toolNames.length > 0) {
607
+ parts.push(`사용 툴: ${toolNames.slice(0, 6).join(', ')}`);
608
+ }
609
+ if (errorObs.length > 0) {
610
+ parts.push(`오류 ${errorObs.length}건 발생`);
611
+ }
612
+
613
+ const summary = parts.join('. ');
614
+ await this.storeSessionSummary(sessionId, summary, { generated: 'rule-based', eventCount: events.length });
615
+ }
616
+
599
617
  /**
600
618
  * Store a tool observation
601
619
  */
@@ -1254,6 +1272,28 @@ export class MemoryService {
1254
1272
  await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
1255
1273
  }
1256
1274
 
1275
+ /**
1276
+ * Record a query-level retrieval trace (used by user-prompt-submit hook).
1277
+ * Feeds the retrieval_traces table that powers dashboard stats.
1278
+ */
1279
+ async recordQueryTrace(input: {
1280
+ sessionId: string;
1281
+ queryText: string;
1282
+ strategy: string;
1283
+ candidateEventIds: string[];
1284
+ selectedEventIds: string[];
1285
+ confidence: string;
1286
+ }): Promise<void> {
1287
+ await this.initialize();
1288
+ await this.sqliteStore.recordRetrievalTrace({
1289
+ ...input,
1290
+ projectHash: this.projectHash || undefined,
1291
+ candidateDetails: [],
1292
+ selectedDetails: [],
1293
+ fallbackTrace: [],
1294
+ });
1295
+ }
1296
+
1257
1297
  /**
1258
1298
  * Evaluate helpfulness of retrievals in a session (called at session end)
1259
1299
  */
@@ -1262,6 +1302,22 @@ export class MemoryService {
1262
1302
  await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
1263
1303
  }
1264
1304
 
1305
+ /**
1306
+ * Backfill helpfulness evaluation for sessions that ended without Stop hook.
1307
+ * Call on first turn of a new session to catch missed evaluations.
1308
+ */
1309
+ async evaluatePendingSessions(currentSessionId: string): Promise<void> {
1310
+ await this.initialize();
1311
+ const sessions = await this.sqliteStore.getUnevaluatedSessions(currentSessionId, 5);
1312
+ for (const sid of sessions) {
1313
+ try {
1314
+ await this.sqliteStore.evaluateSessionHelpfulness(sid);
1315
+ } catch {
1316
+ // non-critical, skip failed
1317
+ }
1318
+ }
1319
+ }
1320
+
1265
1321
  /**
1266
1322
  * Get most helpful memories ranked by helpfulness score
1267
1323
  */
@@ -1609,11 +1665,6 @@ export class MemoryService {
1609
1665
  this.vectorWorker.stop();
1610
1666
  }
1611
1667
 
1612
- // Stop sync worker
1613
- if (this.syncWorker) {
1614
- this.syncWorker.stop();
1615
- }
1616
-
1617
1668
  // Close shared store
1618
1669
  if (this.sharedEventStore) {
1619
1670
  await this.sharedEventStore.close();
@@ -1622,10 +1673,6 @@ export class MemoryService {
1622
1673
  // Close primary store (SQLite)
1623
1674
  await this.sqliteStore.close();
1624
1675
 
1625
- // Close analytics store (DuckDB)
1626
- if (this.analyticsStore) {
1627
- await this.analyticsStore.close();
1628
- }
1629
1676
  }
1630
1677
 
1631
1678
  /**
@@ -59,6 +59,18 @@ export interface ClaudeMessage {
59
59
  * - 'thinking': Assistant thinking (thinking blocks)
60
60
  * - 'skip': Everything else (progress, system, summary, etc.)
61
61
  */
62
+ /**
63
+ * Filter trivial user inputs that aren't worth storing.
64
+ * Mirrors the shouldStorePrompt() logic from user-prompt-submit.ts.
65
+ */
66
+ function isWorthStoringPrompt(content: string): boolean {
67
+ const trimmed = content.trim();
68
+ if (trimmed.startsWith('/')) return false;
69
+ if (trimmed.length < 15) return false;
70
+ if (!/[a-zA-Z가-힣]{2,}/.test(trimmed)) return false;
71
+ return true;
72
+ }
73
+
62
74
  function classifyEntry(entry: ClaudeMessage): 'user_prompt' | 'tool_result' | 'agent_text' | 'tool_use' | 'thinking' | 'skip' {
63
75
  if (entry.type !== 'user' && entry.type !== 'assistant') {
64
76
  return 'skip';
@@ -283,6 +295,12 @@ export class SessionHistoryImporter {
283
295
  const content = this.extractContent(entry);
284
296
  if (!content) continue;
285
297
 
298
+ // Skip trivial inputs: slash commands, very short, no real words
299
+ if (!isWorthStoringPrompt(content)) {
300
+ result.skippedDuplicates++;
301
+ continue;
302
+ }
303
+
286
304
  // New turn starts with each real user prompt
287
305
  currentTurnId = randomUUID();
288
306
 
package/src/ui/app.js CHANGED
@@ -388,9 +388,20 @@ function updateStatsUI() {
388
388
  const sharedCount = state.sharedStats ?
389
389
  ((state.sharedStats.troubleshooting || 0) + (state.sharedStats.bestPractices || 0) + (state.sharedStats.commonErrors || 0)) : 0;
390
390
 
391
- document.getElementById('stat-shared').textContent = formatNumber(sharedCount);
392
391
  document.getElementById('stat-vectors').textContent = formatNumber(vectorCount);
393
392
 
393
+ // Retrieval quality stat card
394
+ const rtStats = state.retrievalTraces?.stats;
395
+ const totalQueries = rtStats?.totalQueries || 0;
396
+ const selRate = rtStats ? ((rtStats.selectionRate || 0) * 100).toFixed(0) : null;
397
+ document.getElementById('stat-retrieval-queries').textContent = formatNumber(totalQueries);
398
+ const rateEl = document.getElementById('stat-retrieval-rate');
399
+ if (rateEl) {
400
+ rateEl.textContent = totalQueries > 0 && selRate !== null
401
+ ? `${selRate}% selection rate`
402
+ : totalQueries > 0 ? '' : 'no queries yet';
403
+ }
404
+
394
405
  const levelCounts = {};
395
406
  if (state.stats.levelStats) {
396
407
  state.stats.levelStats.forEach(item => { levelCounts[item.level] = item.count; });
@@ -596,10 +607,46 @@ function updateEventsListUI() {
596
607
 
597
608
  // --- Memory Usage ---
598
609
 
610
+ function updateTopAccessedEventsUI() {
611
+ const container = document.getElementById('top-accessed-events-list');
612
+ if (!container) return;
613
+
614
+ const events = (state.mostAccessed?.events || state.mostAccessed?.memories || []);
615
+ const filtered = events.filter(e => (e.accessCount || 0) > 0).slice(0, 5);
616
+
617
+ if (filtered.length === 0) {
618
+ container.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No accessed memories yet</div>';
619
+ return;
620
+ }
621
+
622
+ container.innerHTML = filtered.map((m, i) => {
623
+ const type = m.eventType || m.type || 'memory';
624
+ const preview = (m.summary || m.preview || m.content || '').replace(/<[^>]*>/g, '').slice(0, 80);
625
+ const lastAccessed = m.lastAccessedAt ? new Date(m.lastAccessedAt).toLocaleDateString() : (m.lastAccessed ? new Date(m.lastAccessed).toLocaleDateString() : '-');
626
+ const id = m.id || m.memoryId || '';
627
+ return `
628
+ <div class="shared-item" style="cursor:pointer;" ${id ? `onclick="openDetailModal('${id}')"` : ''}>
629
+ <div class="shared-info" style="flex-direction:column; align-items:flex-start; gap:2px;">
630
+ <div style="display:flex; gap:6px; align-items:center;">
631
+ <span class="event-type-badge type-${type.replace('_','-')}">${type}</span>
632
+ <span style="font-size:10px; color:var(--text-muted);">last: ${lastAccessed}</span>
633
+ </div>
634
+ <span style="font-size:12px; color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:200px;" title="${escapeHtml(preview)}">${escapeHtml(preview) || '(no preview)'}</span>
635
+ </div>
636
+ <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:40px;">
637
+ <span style="font-size:15px; font-weight:700; color:var(--accent-primary);">${m.accessCount}</span>
638
+ <span style="font-size:10px; color:var(--text-muted);">hits</span>
639
+ </div>
640
+ </div>
641
+ `;
642
+ }).join('');
643
+ }
644
+
599
645
  function updateMemoryUsageUI() {
600
646
  updateGraduationBars();
601
647
  updateHelpfulnessUI();
602
648
  updateMostHelpfulList();
649
+ updateTopAccessedEventsUI();
603
650
  updateAdherenceSummaryUI();
604
651
  updateRetrievalTraceUI();
605
652
  }
package/src/ui/index.html CHANGED
@@ -110,11 +110,12 @@
110
110
  <i class="ri-discuss-line"></i> Active Sessions
111
111
  </div>
112
112
  </div>
113
- <div class="stat-card" data-stat="shared">
114
- <div class="stat-value" id="stat-shared">0</div>
113
+ <div class="stat-card" data-stat="retrieval">
114
+ <div class="stat-value" id="stat-retrieval-queries">0</div>
115
115
  <div class="stat-label">
116
- <i class="ri-share-forward-line"></i> Shared Items
116
+ <i class="ri-search-eye-line"></i> Retrieval Queries
117
117
  </div>
118
+ <div id="stat-retrieval-rate" style="font-size:11px; color:var(--text-muted); margin-top:2px;">-</div>
118
119
  </div>
119
120
  <div class="stat-card" data-stat="vectors">
120
121
  <div class="stat-value" id="stat-vectors">0</div>
@@ -327,6 +328,13 @@
327
328
  <div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
328
329
  </div>
329
330
 
331
+ <div style="margin-top:20px;">
332
+ <div class="section-label">Top Accessed Events</div>
333
+ <div id="top-accessed-events-list" class="shared-list">
334
+ <div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">Loading...</div>
335
+ </div>
336
+ </div>
337
+
330
338
  <div style="margin-top:20px;">
331
339
  <div class="section-label">Retrieval Trace (1:1)</div>
332
340
  <div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>