memento-mcp-server 1.13.1-a4 → 1.14.0-a

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 (108) hide show
  1. package/dist/database/migration/migrations/007-procedural-memory-enhancement.sql +66 -0
  2. package/dist/database/schema.sql +9 -2
  3. package/dist/domains/memory/tools/recall-tool.d.ts +13 -0
  4. package/dist/domains/memory/tools/recall-tool.d.ts.map +1 -1
  5. package/dist/domains/memory/tools/recall-tool.js +160 -12
  6. package/dist/domains/memory/tools/recall-tool.js.map +1 -1
  7. package/dist/domains/memory/tools/remember-tool.d.ts +10 -0
  8. package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
  9. package/dist/domains/memory/tools/remember-tool.js +264 -35
  10. package/dist/domains/memory/tools/remember-tool.js.map +1 -1
  11. package/dist/domains/relation/services/relation-quality-validator.d.ts.map +1 -1
  12. package/dist/domains/relation/services/relation-quality-validator.js +15 -10
  13. package/dist/domains/relation/services/relation-quality-validator.js.map +1 -1
  14. package/dist/domains/relation/services/rule-based-relation-extractor.d.ts.map +1 -1
  15. package/dist/domains/relation/services/rule-based-relation-extractor.js +18 -0
  16. package/dist/domains/relation/services/rule-based-relation-extractor.js.map +1 -1
  17. package/dist/domains/relation/tools/add-relation-tool.d.ts +3 -3
  18. package/dist/domains/relation/tools/add-relation-tool.js +2 -2
  19. package/dist/domains/relation/tools/add-relation-tool.js.map +1 -1
  20. package/dist/domains/relation/tools/get-relations-tool.d.ts +3 -3
  21. package/dist/domains/relation/tools/get-relations-tool.js +2 -2
  22. package/dist/domains/relation/tools/get-relations-tool.js.map +1 -1
  23. package/dist/domains/search/algorithms/hybrid-search-engine.d.ts +20 -0
  24. package/dist/domains/search/algorithms/hybrid-search-engine.d.ts.map +1 -1
  25. package/dist/domains/search/algorithms/hybrid-search-engine.js +168 -5
  26. package/dist/domains/search/algorithms/hybrid-search-engine.js.map +1 -1
  27. package/dist/domains/search/algorithms/search-engine.d.ts.map +1 -1
  28. package/dist/domains/search/algorithms/search-engine.js +37 -17
  29. package/dist/domains/search/algorithms/search-engine.js.map +1 -1
  30. package/dist/domains/search/algorithms/search-ranking.d.ts +15 -2
  31. package/dist/domains/search/algorithms/search-ranking.d.ts.map +1 -1
  32. package/dist/domains/search/algorithms/search-ranking.js +46 -15
  33. package/dist/domains/search/algorithms/search-ranking.js.map +1 -1
  34. package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
  35. package/dist/domains/search/repositories/vector-search.repository.js +180 -89
  36. package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
  37. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts +63 -0
  38. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts.map +1 -0
  39. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js +257 -0
  40. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js.map +1 -0
  41. package/dist/infrastructure/reflexion-worker.d.ts +18 -0
  42. package/dist/infrastructure/reflexion-worker.d.ts.map +1 -1
  43. package/dist/infrastructure/reflexion-worker.js +216 -0
  44. package/dist/infrastructure/reflexion-worker.js.map +1 -1
  45. package/dist/infrastructure/scheduler/batch-scheduler.d.ts +51 -8
  46. package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
  47. package/dist/infrastructure/scheduler/batch-scheduler.js +299 -205
  48. package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
  49. package/dist/infrastructure/scheduler/file-logger.d.ts +82 -0
  50. package/dist/infrastructure/scheduler/file-logger.d.ts.map +1 -0
  51. package/dist/infrastructure/scheduler/file-logger.js +133 -0
  52. package/dist/infrastructure/scheduler/file-logger.js.map +1 -0
  53. package/dist/infrastructure/scheduler/health-checker.d.ts +54 -0
  54. package/dist/infrastructure/scheduler/health-checker.d.ts.map +1 -0
  55. package/dist/infrastructure/scheduler/health-checker.js +96 -0
  56. package/dist/infrastructure/scheduler/health-checker.js.map +1 -0
  57. package/dist/infrastructure/scheduler/job-queue.d.ts +85 -0
  58. package/dist/infrastructure/scheduler/job-queue.d.ts.map +1 -0
  59. package/dist/infrastructure/scheduler/job-queue.js +125 -0
  60. package/dist/infrastructure/scheduler/job-queue.js.map +1 -0
  61. package/dist/infrastructure/scheduler/relation-validator-executor.d.ts +37 -0
  62. package/dist/infrastructure/scheduler/relation-validator-executor.d.ts.map +1 -0
  63. package/dist/infrastructure/scheduler/relation-validator-executor.js +120 -0
  64. package/dist/infrastructure/scheduler/relation-validator-executor.js.map +1 -0
  65. package/dist/infrastructure/scheduler/retry-manager.d.ts +62 -0
  66. package/dist/infrastructure/scheduler/retry-manager.d.ts.map +1 -0
  67. package/dist/infrastructure/scheduler/retry-manager.js +91 -0
  68. package/dist/infrastructure/scheduler/retry-manager.js.map +1 -0
  69. package/dist/npm-client/utils.d.ts.map +1 -1
  70. package/dist/npm-client/utils.js +2 -1
  71. package/dist/npm-client/utils.js.map +1 -1
  72. package/dist/server/http-server.d.ts.map +1 -1
  73. package/dist/server/http-server.js +15 -17
  74. package/dist/server/http-server.js.map +1 -1
  75. package/dist/services/anchor-manager.d.ts.map +1 -1
  76. package/dist/services/anchor-manager.js.map +1 -1
  77. package/dist/shared/types/index.d.ts +36 -0
  78. package/dist/shared/types/index.d.ts.map +1 -1
  79. package/dist/shared/types/index.js.map +1 -1
  80. package/dist/shared/types/relation.d.ts +1 -1
  81. package/dist/shared/types/relation.d.ts.map +1 -1
  82. package/dist/shared/types/relation.js +7 -4
  83. package/dist/shared/types/relation.js.map +1 -1
  84. package/dist/shared/utils/database.d.ts.map +1 -1
  85. package/dist/shared/utils/database.js +9 -2
  86. package/dist/shared/utils/database.js.map +1 -1
  87. package/dist/shared/utils/procedural-memory-extractor.d.ts +108 -0
  88. package/dist/shared/utils/procedural-memory-extractor.d.ts.map +1 -0
  89. package/dist/shared/utils/procedural-memory-extractor.js +581 -0
  90. package/dist/shared/utils/procedural-memory-extractor.js.map +1 -0
  91. package/dist/shared/utils/relation-type-converter.d.ts +52 -0
  92. package/dist/shared/utils/relation-type-converter.d.ts.map +1 -0
  93. package/dist/shared/utils/relation-type-converter.js +106 -0
  94. package/dist/shared/utils/relation-type-converter.js.map +1 -0
  95. package/dist/shared/utils/type-param-validator.d.ts +31 -0
  96. package/dist/shared/utils/type-param-validator.d.ts.map +1 -1
  97. package/dist/shared/utils/type-param-validator.js +90 -2
  98. package/dist/shared/utils/type-param-validator.js.map +1 -1
  99. package/dist/tools/base-tool.d.ts.map +1 -1
  100. package/dist/tools/types.d.ts +4 -0
  101. package/dist/tools/types.d.ts.map +1 -1
  102. package/dist/tools/types.js +5 -0
  103. package/dist/tools/types.js.map +1 -1
  104. package/dist/workers/consolidation-score-worker.d.ts.map +1 -1
  105. package/dist/workers/consolidation-score-worker.js +0 -2
  106. package/dist/workers/consolidation-score-worker.js.map +1 -1
  107. package/package.json +3 -3
  108. package/scripts/auto-setup.js +1 -1
