memento-mcp-server 1.14.0 → 1.15.0-c

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 (77) hide show
  1. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts +18 -0
  2. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts.map +1 -0
  3. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js +346 -0
  4. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js.map +1 -0
  5. package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
  6. package/dist/domains/memory/tools/remember-tool.js +182 -1
  7. package/dist/domains/memory/tools/remember-tool.js.map +1 -1
  8. package/dist/infrastructure/database/database/migration/migrations/005-relation-engine-schema.sql +7 -7
  9. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts +65 -0
  10. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts.map +1 -0
  11. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js +250 -0
  12. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js.map +1 -0
  13. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.sql +86 -0
  14. package/dist/infrastructure/logging/triple-extraction-logger.d.ts +92 -0
  15. package/dist/infrastructure/logging/triple-extraction-logger.d.ts.map +1 -0
  16. package/dist/infrastructure/logging/triple-extraction-logger.js +194 -0
  17. package/dist/infrastructure/logging/triple-extraction-logger.js.map +1 -0
  18. package/dist/infrastructure/scheduler/batch-scheduler.d.ts +57 -0
  19. package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
  20. package/dist/infrastructure/scheduler/batch-scheduler.js +220 -0
  21. package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
  22. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts +194 -0
  23. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts.map +1 -0
  24. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js +639 -0
  25. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js.map +1 -0
  26. package/dist/services/semantic-memory/semantic-memory-statistics.d.ts +64 -0
  27. package/dist/services/semantic-memory/semantic-memory-statistics.d.ts.map +1 -0
  28. package/dist/services/semantic-memory/semantic-memory-statistics.js +113 -0
  29. package/dist/services/semantic-memory/semantic-memory-statistics.js.map +1 -0
  30. package/dist/services/semantic-memory/semantic-memory-update-service.d.ts +257 -0
  31. package/dist/services/semantic-memory/semantic-memory-update-service.d.ts.map +1 -0
  32. package/dist/services/semantic-memory/semantic-memory-update-service.js +696 -0
  33. package/dist/services/semantic-memory/semantic-memory-update-service.js.map +1 -0
  34. package/dist/services/triple-extraction/entity-linker.d.ts +55 -0
  35. package/dist/services/triple-extraction/entity-linker.d.ts.map +1 -0
  36. package/dist/services/triple-extraction/entity-linker.js +154 -0
  37. package/dist/services/triple-extraction/entity-linker.js.map +1 -0
  38. package/dist/services/triple-extraction/predicate-canonicalizer.d.ts +63 -0
  39. package/dist/services/triple-extraction/predicate-canonicalizer.d.ts.map +1 -0
  40. package/dist/services/triple-extraction/predicate-canonicalizer.js +166 -0
  41. package/dist/services/triple-extraction/predicate-canonicalizer.js.map +1 -0
  42. package/dist/services/triple-extraction/triple-extraction-service.d.ts +181 -0
  43. package/dist/services/triple-extraction/triple-extraction-service.d.ts.map +1 -0
  44. package/dist/services/triple-extraction/triple-extraction-service.js +907 -0
  45. package/dist/services/triple-extraction/triple-extraction-service.js.map +1 -0
  46. package/dist/services/triple-extraction/triple-extraction-statistics.d.ts +74 -0
  47. package/dist/services/triple-extraction/triple-extraction-statistics.d.ts.map +1 -0
  48. package/dist/services/triple-extraction/triple-extraction-statistics.js +146 -0
  49. package/dist/services/triple-extraction/triple-extraction-statistics.js.map +1 -0
  50. package/dist/shared/types/index.d.ts +1 -0
  51. package/dist/shared/types/index.d.ts.map +1 -1
  52. package/dist/shared/types/index.js.map +1 -1
  53. package/dist/shared/types/triple-extraction.d.ts +99 -0
  54. package/dist/shared/types/triple-extraction.d.ts.map +1 -0
  55. package/dist/shared/types/triple-extraction.js +6 -0
  56. package/dist/shared/types/triple-extraction.js.map +1 -0
  57. package/dist/shared/utils/pii-masker.d.ts +67 -0
  58. package/dist/shared/utils/pii-masker.d.ts.map +1 -0
  59. package/dist/shared/utils/pii-masker.js +205 -0
  60. package/dist/shared/utils/pii-masker.js.map +1 -0
  61. package/dist/shared/utils/prompt-template-loader.d.ts +42 -0
  62. package/dist/shared/utils/prompt-template-loader.d.ts.map +1 -0
  63. package/dist/shared/utils/prompt-template-loader.js +92 -0
  64. package/dist/shared/utils/prompt-template-loader.js.map +1 -0
  65. package/dist/shared/utils/triple-cache.d.ts +90 -0
  66. package/dist/shared/utils/triple-cache.d.ts.map +1 -0
  67. package/dist/shared/utils/triple-cache.js +124 -0
  68. package/dist/shared/utils/triple-cache.js.map +1 -0
  69. package/dist/tools/index.d.ts +2 -1
  70. package/dist/tools/index.d.ts.map +1 -1
  71. package/dist/tools/index.js +3 -1
  72. package/dist/tools/index.js.map +1 -1
  73. package/dist/tools/types.d.ts +1 -0
  74. package/dist/tools/types.d.ts.map +1 -1
  75. package/dist/tools/types.js +2 -0
  76. package/dist/tools/types.js.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Triple 추출 배치 작업
