claude-memory-layer 1.0.10 → 1.0.12

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 (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. package/test_access.js +0 -49
@@ -10,14 +10,25 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import * as readline from 'readline';
13
- import { MemoryService } from './memory-service.js';
13
+ import { randomUUID } from 'crypto';
14
+ import { MemoryService, registerSession } from './memory-service.js';
15
+
16
+ export type ProgressEvent =
17
+ | { phase: 'scan'; message: string }
18
+ | { phase: 'session-start'; sessionIndex: number; totalSessions: number; filePath: string }
19
+ | { phase: 'session-progress'; sessionIndex: number; messagesProcessed: number; imported: number; skipped: number }
20
+ | { phase: 'session-done'; sessionIndex: number; importedPrompts: number; importedResponses: number; skipped: number }
21
+ | { phase: 'embedding'; processed: number; total: number }
22
+ | { phase: 'done'; result: ImportResult };
14
23
 
15
24
  export interface ImportOptions {
16
25
  projectPath?: string;
17
26
  sessionId?: string;
18
27
  limit?: number;
19
28
  skipExisting?: boolean;
29
+ force?: boolean;
20
30
  verbose?: boolean;
31
+ onProgress?: (event: ProgressEvent) => void;
21
32
  }
22
33
 
23
34
  export interface ImportResult {
@@ -33,12 +44,62 @@ export interface ClaudeMessage {
33
44
  type: string;
34
45
  message?: {
35
46
  role: string;
36
- content: string | Array<{ type: string; text?: string }>;
47
+ content: string | Array<{ type: string; text?: string; name?: string; tool_use_id?: string }>;
37
48
  };
38
49
  sessionId?: string;
39
50
  timestamp?: string;
40
51
  }
41
52
 
53
+ /**
54
+ * Classify a JSONL entry into a logical message type:
55
+ * - 'user_prompt': Real user input (string content or text blocks without tool_result)
56
+ * - 'tool_result': Tool execution result (user message with tool_result blocks)
57
+ * - 'agent_text': Assistant text response (text blocks)
58
+ * - 'tool_use': Assistant tool call (tool_use blocks)
59
+ * - 'thinking': Assistant thinking (thinking blocks)
60
+ * - 'skip': Everything else (progress, system, summary, etc.)
61
+ */
62
+ function classifyEntry(entry: ClaudeMessage): 'user_prompt' | 'tool_result' | 'agent_text' | 'tool_use' | 'thinking' | 'skip' {
63
+ if (entry.type !== 'user' && entry.type !== 'assistant') {
64
+ return 'skip';
65
+ }
66
+
67
+ const content = entry.message?.content;
68
+ if (!content) return 'skip';
69
+
70
+ if (entry.type === 'user') {
71
+ // String content = real user input
72
+ if (typeof content === 'string') return 'user_prompt';
73
+
74
+ // Array content: check for tool_result blocks
75
+ if (Array.isArray(content)) {
76
+ const hasToolResult = content.some(b => b.type === 'tool_result');
77
+ if (hasToolResult) return 'tool_result';
78
+
79
+ // Text-only blocks from user = real user input
80
+ const hasText = content.some(b => b.type === 'text' && b.text);
81
+ if (hasText) return 'user_prompt';
82
+ }
83
+ return 'skip';
84
+ }
85
+
86
+ // assistant type
87
+ if (Array.isArray(content)) {
88
+ const hasToolUse = content.some(b => b.type === 'tool_use');
89
+ if (hasToolUse) return 'tool_use';
90
+
91
+ const hasText = content.some(b => b.type === 'text' && b.text);
92
+ if (hasText) return 'agent_text';
93
+
94
+ const hasThinking = content.some(b => b.type === 'thinking');
95
+ if (hasThinking) return 'thinking';
96
+ } else if (typeof content === 'string' && content.length > 0) {
97
+ return 'agent_text';
98
+ }
99
+
100
+ return 'skip';
101
+ }
102
+
42
103
  export class SessionHistoryImporter {
43
104
  private readonly memoryService: MemoryService;
44
105
  private readonly claudeDir: string;
@@ -61,7 +122,10 @@ export class SessionHistoryImporter {
61
122
  errors: []
62
123
  };
63
124
 
125
+ const onProgress = options.onProgress;
126
+
64
127
  // Find project directory
128
+ onProgress?.({ phase: 'scan', message: 'Scanning for session files...' });
65
129
  const projectDir = await this.findProjectDir(projectPath);
66
130
  if (!projectDir) {
67
131
  result.errors.push(`Project directory not found for: ${projectPath}`);
@@ -71,19 +135,31 @@ export class SessionHistoryImporter {
71
135
  // Find all session files
72
136
  const sessionFiles = await this.findSessionFiles(projectDir);
73
137
  result.totalSessions = sessionFiles.length;
138
+ onProgress?.({ phase: 'scan', message: `Found ${sessionFiles.length} sessions in ${path.basename(projectDir)}` });
74
139
 
75
140
  if (options.verbose) {
76
141
  console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
77
142
  }
78
143
 
79
144
  // Import each session
80
- for (const sessionFile of sessionFiles) {
145
+ for (let i = 0; i < sessionFiles.length; i++) {
146
+ const sessionFile = sessionFiles[i];
81
147
  try {
82
- const sessionResult = await this.importSessionFile(sessionFile, options);
148
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
149
+ const sessionResult = await this.importSessionFile(sessionFile, {
150
+ ...options,
151
+ _sessionIndex: i,
152
+ } as ImportOptions & { _sessionIndex: number });
83
153
  result.totalMessages += sessionResult.totalMessages;
84
154
  result.importedPrompts += sessionResult.importedPrompts;
85
155
  result.importedResponses += sessionResult.importedResponses;
86
156
  result.skippedDuplicates += sessionResult.skippedDuplicates;
157
+ onProgress?.({
158
+ phase: 'session-done', sessionIndex: i,
159
+ importedPrompts: sessionResult.importedPrompts,
160
+ importedResponses: sessionResult.importedResponses,
161
+ skipped: sessionResult.skippedDuplicates
162
+ });
87
163
  } catch (error) {
88
164
  result.errors.push(`Failed to import ${sessionFile}: ${error}`);
89
165
  }
@@ -113,6 +189,14 @@ export class SessionHistoryImporter {
113
189
  // Extract session ID from filename
114
190
  const sessionId = path.basename(filePath, '.jsonl');
115
191
 
192
+ // Force reimport: delete existing events for this session
193
+ if (options.force) {
194
+ const deleted = await this.memoryService.deleteSessionEvents(sessionId);
195
+ if (options.verbose && deleted > 0) {
196
+ console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
197
+ }
198
+ }
199
+
116
200
  // Start session in memory
117
201
  await this.memoryService.startSession(sessionId, options.projectPath);
118
202
 
@@ -125,6 +209,51 @@ export class SessionHistoryImporter {
125
209
 
126
210
  let lineCount = 0;
127
211
  const limit = options.limit || Infinity;
212
+ const onProgress = options.onProgress;
213
+ const sessionIndex = (options as ImportOptions & { _sessionIndex?: number })._sessionIndex ?? 0;
214
+ let lastProgressAt = 0;
215
+
216
+ // Turn grouping with buffering:
217
+ // - Buffer assistant text blocks within a turn
218
+ // - On new user_prompt or EOF, flush buffer as a single merged agent_response
219
+ // - Filter out short transitional text (< 100 chars) like "Let me check..."
220
+ let currentTurnId: string | null = null;
221
+ let textBuffer: string[] = [];
222
+ let lastTimestamp: string | undefined;
223
+
224
+ // Flush buffered text as a single agent_response
225
+ const flushTextBuffer = async () => {
226
+ if (textBuffer.length === 0 || !currentTurnId) return;
227
+
228
+ // Filter: keep substantive text (>= 100 chars), discard short transitional phrases
229
+ const substantive = textBuffer.filter(t => t.length >= 100);
230
+
231
+ // If all filtered out, keep the longest block (there's always something meaningful)
232
+ const merged = substantive.length > 0
233
+ ? substantive.join('\n\n')
234
+ : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, '');
235
+
236
+ if (!merged) { textBuffer = []; return; }
237
+
238
+ // Truncate if very long
239
+ const truncated = merged.length > 10000
240
+ ? merged.slice(0, 10000) + '...[truncated]'
241
+ : merged;
242
+
243
+ const appendResult = await this.memoryService.storeAgentResponse(
244
+ sessionId,
245
+ truncated,
246
+ { importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
247
+ );
248
+
249
+ if (appendResult.isDuplicate) {
250
+ result.skippedDuplicates++;
251
+ } else {
252
+ result.importedResponses++;
253
+ }
254
+ lineCount++;
255
+ textBuffer = [];
256
+ };
128
257
 
129
258
  for await (const line of rl) {
130
259
  if (lineCount >= limit) break;
@@ -133,43 +262,51 @@ export class SessionHistoryImporter {
133
262
  const entry = JSON.parse(line) as ClaudeMessage;
134
263
  result.totalMessages++;
135
264
 
136
- // Process message entries
137
- if (entry.type === 'user' || entry.type === 'assistant') {
265
+ const msgClass = classifyEntry(entry);
266
+
267
+ if (msgClass === 'user_prompt') {
268
+ // Flush previous turn's buffered responses before starting new turn
269
+ await flushTextBuffer();
270
+
138
271
  const content = this.extractContent(entry);
139
272
  if (!content) continue;
140
273
 
141
- if (entry.type === 'user') {
142
- const appendResult = await this.memoryService.storeUserPrompt(
143
- sessionId,
144
- content,
145
- { importedFrom: filePath, originalTimestamp: entry.timestamp }
146
- );
147
-
148
- if (appendResult.isDuplicate) {
149
- result.skippedDuplicates++;
150
- } else {
151
- result.importedPrompts++;
152
- }
153
- } else if (entry.type === 'assistant') {
154
- // Truncate very long responses
155
- const truncatedContent = content.length > 5000
156
- ? content.slice(0, 5000) + '...[truncated]'
157
- : content;
158
-
159
- const appendResult = await this.memoryService.storeAgentResponse(
160
- sessionId,
161
- truncatedContent,
162
- { importedFrom: filePath, originalTimestamp: entry.timestamp }
163
- );
164
-
165
- if (appendResult.isDuplicate) {
166
- result.skippedDuplicates++;
167
- } else {
168
- result.importedResponses++;
169
- }
170
- }
274
+ // New turn starts with each real user prompt
275
+ currentTurnId = randomUUID();
276
+
277
+ const appendResult = await this.memoryService.storeUserPrompt(
278
+ sessionId,
279
+ content,
280
+ { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
281
+ );
171
282
 
283
+ if (appendResult.isDuplicate) {
284
+ result.skippedDuplicates++;
285
+ } else {
286
+ result.importedPrompts++;
287
+ }
172
288
  lineCount++;
289
+ } else if (msgClass === 'agent_text') {
290
+ // Buffer text instead of storing immediately
291
+ const content = this.extractContent(entry);
292
+ if (content) {
293
+ textBuffer.push(content);
294
+ lastTimestamp = entry.timestamp;
295
+ }
296
+ }
297
+ // tool_result, tool_use, thinking, skip → ignored
298
+
299
+ // Emit progress periodically
300
+ const now = Date.now();
301
+ if (now - lastProgressAt > 200) {
302
+ lastProgressAt = now;
303
+ onProgress?.({
304
+ phase: 'session-progress',
305
+ sessionIndex,
306
+ messagesProcessed: result.totalMessages,
307
+ imported: result.importedPrompts + result.importedResponses,
308
+ skipped: result.skippedDuplicates
309
+ });
173
310
  }
174
311
  } catch (parseError) {
175
312
  // Skip malformed lines
@@ -177,9 +314,17 @@ export class SessionHistoryImporter {
177
314
  }
178
315
  }
179
316
 
317
+ // Flush any remaining buffered text from the last turn
318
+ await flushTextBuffer();
319
+
180
320
  // End session
181
321
  await this.memoryService.endSession(sessionId);
182
322
 
323
+ // Register session in registry so projects API can map hash → path
324
+ if (options.projectPath) {
325
+ registerSession(sessionId, options.projectPath);
326
+ }
327
+
183
328
  if (options.verbose) {
184
329
  console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
185
330
  }
@@ -200,36 +345,55 @@ export class SessionHistoryImporter {
200
345
  errors: []
201
346
  };
202
347
 
348
+ const onProgress = options.onProgress;
349
+
203
350
  const projectsDir = path.join(this.claudeDir, 'projects');
204
351
  if (!fs.existsSync(projectsDir)) {
205
352
  result.errors.push(`Projects directory not found: ${projectsDir}`);
206
353
  return result;
207
354
  }
208
355
 
209
- // Find all project directories
356
+ // Find all project directories and session files
357
+ onProgress?.({ phase: 'scan', message: 'Scanning all projects...' });
210
358
  const projectDirs = fs.readdirSync(projectsDir)
211
359
  .map(name => path.join(projectsDir, name))
212
360
  .filter(p => fs.statSync(p).isDirectory());
213
361
 
362
+ // Collect all session files across all projects
363
+ const allSessionFiles: string[] = [];
364
+ for (const projectDir of projectDirs) {
365
+ const sessionFiles = await this.findSessionFiles(projectDir);
366
+ allSessionFiles.push(...sessionFiles);
367
+ }
368
+ onProgress?.({ phase: 'scan', message: `Found ${allSessionFiles.length} sessions across ${projectDirs.length} projects` });
369
+
214
370
  if (options.verbose) {
215
- console.log(`Found ${projectDirs.length} project directories`);
371
+ console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
216
372
  }
217
373
 
218
- for (const projectDir of projectDirs) {
374
+ // Import all session files with progress tracking
375
+ for (let i = 0; i < allSessionFiles.length; i++) {
376
+ const sessionFile = allSessionFiles[i];
219
377
  try {
220
- const sessionFiles = await this.findSessionFiles(projectDir);
221
-
222
- for (const sessionFile of sessionFiles) {
223
- const sessionResult = await this.importSessionFile(sessionFile, options);
224
- result.totalSessions++;
225
- result.totalMessages += sessionResult.totalMessages;
226
- result.importedPrompts += sessionResult.importedPrompts;
227
- result.importedResponses += sessionResult.importedResponses;
228
- result.skippedDuplicates += sessionResult.skippedDuplicates;
229
- result.errors.push(...sessionResult.errors);
230
- }
378
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
379
+ const sessionResult = await this.importSessionFile(sessionFile, {
380
+ ...options,
381
+ _sessionIndex: i,
382
+ } as ImportOptions & { _sessionIndex: number });
383
+ result.totalSessions++;
384
+ result.totalMessages += sessionResult.totalMessages;
385
+ result.importedPrompts += sessionResult.importedPrompts;
386
+ result.importedResponses += sessionResult.importedResponses;
387
+ result.skippedDuplicates += sessionResult.skippedDuplicates;
388
+ result.errors.push(...sessionResult.errors);
389
+ onProgress?.({
390
+ phase: 'session-done', sessionIndex: i,
391
+ importedPrompts: sessionResult.importedPrompts,
392
+ importedResponses: sessionResult.importedResponses,
393
+ skipped: sessionResult.skippedDuplicates
394
+ });
231
395
  } catch (error) {
232
- result.errors.push(`Failed to process ${projectDir}: ${error}`);
396
+ result.errors.push(`Failed to process ${sessionFile}: ${error}`);
233
397
  }
234
398
  }
235
399