@@ -6,14 +6,15 @@
6
6
  import { ForgettingPolicyService } from '../../domains/forgetting/services/forgetting-policy-service.js';
7
7
  import { getPerformanceMonitor } from '../../domains/monitoring/services/performance-monitor.js';
8
8
  import Database from 'better-sqlite3';
9
- import fs from 'fs';
10
- import path from 'path';
11
9
  import { ConsolidationScoreWorker } from '../../workers/consolidation-score-worker.js';
12
10
  import { ReflexionWorker } from '../reflexion-worker.js';
13
11
  import { mementoConfig } from '../../shared/config/index.js';
14
12
  import { mcpLogger } from '../../server/mcp-logger.js';
15
- import { spawn } from 'child_process';
16
- import { join } from 'path';
13
+ import { JobQueue } from './job-queue.js';
14
+ import { RetryManager } from './retry-manager.js';
15
+ import { HealthChecker } from './health-checker.js';
16
+ import { FileLogger } from './file-logger.js';
17
+ import { RelationValidatorExecutor } from './relation-validator-executor.js';
17
18
  export class BatchScheduler {
18
19
  config;
19
20
  forgettingService;
@@ -26,11 +27,14 @@ export class BatchScheduler {
26
27
  startTime = null;
27
28
  lastExecution = new Map();
28
29
  totalExecutions = new Map();
29
- errorCount = new Map();
30
- runningJobs = new Set();
31
- jobQueue = [];
32
30
  jobProcessorInterval = null;
33
- constructor(config) {
31
+ // 분리된 모듈들 (DI)
32
+ jobQueue;
33
+ retryManager;
34
+ healthChecker;
35
+ fileLogger;
36
+ relationValidatorExecutor;
37
+ constructor(config, dependencies) {
34
38
  this.config = {
35
39
  cleanupInterval: 60 * 60 * 1000, // 1시간
36
40
  monitoringInterval: 5 * 60 * 1000, // 5분
@@ -49,6 +53,7 @@ export class BatchScheduler {
49
53
  jobTimeout: 5 * 60 * 1000, // 5분
50
54
  retryAttempts: 3,
51
55
  retryDelay: 1000, // 1초
56
+ weeklyRelationValidationTimeout: undefined, // 기본값: jobTimeout 사용
52
57
  ...config
53
58
  };
54
59
  // 생성자에서 설정 검증
@@ -59,6 +64,20 @@ export class BatchScheduler {
59
64
  if (mementoConfig.consolidationScoreEnabled) {
60
65
  this.consolidationScoreWorker = new ConsolidationScoreWorker();
61
66
  }
67
+ // 분리된 모듈들 초기화 (DI 또는 기본 생성)
68
+ this.jobQueue = dependencies?.jobQueue ?? new JobQueue();
69
+ this.retryManager = dependencies?.retryManager ?? new RetryManager({
70
+ maxAttempts: this.config.retryAttempts,
71
+ baseDelay: this.config.retryDelay,
72
+ maxErrorCount: this.config.retryAttempts * 3
73
+ });
74
+ this.healthChecker = dependencies?.healthChecker ?? new HealthChecker();
75
+ this.fileLogger = dependencies?.fileLogger ?? new FileLogger({
76
+ enabled: this.config.enableLogging
77
+ });
78
+ this.relationValidatorExecutor = dependencies?.relationValidatorExecutor ?? new RelationValidatorExecutor({
79
+ timeout: this.config.weeklyRelationValidationTimeout ?? this.config.jobTimeout
80
+ });
62
81
  }
63
82
  /**
64
83
  * 스케줄러 시작
@@ -73,6 +92,15 @@ export class BatchScheduler {
73
92
  this.db = db;
74
93
  this.isRunning = true;
75
94
  this.startTime = new Date();
95
+ // 재시작 시 큐 초기화 (이전 세션의 작업이 남아있을 수 있음)
96
+ if (this.jobQueue.size > 0) {
97
+ this.log(`Clearing ${this.jobQueue.size} leftover jobs from previous session`, {
98
+ leftoverJobs: this.jobQueue.size
99
+ });
100
+ this.jobQueue.clear();
101
+ }
102
+ // 헬스체크 시작 시간 설정
103
+ this.healthChecker.setStartTime(this.startTime);
76
104
  // 성능 모니터 초기화
77
105
  this.performanceMonitor.initialize(db);
78
106
  // Reflexion Worker 통합 (Phase 2)
@@ -107,6 +135,7 @@ export class BatchScheduler {
107
135
  }
108
136
  /**
109
137
  * 스케줄러 중지
138
+ * 재시작 시 의도하지 않은 배치 실행과 상태 오염을 방지하기 위해 큐를 비움
110
139
  */
111
140
  async stop() {
112
141
  if (!this.isRunning) {
@@ -127,8 +156,17 @@ export class BatchScheduler {
127
156
  }
128
157
  // 실행 중인 작업 완료 대기
129
158
  await this.waitForRunningJobs();
159
+ // 큐에 남아있는 작업 제거 (재시작 시 의도하지 않은 실행 방지)
160
+ const queuedJobsCount = this.jobQueue.size;
161
+ if (queuedJobsCount > 0) {
162
+ this.log(`Clearing ${queuedJobsCount} queued jobs to prevent unintended execution on restart`, {
163
+ queuedJobs: queuedJobsCount
164
+ });
165
+ this.jobQueue.clear(); // 큐 비우기
166
+ }
130
167
  this.log('BatchScheduler stopped', {
131
- uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
168
+ uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0,
169
+ clearedQueuedJobs: queuedJobsCount
132
170
  });
133
171
  }
134
172
  /**
@@ -150,95 +188,149 @@ export class BatchScheduler {
150
188
  if (this.config.jobTimeout < 1000) {
151
189
  throw new Error('jobTimeout must be at least 1 second');
152
190
  }
191
+ // weeklyRelationValidationTimeout 검증 (설정된 경우에만)
192
+ if (this.config.weeklyRelationValidationTimeout !== undefined) {
193
+ if (typeof this.config.weeklyRelationValidationTimeout !== 'number' ||
194
+ isNaN(this.config.weeklyRelationValidationTimeout) ||
195
+ this.config.weeklyRelationValidationTimeout <= 0) {
196
+ throw new Error('weeklyRelationValidationTimeout must be a positive number (at least 1 second)');
197
+ }
198
+ if (this.config.weeklyRelationValidationTimeout < 1000) {
199
+ throw new Error('weeklyRelationValidationTimeout must be at least 1 second');
200
+ }
201
+ }
153
202
  }
154
203
  /**
155
- * 작업 스케줄링
204
+ * 작업 실행 래퍼 (타임아웃, 상태 관리, 재시도 포함)
205
+ * 재시도 큐에서도 동일한 래퍼를 사용하여 타임아웃/상태 관리가 적용되도록 함
206
+ *
207
+ * @param name 작업 이름
208
+ * @param job 실행할 작업 함수
209
+ * @param priority 우선순위 (재시도 시 사용)
210
+ * @param initialRetryCount 초기 재시도 횟수 (기본값: 0)
211
+ * @returns 실행 결과
156
212
  */
157
- scheduleJob(name, interval, job, priority) {
158
- const wrappedJob = async () => {
159
- if (this.runningJobs.has(name)) {
160
- this.log(`Job ${name} is already running, skipping`, { level: 'warn' });
213
+ async executeJobWithRetry(name, job, priority, initialRetryCount = 0) {
214
+ // 이미 실행 중인 작업은 큐에 남겨두어 다음 턴에 실행되도록 함
215
+ // (스킵하면 주기적 실행이 누락될 수 있음)
216
+ // 단, 중복 방지를 위해 동일 이름의 잡이 이미 큐에 있으면 추가하지 않음
217
+ if (this.jobQueue.isRunning(name)) {
218
+ const added = this.addJobToQueue(name, job, priority, initialRetryCount);
219
+ if (!added) {
220
+ // 이미 큐에 있으면 스킵
161
221
  return;
162
222
  }
163
- this.runningJobs.add(name);
164
- const startTime = Date.now();
165
- let retryCount = 0;
166
- const executeWithRetry = async () => {
167
- try {
168
- await this.executeWithTimeout(job, this.config.jobTimeout);
169
- this.lastExecution.set(name, new Date());
170
- this.totalExecutions.set(name, (this.totalExecutions.get(name) || 0) + 1);
171
- // 성공시 에러 카운트 리셋
172
- this.errorCount.set(name, 0);
173
- this.log(`Job ${name} completed successfully`, {
174
- duration: Date.now() - startTime,
175
- totalExecutions: this.totalExecutions.get(name),
176
- retryCount
177
- });
178
- }
179
- catch (error) {
180
- retryCount++;
181
- const totalErrorCount = (this.errorCount.get(name) || 0) + 1;
182
- this.errorCount.set(name, totalErrorCount);
183
- const errorInfo = {
184
- error: error instanceof Error ? error.message : String(error),
185
- stack: error instanceof Error ? error.stack : undefined,
186
- errorCount: totalErrorCount,
187
- retryCount,
188
- duration: Date.now() - startTime
189
- };
190
- this.log(`Job ${name} failed`, errorInfo, 'error');
191
- // 재시도 로직
192
- if (retryCount <= this.config.retryAttempts) {
193
- const retryDelay = this.config.retryDelay * Math.pow(2, retryCount - 1); // 지수 백오프
194
- this.log(`Retrying job ${name} in ${retryDelay}ms`, {
195
- attempt: retryCount,
196
- totalAttempts: this.config.retryAttempts,
197
- nextRetryDelay: retryDelay
198
- });
199
- setTimeout(() => {
200
- if (this.isRunning) { // 스케줄러가 여전히 실행 중인지 확인
201
- this.jobQueue.push({ name, job, priority });
202
- }
203
- }, retryDelay);
204
- }
205
- else {
206
- this.log(`Job ${name} failed permanently after ${retryCount} attempts`, {
207
- totalErrorCount,
208
- finalError: errorInfo
209
- }, 'error');
210
- // 심각한 에러의 경우 스케줄러 상태 확인
211
- if (totalErrorCount > this.config.retryAttempts * 2) {
212
- this.log(`Job ${name} has too many consecutive failures, checking scheduler health`, { level: 'warn' });
213
- await this.checkSchedulerHealth();
214
- }
223
+ this.log(`Job ${name} is already running, will retry after completion`, { level: 'debug' });
224
+ return;
225
+ }
226
+ this.jobQueue.markRunning(name);
227
+ const startTime = Date.now();
228
+ let retryCount = initialRetryCount;
229
+ try {
230
+ await this.executeWithTimeout(job, this.config.jobTimeout);
231
+ this.lastExecution.set(name, new Date());
232
+ this.totalExecutions.set(name, (this.totalExecutions.get(name) || 0) + 1);
233
+ // 성공시 에러 카운트 리셋 (RetryManager 사용)
234
+ this.retryManager.resetErrorCount(name);
235
+ this.log(`Job ${name} completed successfully`, {
236
+ duration: Date.now() - startTime,
237
+ totalExecutions: this.totalExecutions.get(name),
238
+ retryCount
239
+ });
240
+ }
241
+ catch (error) {
242
+ retryCount++;
243
+ const totalErrorCount = this.retryManager.incrementErrorCount(name);
244
+ const errorInfo = {
245
+ error: error instanceof Error ? error.message : String(error),
246
+ stack: error instanceof Error ? error.stack : undefined,
247
+ errorCount: totalErrorCount,
248
+ retryCount,
249
+ duration: Date.now() - startTime
250
+ };
251
+ this.log(`Job ${name} failed`, errorInfo, 'error');
252
+ // RetryManager를 사용하여 재시도 여부 결정
253
+ const retryResult = this.retryManager.shouldRetry(name, retryCount, totalErrorCount);
254
+ if (retryResult.exceededMaxErrors) {
255
+ this.log(`Job ${name} exceeded maximum error count (${totalErrorCount}), stopping retries`, {
256
+ totalErrorCount,
257
+ finalError: errorInfo
258
+ }, 'error');
259
+ // 심각한 에러의 경우 스케줄러 상태 확인
260
+ this.log(`Job ${name} has too many consecutive failures, checking scheduler health`, { level: 'warn' });
261
+ await this.checkSchedulerHealth();
262
+ return;
263
+ }
264
+ // 재시도 로직
265
+ if (retryResult.shouldRetry) {
266
+ this.log(`Retrying job ${name} in ${retryResult.nextRetryDelay}ms`, {
267
+ attempt: retryResult.retryCount,
268
+ totalAttempts: this.config.retryAttempts,
269
+ nextRetryDelay: retryResult.nextRetryDelay,
270
+ totalErrorCount
271
+ });
272
+ setTimeout(() => {
273
+ if (this.isRunning) { // 스케줄러가 여전히 실행 중인지 확인
274
+ // 재시도 시 retryCount를 큐 항목에 저장하여 다음 실행 시 전달 (중복 방지 포함)
275
+ this.addJobToQueue(name, job, priority, retryResult.retryCount);
215
276
  }
277
+ }, retryResult.nextRetryDelay);
278
+ }
279
+ else {
280
+ this.log(`Job ${name} failed permanently after ${retryCount} attempts`, {
281
+ totalErrorCount,
282
+ finalError: errorInfo
283
+ }, 'error');
284
+ // 심각한 에러의 경우 스케줄러 상태 확인
285
+ if (totalErrorCount > this.config.retryAttempts * 2) {
286
+ this.log(`Job ${name} has too many consecutive failures, checking scheduler health`, { level: 'warn' });
287
+ await this.checkSchedulerHealth();
216
288
  }
217
- };
218
- // 실행
219
- executeWithRetry().finally(() => {
220
- this.runningJobs.delete(name);
221
- });
289
+ }
290
+ }
291
+ finally {
292
+ this.jobQueue.markCompleted(name);
293
+ }
294
+ }
295
+ /**
296
+ * 큐에 작업 추가 (중복 방지 포함)
297
+ * 동일 이름의 잡이 이미 큐에 있거나 실행 중이면 추가하지 않음
298
+ */
299
+ addJobToQueue(name, job, priority, retryCount = 0) {
300
+ return this.jobQueue.add(name, job, priority, retryCount);
301
+ }
302
+ /**
303
+ * 작업 스케줄링
304
+ * 시작 시 maxConcurrentJobs를 보장하기 위해 무조건 큐를 통해 실행
305
+ * 여러 작업이 동시에 시작될 때 race condition을 방지하기 위함
306
+ */
307
+ scheduleJob(name, interval, job, priority) {
308
+ const wrappedJob = async () => {
309
+ // 주기적 실행도 큐를 통해 실행하여 maxConcurrentJobs 보장 (중복 방지 포함)
310
+ this.addJobToQueue(name, job, priority, 0);
222
311
  };
223
- // 즉시 실행
224
- wrappedJob();
225
- // 주기적 실행
312
+ // 즉시 실행도 큐를 통해 실행 (maxConcurrentJobs 보장, 중복 방지 포함)
313
+ // 여러 작업이 동시에 시작될 때 race condition 방지
314
+ this.addJobToQueue(name, job, priority, 0);
315
+ // 주기적 실행도 큐를 통해 실행
226
316
  const intervalId = setInterval(wrappedJob, interval);
227
317
  this.intervals.set(name, intervalId);
228
318
  }
229
319
  /**
230
320
  * 작업 큐 처리기 시작
321
+ * 재시도 큐에서도 동일한 래퍼(타임아웃+상태 관리)를 사용하도록 수정
231
322
  */
232
323
  startJobProcessor() {
233
324
  const processQueue = async () => {
234
- if (this.jobQueue.length === 0 || this.runningJobs.size >= this.config.maxConcurrentJobs) {
325
+ if (this.jobQueue.isEmpty || this.jobQueue.runningCount >= this.config.maxConcurrentJobs) {
235
326
  return;
236
327
  }
237
- // 우선순위 순으로 정렬
238
- this.jobQueue.sort((a, b) => a.priority - b.priority);
239
- const nextJob = this.jobQueue.shift();
328
+ const nextJob = this.jobQueue.getNext();
240
329
  if (nextJob) {
241
- await nextJob.job();
330
+ // 재시도 큐에서도 동일한 래퍼를 사용하여 타임아웃/상태 관리가 적용되도록 함
331
+ // 재시도 시 저장된 retryCount를 사용하여 무한 재시도 방지
332
+ const retryCount = nextJob.retryCount ?? 0;
333
+ await this.executeJobWithRetry(nextJob.name, nextJob.job, nextJob.priority, retryCount);
242
334
  }
243
335
  };
244
336
  // 큐 처리 인터벌 (ID 저장)
@@ -261,11 +353,12 @@ export class BatchScheduler {
261
353
  async waitForRunningJobs() {
262
354
  const maxWaitTime = 30000; // 30초
263
355
  const startTime = Date.now();
264
- while (this.runningJobs.size > 0 && (Date.now() - startTime) < maxWaitTime) {
356
+ while (this.jobQueue.runningCount > 0 && (Date.now() - startTime) < maxWaitTime) {
357
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
265
358
  await new Promise(resolve => setTimeout(resolve, 100));
266
359
  }
267
- if (this.runningJobs.size > 0) {
268
- this.log(`Warning: ${this.runningJobs.size} jobs still running after timeout`, { level: 'warn' });
360
+ if (this.jobQueue.runningCount > 0) {
361
+ this.log(`Warning: ${this.jobQueue.runningCount} jobs still running after timeout`, { level: 'warn' });
269
362
  }
270
363
  }
271
364
  /**
@@ -391,27 +484,17 @@ export class BatchScheduler {
391
484
  warnings: []
392
485
  };
393
486
  try {
394
- if (!this.db) {
395
- throw new Error('Database not initialized');
396
- }
397
- // 데이터베이스 연결 확인
398
- await this.db.prepare('SELECT 1').get();
399
- // 메모리 사용량 확인
400
- const memUsage = process.memoryUsage();
401
- const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
402
- if (memUsagePercent > 90) {
403
- result.warnings.push(`High memory usage: ${memUsagePercent.toFixed(1)}%`);
404
- }
405
- // 실행 중인 작업 수 확인
406
- if (this.runningJobs.size > this.config.maxConcurrentJobs * 0.8) {
407
- result.warnings.push(`High job concurrency: ${this.runningJobs.size}/${this.config.maxConcurrentJobs}`);
408
- }
409
- result.success = true;
487
+ // HealthChecker를 사용하여 헬스체크 실행
488
+ const healthResult = await this.healthChecker.check(this.db, this.jobQueue.runningCount, this.jobQueue.size, this.config.maxConcurrentJobs);
489
+ result.success = healthResult.isHealthy;
410
490
  result.processed = 1;
491
+ result.warnings = healthResult.warnings;
492
+ result.errors = healthResult.errors;
411
493
  result.details = {
412
- memoryUsage: memUsagePercent,
413
- runningJobs: this.runningJobs.size,
414
- uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
494
+ memoryUsage: healthResult.memoryUsage,
495
+ runningJobs: healthResult.runningJobs,
496
+ queueSize: healthResult.queueSize,
497
+ uptime: healthResult.uptime
415
498
  };
416
499
  }
417
500
  catch (error) {
@@ -477,9 +560,10 @@ export class BatchScheduler {
477
560
  /**
478
561
  * Consolidation Score 전체 스윕 작업 스케줄링
479
562
  * 지정된 시간에 하루 1회 실행
563
+ * 큐를 통해 실행하여 maxConcurrentJobs, 타임아웃, 재시도, lastExecution 기록이 적용되도록 함
480
564
  */
481
565
  scheduleConsolidationScoreFullSweep() {
482
- const checkAndRun = async () => {
566
+ const checkAndRun = () => {
483
567
  const now = new Date();
484
568
  const currentHour = now.getHours();
485
569
  // 지정된 시간에 실행
@@ -488,7 +572,9 @@ export class BatchScheduler {
488
572
  const lastExecution = this.lastExecution.get('consolidation_score_full_sweep');
489
573
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
490
574
  if (!lastExecution || lastExecution < today) {
491
- await this.runConsolidationScoreFullSweep();
575
+ // 큐를 통해 실행하여 maxConcurrentJobs, 타임아웃, 재시도, lastExecution 기록이 적용되도록 함 (중복 방지 포함)
576
+ this.addJobToQueue('consolidation_score_full_sweep', async () => { await this.runConsolidationScoreFullSweep(); }, 4, // consolidation_score_incremental과 동일한 우선순위
577
+ 0);
492
578
  }
493
579
  }
494
580
  };
@@ -502,9 +588,10 @@ export class BatchScheduler {
502
588
  }
503
589
  /**
504
590
  * 주간 관계 추출 품질 검증 작업 스케줄링
591
+ * 큐를 통해 실행하여 maxConcurrentJobs, 타임아웃, 재시도, lastExecution 기록이 적용되도록 함
505
592
  */
506
593
  scheduleWeeklyRelationValidation() {
507
- const checkAndRun = async () => {
594
+ const checkAndRun = () => {
508
595
  const now = new Date();
509
596
  const currentDayOfWeek = now.getDay(); // 0=일요일, 6=토요일
510
597
  const currentHour = now.getHours();
@@ -515,7 +602,9 @@ export class BatchScheduler {
515
602
  const lastExecution = this.lastExecution.get('weekly_relation_validation');
516
603
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
517
604
  if (!lastExecution || lastExecution < today) {
518
- await this.runWeeklyRelationValidation();
605
+ // 큐를 통해 실행하여 maxConcurrentJobs, 타임아웃, 재시도, lastExecution 기록이 적용되도록 함 (중복 방지 포함)
606
+ this.addJobToQueue('weekly_relation_validation', async () => { await this.runWeeklyRelationValidation(); }, 5, // 다른 작업보다 낮은 우선순위
607
+ 0);
519
608
  }
520
609
  }
521
610
  };
@@ -528,6 +617,7 @@ export class BatchScheduler {
528
617
  }
529
618
  /**
530
619
  * 주간 관계 추출 품질 검증 실행
620
+ * 타임아웃 및 강제 종료 로직 포함
531
621
  */
532
622
  async runWeeklyRelationValidation() {
533
623
  const startTime = new Date();
@@ -543,43 +633,29 @@ export class BatchScheduler {
543
633
  };
544
634
  try {
545
635
  this.log('Starting weekly relation validation...');
546
- // 주간 검증 스크립트 실행
547
- const scriptPath = join(process.cwd(), 'scripts', 'weekly-relation-validation.ts');
548
- const scriptArgs = ['--method', 'hybrid', '--allow-soft-fail'];
549
- const childProcess = spawn('npx', ['tsx', scriptPath, ...scriptArgs], {
550
- cwd: process.cwd(),
551
- stdio: ['ignore', 'pipe', 'pipe'],
552
- env: { ...process.env }
553
- });
554
- let stdout = '';
555
- let stderr = '';
556
- childProcess.stdout?.on('data', (data) => {
557
- stdout += data.toString();
558
- });
559
- childProcess.stderr?.on('data', (data) => {
560
- stderr += data.toString();
561
- });
562
- await new Promise((resolve, reject) => {
563
- childProcess.on('close', (code) => {
564
- if (code === 0) {
565
- resolve();
566
- }
567
- else {
568
- reject(new Error(`Script exited with code ${code}\n${stderr}`));
569
- }
570
- });
571
- childProcess.on('error', (error) => {
572
- reject(error);
573
- });
574
- });
575
- result.success = true;
636
+ // RelationValidatorExecutor를 사용하여 스크립트 실행
637
+ const timeout = this.config.weeklyRelationValidationTimeout ?? this.config.jobTimeout;
638
+ const executorResult = await this.relationValidatorExecutor.execute([], timeout);
639
+ result.success = executorResult.success;
576
640
  result.endTime = new Date();
577
- result.duration = result.endTime.getTime() - startTime.getTime();
641
+ result.duration = executorResult.duration;
578
642
  result.processed = 1;
579
- this.log('Weekly relation validation completed successfully', {
580
- duration: result.duration,
581
- stdout: stdout.substring(0, 500) // 처음 500자만 로그
582
- });
643
+ if (executorResult.error) {
644
+ result.errors.push(executorResult.error);
645
+ }
646
+ if (executorResult.success) {
647
+ this.log('Weekly relation validation completed successfully', {
648
+ duration: result.duration,
649
+ stdout: executorResult.stdout.substring(0, 500) // 처음 500자만 로그
650
+ });
651
+ }
652
+ else {
653
+ this.log('Weekly relation validation failed', {
654
+ error: executorResult.error,
655
+ duration: result.duration,
656
+ stderr: executorResult.stderr.substring(0, 500)
657
+ }, 'error');
658
+ }
583
659
  }
584
660
  catch (error) {
585
661
  result.endTime = new Date();
@@ -668,6 +744,7 @@ export class BatchScheduler {
668
744
  totalMemories: totalMemories.count,
669
745
  estimatedSize: dbSize.page_count * pageSize.page_size
670
746
  };
747
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
671
748
  }
672
749
  catch (error) {
673
750
  this.log('Failed to collect database stats:', error, 'warn');
@@ -676,6 +753,7 @@ export class BatchScheduler {
676
753
  }
677
754
  /**
678
755
  * 로깅
756
+ * data 객체에 level 속성이 있으면 이를 우선적으로 사용하여 호출부의 편의성을 높임
679
757
  */
680
758
  log(message, data, level = 'info') {
681
759
  if (!this.config.enableLogging)
@@ -683,6 +761,7 @@ export class BatchScheduler {
683
761
  // 배치 작업 컨텍스트 정보 추가
684
762
  // Error 객체는 non-enumerable 속성을 가지므로 명시적으로 처리 필요
685
763
  let safeData;
764
+ let actualLevel = level;
686
765
  if (data instanceof Error) {
687
766
  // Error 객체의 속성을 명시적으로 추출 (non-enumerable 속성 포함)
688
767
  safeData = {
@@ -693,7 +772,17 @@ export class BatchScheduler {
693
772
  }
694
773
  else if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
695
774
  // 일반 객체는 spread 가능
696
- safeData = data;
775
+ safeData = { ...data };
776
+ // data.level이 있으면 이를 우선적으로 사용 (호출부 편의성)
777
+ if ('level' in safeData && typeof safeData.level === 'string') {
778
+ const dataLevel = safeData.level.toLowerCase();
779
+ // 'debug'는 'info'로 변환 (mcpLogger가 debug를 지원하지 않을 수 있음)
780
+ if (dataLevel === 'debug' || dataLevel === 'info' || dataLevel === 'warn' || dataLevel === 'error') {
781
+ actualLevel = dataLevel === 'debug' ? 'info' : dataLevel;
782
+ }
783
+ // level 속성은 제거 (중복 방지)
784
+ delete safeData.level;
785
+ }
697
786
  }
698
787
  else {
699
788
  // 원시 타입이나 배열은 빈 객체로 처리
@@ -702,43 +791,34 @@ export class BatchScheduler {
702
791
  const batchContext = {
703
792
  ...safeData,
704
793
  uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0,
705
- activeJobs: this.runningJobs.size,
706
- queueSize: this.jobQueue.length
794
+ activeJobs: this.jobQueue.runningCount,
795
+ queueSize: this.jobQueue.size
707
796
  };
708
797
  // MCP 로거 사용
709
- mcpLogger.logBatch(level, message, batchContext);
710
- // 에러 로그는 파일에도 저장
711
- if (level === 'error') {
712
- const logEntry = {
713
- timestamp: new Date(),
714
- service: 'BatchScheduler',
715
- level,
716
- message,
717
- data: batchContext,
798
+ mcpLogger.logBatch(actualLevel, message, batchContext);
799
+ // 에러/경고 로그는 파일에도 저장 (FileLogger 사용, 비동기이므로 await 없이 fire-and-forget)
800
+ // warn과 error 구분하여 원본 레벨을 보존
801
+ if (actualLevel === 'warn') {
802
+ // 비동기 로깅이지만 await하지 않음 (로깅 실패가 작업 실패로 이어지지 않도록)
803
+ this.fileLogger.logWarn(message, batchContext, {
718
804
  uptime: batchContext.uptime,
719
805
  activeJobs: batchContext.activeJobs,
720
806
  queueSize: batchContext.queueSize
721
- };
722
- this.logToFile(logEntry);
723
- }
724
- }
725
- /**
726
- * 파일 로깅 (에러 로그)
727
- */
728
- logToFile(logEntry) {
729
- try {
730
- const logDir = path.join(process.cwd(), 'logs');
731
- // 로그 디렉토리 생성
732
- if (!fs.existsSync(logDir)) {
733
- fs.mkdirSync(logDir, { recursive: true });
734
- }
735
- const logFile = path.join(logDir, 'batch-scheduler.log');
736
- const logLine = JSON.stringify(logEntry) + '\n';
737
- fs.appendFileSync(logFile, logLine);
807
+ }).catch((error) => {
808
+ // 파일 로깅 실패는 콘솔에만 기록 (무한 루프 방지)
809
+ console.error('File logging failed:', error);
810
+ });
738
811
  }
739
- catch (error) {
740
- // 파일 로깅 실패는 무시 (MCP 로거 사용)
741
- mcpLogger.logBatch('warn', 'Failed to write to log file', { error: error instanceof Error ? error.message : String(error) });
812
+ else if (actualLevel === 'error') {
813
+ // 비동기 로깅이지만 await하지 않음 (로깅 실패가 작업 실패로 이어지지 않도록)
814
+ this.fileLogger.logError(message, batchContext, {
815
+ uptime: batchContext.uptime,
816
+ activeJobs: batchContext.activeJobs,
817
+ queueSize: batchContext.queueSize
818
+ }).catch((error) => {
819
+ // 파일 로깅 실패는 콘솔에만 기록 (무한 루프 방지)
820
+ console.error('File logging failed:', error);
821
+ });
742
822
  }
743
823
  }
744
824
  /**
@@ -771,29 +851,47 @@ export class BatchScheduler {
771
851
  }
772
852
  /**
773
853
  * 수동으로 작업 실행
854
+ * 직접 실행하되 lastExecution과 totalExecutions을 기록함
774
855
  */
775
856
  async runJob(jobType) {
857
+ let result;
776
858
  switch (jobType) {
777
859
  case 'cleanup':
778
- return await this.runMemoryCleanup();
860
+ result = await this.runMemoryCleanup();
861
+ break;
779
862
  case 'monitoring':
780
- return await this.runMonitoring();
863
+ result = await this.runMonitoring();
864
+ break;
781
865
  case 'healthcheck':
782
- return await this.runHealthCheck();
866
+ result = await this.runHealthCheck();
867
+ break;
783
868
  default:
784
869
  throw new Error(`Unknown job type: ${jobType}`);
785
870
  }
871
+ // lastExecution과 totalExecutions 업데이트 (큐를 통한 실행과 일관성 유지)
872
+ this.lastExecution.set(jobType, new Date());
873
+ this.totalExecutions.set(jobType, (this.totalExecutions.get(jobType) || 0) + 1);
874
+ return result;
786
875
  }
787
876
  /**
788
877
  * 스케줄러 상태 확인
789
878
  */
790
879
  getStatus() {
880
+ // RetryManager에서 errorCount를 가져와서 Map으로 변환
881
+ const errorCountMap = new Map();
882
+ // 모든 작업 이름에 대해 errorCount 조회 (intervals의 키 사용)
883
+ for (const jobName of this.intervals.keys()) {
884
+ const errorCount = this.retryManager.getErrorCount(jobName);
885
+ if (errorCount > 0) {
886
+ errorCountMap.set(jobName, errorCount);
887
+ }
888
+ }
791
889
  return {
792
890
  isRunning: this.isRunning,
793
891
  activeJobs: Array.from(this.intervals.keys()),
794
892
  lastExecution: new Map(this.lastExecution),
795
893
  totalExecutions: new Map(this.totalExecutions),
796
- errorCount: new Map(this.errorCount),
894
+ errorCount: errorCountMap,
797
895
  uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0,
798
896
  config: { ...this.config }
799
897
  };
@@ -846,34 +944,27 @@ export class BatchScheduler {
846
944
  async checkSchedulerHealth() {
847
945
  try {
848
946
  this.log('Performing scheduler health check...');
849
- // 데이터베이스 연결 확인
850
- if (this.db) {
851
- await this.db.prepare('SELECT 1').get();
947
+ // HealthChecker를 사용하여 헬스체크 실행
948
+ const healthResult = await this.healthChecker.check(this.db, this.jobQueue.runningCount, this.jobQueue.size, this.config.maxConcurrentJobs);
949
+ // 경고가 있으면 로깅
950
+ if (healthResult.warnings.length > 0) {
951
+ healthResult.warnings.forEach(warning => {
952
+ this.log(warning, { level: 'warn' });
953
+ });
852
954
  }
853
- // 메모리 사용량 확인
854
- const memUsage = process.memoryUsage();
855
- const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
856
- if (memUsagePercent > 90) {
857
- this.log(`High memory usage detected: ${memUsagePercent.toFixed(1)}%`, { level: 'warn' });
858
- // 메모리 정리 시도
859
- if (global.gc) {
860
- global.gc();
955
+ // 메모리 사용량이 높으면 가비지 컬렉션 시도
956
+ if (healthResult.memoryUsage > 90) {
957
+ if (this.healthChecker.triggerGarbageCollection()) {
861
958
  this.log('Garbage collection triggered');
862
959
  }
863
960
  }
864
- // 실행 중인 작업 수 확인
865
- if (this.runningJobs.size > this.config.maxConcurrentJobs) {
866
- this.log(`Too many running jobs: ${this.runningJobs.size}/${this.config.maxConcurrentJobs}`, { level: 'warn' });
867
- }
868
- // 큐 크기 확인
869
- if (this.jobQueue.length > 100) {
870
- this.log(`Large job queue: ${this.jobQueue.length} items`, { level: 'warn' });
871
- }
872
961
  this.log('Scheduler health check completed', {
873
- memoryUsage: memUsagePercent,
874
- runningJobs: this.runningJobs.size,
875
- queueSize: this.jobQueue.length,
876
- uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
962
+ memoryUsage: healthResult.memoryUsage,
963
+ runningJobs: healthResult.runningJobs,
964
+ queueSize: healthResult.queueSize,
965
+ uptime: healthResult.uptime,
966
+ warnings: healthResult.warnings.length,
967
+ errors: healthResult.errors.length
877
968
  });
878
969
  }
879
970
  catch (error) {
@@ -887,22 +978,25 @@ export class BatchScheduler {
887
978
  const memUsage = process.memoryUsage();
888
979
  const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
889
980
  const totalExecutions = Array.from(this.totalExecutions.values()).reduce((sum, count) => sum + count, 0);
890
- const totalErrors = Array.from(this.errorCount.values()).reduce((sum, count) => sum + count, 0);
981
+ // RetryManager에서 에러 카운트 계산
982
+ const totalErrors = Array.from(this.intervals.keys()).reduce((sum, name) => {
983
+ return sum + this.retryManager.getErrorCount(name);
984
+ }, 0);
891
985
  const errorRate = totalExecutions > 0 ? totalErrors / totalExecutions : 0;
892
986
  const jobs = Array.from(this.intervals.keys()).map(name => ({
893
987
  name,
894
988
  lastExecution: this.lastExecution.get(name) || null,
895
989
  totalExecutions: this.totalExecutions.get(name) || 0,
896
- errorCount: this.errorCount.get(name) || 0,
897
- errorRate: (this.totalExecutions.get(name) || 0) > 0 ? (this.errorCount.get(name) || 0) / (this.totalExecutions.get(name) || 1) : 0,
898
- isRunning: this.runningJobs.has(name)
990
+ errorCount: this.retryManager.getErrorCount(name),
991
+ errorRate: (this.totalExecutions.get(name) || 0) > 0 ? this.retryManager.getErrorCount(name) / (this.totalExecutions.get(name) || 1) : 0,
992
+ isRunning: this.jobQueue.isRunning(name)
899
993
  }));
900
994
  return {
901
995
  status: this.getStatus(),
902
996
  health: {
903
997
  memoryUsage: memUsagePercent,
904
- runningJobs: this.runningJobs.size,
905
- queueSize: this.jobQueue.length,
998
+ runningJobs: this.jobQueue.runningCount,
999
+ queueSize: this.jobQueue.size,
906
1000
  errorRate,
907
1001
  uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
908
1002
  },