3
+ *
4
+ * 미처리 또는 실패한 Episodic Memory에 대해 Triple 추출을 수행하는 배치 작업입니다.
5
+ *
6
+ * 주요 기능:
7
+ * - 미처리 Episodic Memory 조회
8
+ * - Triple 추출 및 Semantic Memory 생성
9
+ * - 재시도 정책 적용 (최대 3회, 지수 백오프: 1일, 2일, 4일)
10
+ * - 상태 업데이트 (성공/실패/abandoned)
11
+ * - 로깅 및 통계 수집
12
+ *
13
+ * 재시도 정책:
14
+ * - 최대 시도 횟수: 3회 (설정 가능)
15
+ * - 지수 백오프: 1일, 2일, 4일 후 재시도
16
+ * - 즉각 재시도 금지: LLM 호출 실패 시 바로 재시도하지 않음 (비용 절감)
17
+ * - 지연 재시도: 배치 작업에서 실패한 항목을 다음 배치에서 재시도
18
+ * - 최대 시도 횟수 초과 시: abandoned 상태로 설정하여 재시도 중단
19
+ */
20
+ import Database from 'better-sqlite3';
21
+ import { DatabaseUtils } from '../../../shared/utils/database.js';
22
+ import { TripleExtractionService } from '../../../services/triple-extraction/triple-extraction-service.js';
23
+ import { SemanticMemoryUpdateService } from '../../../services/semantic-memory/semantic-memory-update-service.js';
24
+ import { logger } from '../../../shared/utils/logger.js';
25
+ /**
26
+ * Triple 추출 배치 작업 클래스
27
+ */
28
+ export class TripleExtractionBatchJob {
29
+ config;
30
+ tripleExtractionService;
31
+ semanticMemoryUpdateService = null;
32
+ constructor(config, dependencies) {
33
+ this.config = {
34
+ batchSize: config?.batchSize ?? 10, // PRD 6.2: 배치 크기 10개
35
+ timeout: config?.timeout ?? 30000, // PRD 6.2: 타임아웃 30초
36
+ maxRetries: config?.maxRetries ?? 3,
37
+ retryBackoffDays: config?.retryBackoffDays ?? [1, 2, 4],
38
+ chunkSize: config?.chunkSize ?? 5, // SQLite WAL 환경 고려: 작은 단위로 처리
39
+ chunkDelayMs: config?.chunkDelayMs ?? 100, // 청크 사이 짧은 지연
40
+ parallelism: config?.parallelism ?? 1, // PRD 6.2: 병렬성 제어, 기본값 1 (싱글톤 배치 작업)
41
+ ...config
42
+ };
43
+ this.tripleExtractionService = dependencies?.tripleExtractionService ?? new TripleExtractionService();
44
+ this.semanticMemoryUpdateService = dependencies?.semanticMemoryUpdateService ?? null;
45
+ }
46
+ /**
47
+ * 배치 작업 실행
48
+ *
49
+ * PRD 6.2 배치 처리 최적화:
50
+ * - 배치 크기: 10개씩 처리 (설정 가능)
51
+ * - 타임아웃: 배치당 최대 30초 (설정 가능)
52
+ * - 병렬성 제어: Parallelism = 1 (동시에 하나의 배치만 실행)
53
+ *
54
+ * @param db 데이터베이스 연결
55
+ * @returns 배치 작업 결과
56
+ */
57
+ async execute(db) {
58
+ const startTime = new Date();
59
+ const timeoutDeadline = startTime.getTime() + this.config.timeout;
60
+ const result = {
61
+ jobType: 'triple_extraction_batch',
62
+ startTime,
63
+ endTime: new Date(),
64
+ duration: 0,
65
+ success: false,
66
+ processed: 0,
67
+ errors: [],
68
+ warnings: [],
69
+ details: {
70
+ processed: 0,
71
+ success: 0,
72
+ failed: 0,
73
+ skipped: 0,
74
+ semanticMemoriesCreated: 0,
75
+ semanticMemoriesUpdated: 0,
76
+ retryCounts: new Map()
77
+ }
78
+ };
79
+ try {
80
+ // PRD 6.3: 배치 작업 로깅 - 시작 로깅
81
+ logger.info('Starting triple extraction batch job', {
82
+ batchSize: this.config.batchSize,
83
+ timeout: `${this.config.timeout}ms`,
84
+ parallelism: 1, // PRD 6.2: 병렬성 제어, 기본값 1 (싱글톤 배치 작업)
85
+ chunkSize: this.config.chunkSize,
86
+ chunkDelayMs: this.config.chunkDelayMs,
87
+ maxRetries: this.config.maxRetries,
88
+ retryBackoffDays: this.config.retryBackoffDays
89
+ });
90
+ // SemanticMemoryUpdateService 초기화 (아직 초기화되지 않은 경우)
91
+ if (!this.semanticMemoryUpdateService) {
92
+ this.semanticMemoryUpdateService = new SemanticMemoryUpdateService(db);
93
+ }
94
+ // 배치 작업 대상 조회
95
+ const targetMemories = await this.getTargetMemories(db, this.config.batchSize);
96
+ // PRD 6.3: 배치 작업 로깅 - 대상 조회 결과
97
+ logger.info('Found target memories for triple extraction', {
98
+ count: targetMemories.length,
99
+ batchSize: this.config.batchSize,
100
+ chunkSize: this.config.chunkSize,
101
+ estimatedChunks: Math.ceil(targetMemories.length / this.config.chunkSize),
102
+ estimatedDuration: targetMemories.length > 0
103
+ ? `${Math.ceil((targetMemories.length * 3) / 60)} minutes (estimated)`
104
+ : '0 minutes'
105
+ });
106
+ // SQLite WAL 환경 고려: 작은 청크 단위로 나누어 처리하여 Lock 충돌 방지
107
+ // PRD 6.1: 배치 작업은 단일 트랜잭션으로 처리하지 않고, 작은 단위로 나누어 처리
108
+ const chunks = this.splitIntoChunks(targetMemories, this.config.chunkSize);
109
+ logger.debug('Split memories into chunks for WAL-safe processing', {
110
+ totalMemories: targetMemories.length,
111
+ chunkCount: chunks.length,
112
+ chunkSize: this.config.chunkSize
113
+ });
114
+ // 각 청크를 순차적으로 처리 (PRD 6.2: 병렬성 제어, parallelism=1)
115
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
116
+ // 타임아웃 체크 (PRD 6.2: 배치당 최대 30초)
117
+ const currentTime = Date.now();
118
+ if (currentTime >= timeoutDeadline) {
119
+ const elapsed = currentTime - startTime.getTime();
120
+ result.warnings.push(`Batch job timeout after ${elapsed}ms (limit: ${this.config.timeout}ms)`);
121
+ result.timeoutOccurred = true; // 타임아웃 플래그 설정
122
+ logger.warn('Triple extraction batch job timeout', {
123
+ elapsed,
124
+ timeout: this.config.timeout,
125
+ processed: result.details.processed,
126
+ remainingChunks: chunks.length - chunkIndex
127
+ });
128
+ break; // 타임아웃 발생 시 처리 중단
129
+ }
130
+ const chunk = chunks[chunkIndex];
131
+ if (!chunk) {
132
+ // 청크가 없으면 건너뛰기
133
+ continue;
134
+ }
135
+ try {
136
+ // 청크 단위로 처리 (작은 트랜잭션으로 Lock 충돌 방지)
137
+ const chunkResult = await this.processChunk(db, chunk, result, timeoutDeadline);
138
+ // 청크 처리 결과를 전체 결과에 반영
139
+ result.details.processed += chunkResult.processed;
140
+ result.details.success += chunkResult.success;
141
+ result.details.failed += chunkResult.failed;
142
+ result.details.skipped += chunkResult.skipped;
143
+ result.details.semanticMemoriesCreated += chunkResult.semanticMemoriesCreated;
144
+ result.details.semanticMemoriesUpdated += chunkResult.semanticMemoriesUpdated;
145
+ // 청크별 재시도 횟수 반영
146
+ for (const [memoryId, retryCount] of chunkResult.retryCounts) {
147
+ result.details.retryCounts.set(memoryId, retryCount);
148
+ }
149
+ // 에러 수집
150
+ result.errors.push(...chunkResult.errors);
151
+ // PRD 6.3: 배치 작업 로깅 - 청크 처리 결과
152
+ logger.debug('Chunk processed', {
153
+ chunkIndex: chunkIndex + 1,
154
+ totalChunks: chunks.length,
155
+ chunkSize: chunk?.length ?? 0,
156
+ processed: chunkResult.processed,
157
+ success: chunkResult.success,
158
+ failed: chunkResult.failed,
159
+ skipped: chunkResult.skipped,
160
+ semanticMemoriesCreated: chunkResult.semanticMemoriesCreated,
161
+ semanticMemoriesUpdated: chunkResult.semanticMemoriesUpdated,
162
+ progress: `${((chunkIndex + 1) / chunks.length * 100).toFixed(1)}%`
163
+ });
164
+ // 마지막 청크가 아니면 짧은 지연 (SQLite WAL Lock 충돌 방지)
165
+ if (chunkIndex < chunks.length - 1 && this.config.chunkDelayMs > 0) {
166
+ await new Promise(resolve => setTimeout(resolve, this.config.chunkDelayMs));
167
+ }
168
+ }
169
+ catch (error) {
170
+ // 청크 처리 실패 시 에러 기록하고 다음 청크 계속 처리
171
+ const errorMessage = `Failed to process chunk ${chunkIndex + 1}/${chunks.length}: ${error instanceof Error ? error.message : String(error)}`;
172
+ result.errors.push(errorMessage);
173
+ logger.error('Error processing chunk in batch job', {
174
+ chunkIndex: chunkIndex + 1,
175
+ totalChunks: chunks.length,
176
+ error: error instanceof Error ? error.message : String(error)
177
+ });
178
+ // 청크 내 모든 메모리를 실패로 처리
179
+ result.details.failed += chunk?.length ?? 0;
180
+ result.details.processed += chunk?.length ?? 0;
181
+ }
182
+ }
183
+ // 배치 작업 성공 여부: 처리된 항목이 있고, 치명적 에러가 없는 경우 성공으로 간주
184
+ // 부분 실패(failed > 0)는 허용 (재시도 정책에 따라 처리됨)
185
+ result.success = result.details.processed > 0;
186
+ result.processed = result.details.processed;
187
+ // PRD 6.3: 배치 작업 로깅
188
+ // - 처리된 Episodic Memory 수
189
+ // - 생성된 Semantic Memory 수
190
+ // - 실패한 항목 수 및 에러 로그
191
+ // - 배치 실행 시간 및 성능 메트릭
192
+ const duration = result.endTime.getTime() - result.startTime.getTime();
193
+ const durationSeconds = (duration / 1000).toFixed(2);
194
+ const avgProcessingTime = result.details.processed > 0
195
+ ? (duration / result.details.processed).toFixed(0)
196
+ : '0';
197
+ const successRate = result.details.processed > 0
198
+ ? ((result.details.success / result.details.processed) * 100).toFixed(1)
199
+ : '0.0';
200
+ logger.info('Triple extraction batch job completed', {
201
+ // 처리된 Episodic Memory 수
202
+ processed: result.details.processed,
203
+ success: result.details.success,
204
+ failed: result.details.failed,
205
+ skipped: result.details.skipped,
206
+ // 생성된 Semantic Memory 수
207
+ semanticMemoriesCreated: result.details.semanticMemoriesCreated,
208
+ semanticMemoriesUpdated: result.details.semanticMemoriesUpdated,
209
+ totalSemanticMemories: result.details.semanticMemoriesCreated + result.details.semanticMemoriesUpdated,
210
+ // 실패한 항목 수 및 에러 로그
211
+ errorCount: result.errors.length,
212
+ warningCount: result.warnings.length,
213
+ errors: result.errors.length > 0 ? result.errors.slice(0, 5) : [], // 최대 5개만 로깅
214
+ warnings: result.warnings.length > 0 ? result.warnings.slice(0, 5) : [], // 최대 5개만 로깅
215
+ // 배치 실행 시간 및 성능 메트릭
216
+ duration: `${durationSeconds}s`,
217
+ durationMs: duration,
218
+ avgProcessingTimeMs: avgProcessingTime,
219
+ successRate: `${successRate}%`,
220
+ // 재시도 통계
221
+ retryCounts: result.details.retryCounts.size > 0
222
+ ? Array.from(result.details.retryCounts.values()).reduce((sum, count) => sum + count, 0)
223
+ : 0,
224
+ uniqueRetriedMemories: result.details.retryCounts.size,
225
+ // 청크 통계
226
+ totalChunks: chunks.length,
227
+ chunkSize: this.config.chunkSize,
228
+ // 작업 상태
229
+ jobStatus: result.success ? 'success' : 'partial_failure',
230
+ timeoutOccurred: result.warnings.some(w => w.includes('timeout'))
231
+ });
232
+ // 성능 메트릭 상세 로깅 (디버그 레벨)
233
+ logger.debug('Triple extraction batch job performance metrics', {
234
+ throughput: result.details.processed > 0
235
+ ? `${(result.details.processed / (duration / 1000)).toFixed(2)} memories/sec`
236
+ : '0 memories/sec',
237
+ semanticMemoryCreationRate: result.details.semanticMemoriesCreated > 0
238
+ ? `${(result.details.semanticMemoriesCreated / (duration / 1000)).toFixed(2)} semantic memories/sec`
239
+ : '0 semantic memories/sec',
240
+ chunkProcessingStats: {
241
+ totalChunks: chunks.length,
242
+ avgChunkSize: chunks.length > 0
243
+ ? (targetMemories.length / chunks.length).toFixed(1)
244
+ : '0',
245
+ chunkDelayMs: this.config.chunkDelayMs
246
+ },
247
+ retryDistribution: result.details.retryCounts.size > 0
248
+ ? Array.from(result.details.retryCounts.values()).reduce((acc, count) => {
249
+ acc[count] = (acc[count] || 0) + 1;
250
+ return acc;
251
+ }, {})
252
+ : {}
253
+ });
254
+ }
255
+ catch (error) {
256
+ // PRD 6.3: 배치 작업 로깅 - 에러 로그
257
+ const errorMessage = error instanceof Error ? error.message : String(error);
258
+ result.errors.push(errorMessage);
259
+ const duration = result.endTime.getTime() - result.startTime.getTime();
260
+ logger.error('Triple extraction batch job failed', {
261
+ error: errorMessage,
262
+ errorStack: error instanceof Error ? error.stack : undefined,
263
+ processed: result.details.processed,
264
+ success: result.details.success,
265
+ failed: result.details.failed,
266
+ duration: `${(duration / 1000).toFixed(2)}s`,
267
+ errors: result.errors,
268
+ warnings: result.warnings
269
+ });
270
+ }
271
+ finally {
272
+ result.endTime = new Date();
273
+ result.duration = result.endTime.getTime() - result.startTime.getTime();
274
+ }
275
+ return result;
276
+ }
277
+ /**
278
+ * 메모리 배열을 청크로 분할
279
+ *
280
+ * SQLite WAL 환경 고려: 작은 단위로 나누어 처리하여 Lock 충돌 방지
281
+ *
282
+ * @param memories 메모리 배열
283
+ * @param chunkSize 청크 크기
284
+ * @returns 청크 배열
285
+ */
286
+ splitIntoChunks(memories, chunkSize) {
287
+ const chunks = [];
288
+ for (let i = 0; i < memories.length; i += chunkSize) {
289
+ chunks.push(memories.slice(i, i + chunkSize));
290
+ }
291
+ return chunks;
292
+ }
293
+ /**
294
+ * 청크 단위 처리
295
+ *
296
+ * SQLite WAL 환경 고려: 작은 트랜잭션으로 처리하여 Lock 충돌 방지
297
+ * 각 메모리 처리는 개별적으로 처리되며, SQLITE_BUSY 오류 발생 시 재시도
298
+ *
299
+ * PRD 6.2 배치 처리 최적화:
300
+ * - 타임아웃 체크: 배치당 최대 30초
301
+ * - 병렬성 제어: 순차 처리 (parallelism=1)
302
+ *
303
+ * @param db 데이터베이스 연결
304
+ * @param chunk 청크 (메모리 배열)
305
+ * @param overallResult 전체 결과 객체 (로깅용)
306
+ * @param timeoutDeadline 타임아웃 데드라인 (타임스탬프)
307
+ * @returns 청크 처리 결과
308
+ */
309
+ async processChunk(db, chunk, overallResult, timeoutDeadline) {
310
+ const chunkResult = {
311
+ processed: 0,
312
+ success: 0,
313
+ failed: 0,
314
+ skipped: 0,
315
+ semanticMemoriesCreated: 0,
316
+ semanticMemoriesUpdated: 0,
317
+ retryCounts: new Map(),
318
+ errors: []
319
+ };
320
+ // 각 메모리를 개별적으로 처리 (작은 트랜잭션으로 Lock 충돌 방지)
321
+ // PRD 6.2: 병렬성 제어, 순차 처리 (parallelism=1)
322
+ for (const memory of chunk) {
323
+ // 타임아웃 체크 (PRD 6.2: 배치당 최대 30초)
324
+ if (Date.now() >= timeoutDeadline) {
325
+ overallResult.timeoutOccurred = true; // 타임아웃 플래그 설정
326
+ logger.warn('Chunk processing timeout, stopping chunk processing', {
327
+ chunkSize: chunk.length,
328
+ processedInChunk: chunkResult.processed
329
+ });
330
+ break; // 타임아웃 발생 시 청크 처리 중단
331
+ }
332
+ try {
333
+ // 재시도 정책 확인 (getTargetMemories에서 이미 필터링되었지만, 이중 확인)
334
+ const shouldRetry = this.shouldRetry(memory);
335
+ if (!shouldRetry) {
336
+ chunkResult.skipped++;
337
+ logger.debug('Skipping memory due to retry policy', {
338
+ memory_id: memory.id,
339
+ retry_count: this.getRetryCount(memory)
340
+ });
341
+ continue;
342
+ }
343
+ // Triple 추출 수행 (LLM 호출, 트랜잭션 외부에서 처리)
344
+ const extractionResult = await this.tripleExtractionService.extractTriples(memory.content);
345
+ if (extractionResult.triples.length > 0) {
346
+ // Semantic Memory 생성/업데이트 (트랜잭션 내부에서 처리)
347
+ // DatabaseUtils.runTransaction은 SQLITE_BUSY 오류 발생 시 자동 재시도
348
+ await DatabaseUtils.runTransaction(db, async () => {
349
+ const updateResult = await this.semanticMemoryUpdateService.updateSemanticMemory(extractionResult, {
350
+ episodicMemoryId: memory.id,
351
+ episodicImportance: memory.importance ?? 0.5
352
+ });
353
+ // 성공 상태 업데이트 (트랜잭션 내부)
354
+ const confidenceAvg = await this.calculateAverageConfidence(db, memory.id);
355
+ const successMetadata = {
356
+ triple_count: extractionResult.triples.length,
357
+ extracted_at: new Date().toISOString()
358
+ };
359
+ if (confidenceAvg !== null) {
360
+ successMetadata.confidence_avg = confidenceAvg;
361
+ }
362
+ await this.updateMemoryStatus(db, memory.id, 'success', successMetadata);
363
+ chunkResult.success++;
364
+ chunkResult.semanticMemoriesCreated += updateResult.created;
365
+ chunkResult.semanticMemoriesUpdated += updateResult.updated;
366
+ });
367
+ }
368
+ else {
369
+ // Triple 추출 실패 - 상태 업데이트 (트랜잭션 내부)
370
+ const failureReason = extractionResult.extractionInfo.failureReason || 'no_triple';
371
+ const currentRetryCount = this.getRetryCount(memory);
372
+ const newRetryCount = currentRetryCount + 1;
373
+ await DatabaseUtils.runTransaction(db, async () => {
374
+ // 재시도 정책 적용: 최대 시도 횟수 확인
375
+ if (newRetryCount >= this.config.maxRetries) {
376
+ // 최대 시도 횟수 초과 - abandoned 상태로 설정
377
+ await this.updateMemoryStatus(db, memory.id, 'abandoned', {
378
+ failureReason,
379
+ retry_count: newRetryCount,
380
+ last_attempt: new Date().toISOString(),
381
+ abandoned_at: new Date().toISOString()
382
+ });
383
+ logger.info('Triple extraction abandoned after max retries', {
384
+ memory_id: memory.id,
385
+ retry_count: newRetryCount,
386
+ max_retries: this.config.maxRetries,
387
+ failure_reason: failureReason
388
+ });
389
+ }
390
+ else {
391
+ // 재시도 가능 - failed 상태로 업데이트
392
+ const backoffDays = this.config.retryBackoffDays[newRetryCount - 1] ||
393
+ this.config.retryBackoffDays[this.config.retryBackoffDays.length - 1];
394
+ await this.updateMemoryStatus(db, memory.id, 'failed', {
395
+ failureReason,
396
+ retry_count: newRetryCount,
397
+ last_attempt: new Date().toISOString(),
398
+ next_retry_after_days: backoffDays
399
+ });
400
+ logger.debug('Triple extraction failed, will retry after backoff', {
401
+ memory_id: memory.id,
402
+ retry_count: newRetryCount,
403
+ backoff_days: backoffDays,
404
+ failure_reason: failureReason
405
+ });
406
+ }
407
+ });
408
+ chunkResult.failed++;
409
+ chunkResult.retryCounts.set(memory.id, newRetryCount);
410
+ }
411
+ chunkResult.processed++;
412
+ }
413
+ catch (error) {
414
+ // SQLITE_BUSY 오류는 DatabaseUtils.runTransaction에서 이미 재시도됨
415
+ // 여기서는 예상치 못한 오류만 처리
416
+ const errorMessage = `Failed to process memory ${memory.id}: ${error instanceof Error ? error.message : String(error)}`;
417
+ chunkResult.errors.push(errorMessage);
418
+ chunkResult.failed++;
419
+ chunkResult.processed++;
420
+ logger.error('Error processing episodic memory in batch job', {
421
+ memory_id: memory.id,
422
+ error: error instanceof Error ? error.message : String(error),
423
+ error_code: error?.code
424
+ });
425
+ }
426
+ }
427
+ return chunkResult;
428
+ }
429
+ /**
430
+ * 배치 작업 대상 조회
431
+ *
432
+ * @param db 데이터베이스 연결
433
+ * @param limit 최대 조회 개수
434
+ * @returns 대상 Episodic Memory 목록 (재시도 정책 적용)
435
+ */
436
+ async getTargetMemories(db, limit) {
437
+ // 미처리 또는 실패한 Episodic Memory 조회
438
+ // - triple_extracted=false 또는 null (미처리)
439
+ // - triple_extracted_status='failed' (재시도 가능, 백오프 간격 확인 필요)
440
+ // - triple_extracted_status IS NULL (미처리)
441
+ // - abandoned 상태는 제외 (수동 재시도 필요)
442
+ const memories = DatabaseUtils.all(db, `
443
+ SELECT
444
+ id,
445
+ content,
446
+ importance,
447
+ triple_extracted,
448
+ triple_extracted_status,
449
+ triple_extraction_metadata
450
+ FROM memory_item
451
+ WHERE type = 'episodic'
452
+ AND (
453
+ triple_extracted IS NULL
454
+ OR triple_extracted = 0
455
+ OR triple_extracted_status = 'failed'
456
+ )
457
+ AND (triple_extracted_status IS NULL OR triple_extracted_status != 'abandoned')
458
+ ORDER BY created_at ASC
459
+ LIMIT ?
460
+ `, [limit]);
461
+ // 재시도 정책 적용: 백오프 간격이 지나지 않은 항목은 제외
462
+ const now = new Date();
463
+ const filteredMemories = memories.filter(memory => {
464
+ // 미처리 항목은 항상 포함
465
+ if (memory.triple_extracted_status === null || memory.triple_extracted_status === '') {
466
+ return true;
467
+ }
468
+ // failed 상태인 경우 재시도 정책 확인
469
+ if (memory.triple_extracted_status === 'failed') {
470
+ return this.shouldRetry(memory, now);
471
+ }
472
+ return true;
473
+ });
474
+ return filteredMemories;
475
+ }
476
+ /**
477
+ * 재시도 정책 확인
478
+ *
479
+ * @param memory Episodic Memory (triple_extraction_metadata 포함)
480
+ * @param now 현재 시간
481
+ * @returns 재시도 가능 여부
482
+ */
483
+ shouldRetry(memory, now = new Date()) {
484
+ // 최대 재시도 횟수 확인
485
+ const retryCount = this.getRetryCount(memory);
486
+ if (retryCount >= this.config.maxRetries) {
487
+ return false; // 최대 시도 횟수 초과
488
+ }
489
+ // 미처리 항목은 항상 재시도 가능
490
+ if (!memory.triple_extraction_metadata) {
491
+ return true;
492
+ }
493
+ try {
494
+ const metadata = JSON.parse(memory.triple_extraction_metadata);
495
+ const lastAttempt = metadata.last_attempt;
496
+ if (!lastAttempt) {
497
+ return true; // last_attempt가 없으면 재시도 가능
498
+ }
499
+ // 백오프 간격 확인 (지수 백오프: 1일, 2일, 4일)
500
+ const lastAttemptDate = new Date(lastAttempt);
501
+ const daysSinceLastAttempt = Math.floor((now.getTime() - lastAttemptDate.getTime()) / (24 * 60 * 60 * 1000));
502
+ // 현재 retry_count에 해당하는 백오프 간격 확인
503
+ // retry_count가 0이면 첫 번째 재시도 (1일), 1이면 두 번째 재시도 (2일), 2이면 세 번째 재시도 (4일)
504
+ const backoffDays = this.config.retryBackoffDays[retryCount] ??
505
+ this.config.retryBackoffDays[this.config.retryBackoffDays.length - 1] ?? 1;
506
+ // 백오프 간격이 지났으면 재시도 가능
507
+ return daysSinceLastAttempt >= backoffDays;
508
+ }
509
+ catch (error) {
510
+ // 메타데이터 파싱 실패 시 재시도 가능으로 간주
511
+ logger.warn('Failed to parse triple_extraction_metadata for retry check', {
512
+ memory_id: memory.id,
513
+ error: error instanceof Error ? error.message : String(error)
514
+ });
515
+ return true;
516
+ }
517
+ }
518
+ /**
519
+ * 재시도 횟수 조회
520
+ *
521
+ * @param memory Episodic Memory (triple_extraction_metadata 포함)
522
+ * @returns 재시도 횟수
523
+ */
524
+ getRetryCount(memory) {
525
+ if (!memory.triple_extraction_metadata) {
526
+ return 0; // 메타데이터가 없으면 재시도 횟수 0
527
+ }
528
+ try {
529
+ const metadata = JSON.parse(memory.triple_extraction_metadata);
530
+ return metadata.retry_count || 0;
531
+ }
532
+ catch (error) {
533
+ logger.warn('Failed to parse triple_extraction_metadata for retry count', {
534
+ memory_id: memory.id,
535
+ error: error instanceof Error ? error.message : String(error)
536
+ });
537
+ return 0;
538
+ }
539
+ }
540
+ /**
541
+ * 메모리 상태 업데이트
542
+ *
543
+ * PRD 1.4 재시도 종료 조건 및 필드 조합 규칙에 따라 상태를 업데이트합니다.
544
+ *
545
+ * 상태 전이 규칙:
546
+ * - 성공 시: triple_extracted=true, triple_extracted_status='success', 이전 실패 기록 초기화
547
+ * - 최대 시도 횟수 초과 시: triple_extracted=false, triple_extracted_status='abandoned', 재시도 중단
548
+ * - 재시도 가능 시: triple_extracted=false, triple_extracted_status='failed', 재시도 정보 저장
549
+ *
550
+ * 필드 조합 규칙 (PRD 1.4):
551
+ * | triple_extracted | triple_extracted_status | 의미 |
552
+ * | ---------------- | ----------------------- | ---- |
553
+ * | NULL | NULL | 미처리 |
554
+ * | true | 'success' | 성공 |
555
+ * | false | 'failed' | 실패 (재시도 가능) |
556
+ * | false | 'abandoned' | 포기 (수동 재시도 필요) |
557
+ * | false | NULL | 미처리 또는 초기 상태 |
558
+ *
559
+ * 이 메서드는 필드 조합 규칙을 준수하여 triple_extracted와 triple_extracted_status를 동기화합니다.
560
+ *
561
+ * @param db 데이터베이스 연결
562
+ * @param memoryId 메모리 ID
563
+ * @param status 상태 ('success' | 'failed' | 'abandoned')
564
+ * @param metadata 메타데이터
565
+ */
566
+ async updateMemoryStatus(db, memoryId, status, metadata) {
567
+ // 필드 조합 규칙에 따른 triple_extracted boolean 동기화
568
+ // PRD 1.4 필드 조합 규칙 준수:
569
+ // - 성공: triple_extracted=true, triple_extracted_status='success'
570
+ // - 실패: triple_extracted=false, triple_extracted_status='failed' (재시도 가능)
571
+ // - 포기: triple_extracted=false, triple_extracted_status='abandoned' (수동 재시도 필요)
572
+ //
573
+ // 상태 전이 시 항상 두 필드를 함께 업데이트하여 일관성 유지
574
+ // 이는 필드 조합 규칙을 보장하기 위한 필수 동기화 로직입니다.
575
+ const tripleExtracted = status === 'success' ? true : false;
576
+ // 필드 조합 규칙 준수: triple_extracted와 triple_extracted_status를 항상 함께 업데이트
577
+ // 이는 데이터 일관성을 보장하기 위한 필수 동기화 로직입니다.
578
+ // SQLite에서는 boolean을 INTEGER로 변환 (true=1, false=0)
579
+ await DatabaseUtils.run(db, `
580
+ UPDATE memory_item SET
581
+ triple_extracted = ?,
582
+ triple_extracted_status = ?,
583
+ triple_extraction_metadata = ?
584
+ WHERE id = ?
585
+ `, [
586
+ tripleExtracted ? 1 : 0, // SQLite에서는 boolean을 INTEGER로 변환
587
+ status,
588
+ JSON.stringify(metadata),
589
+ memoryId
590
+ ]);
591
+ // 상태 전이 로깅 (필드 조합 규칙 준수 확인)
592
+ logger.debug('Memory status updated with field combination rule', {
593
+ memory_id: memoryId,
594
+ triple_extracted: tripleExtracted,
595
+ triple_extracted_status: status,
596
+ field_combination_valid: ((status === 'success' && tripleExtracted === true) ||
597
+ ((status === 'failed' || status === 'abandoned') && tripleExtracted === false))
598
+ });
599
+ }
600
+ /**
601
+ * 평균 Confidence 계산
602
+ *
603
+ * memory_relation 테이블에서 해당 Episodic Memory에서 생성된 모든 관계의 confidence 값을 수집하여 평균 계산
604
+ *
605
+ * @param db 데이터베이스 연결
606
+ * @param episodicMemoryId Episodic Memory ID
607
+ * @returns 평균 Confidence (0.0~1.0), 관계가 없으면 null
608
+ */
609
+ async calculateAverageConfidence(db, episodicMemoryId) {
610
+ try {
611
+ // memory_relation에서 confidence 값 수집 (각 triple별로 저장됨)
612
+ const relations = DatabaseUtils.all(db, `
613
+ SELECT confidence FROM memory_relation
614
+ WHERE source_id = ? AND relation_type = 'extracted_from'
615
+ `, [episodicMemoryId]);
616
+ if (relations.length === 0) {
617
+ return null; // 관계가 없으면 null 반환
618
+ }
619
+ // confidence 값이 있는 관계만 필터링
620
+ const confidenceValues = relations
621
+ .map(rel => rel.confidence)
622
+ .filter((c) => c !== null && c !== undefined);
623
+ if (confidenceValues.length === 0) {
624
+ return null; // confidence 값이 없으면 null 반환
625
+ }
626
+ // 평균 계산
627
+ const average = confidenceValues.reduce((sum, c) => sum + c, 0) / confidenceValues.length;
628
+ return Math.min(1.0, Math.max(0.0, average)); // 0.0~1.0 범위로 정규화
629
+ }
630
+ catch (error) {
631
+ logger.warn('Failed to calculate average confidence', {
632
+ episodic_memory_id: episodicMemoryId,
633
+ error: error instanceof Error ? error.message : String(error)
634
+ });
635
+ return null;
636
+ }
637
+ }
638
+ }
639
+ //# sourceMappingURL=triple-extraction-batch-job.js.map