memento-mcp-server 1.12.0 → 1.13.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 (119) hide show
  1. package/dist/algorithms/forgetting-algorithm.d.ts +21 -13
  2. package/dist/algorithms/forgetting-algorithm.d.ts.map +1 -1
  3. package/dist/algorithms/forgetting-algorithm.js +32 -24
  4. package/dist/algorithms/forgetting-algorithm.js.map +1 -1
  5. package/dist/algorithms/hybrid-search-engine.d.ts +11 -9
  6. package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
  7. package/dist/algorithms/hybrid-search-engine.js +77 -75
  8. package/dist/algorithms/hybrid-search-engine.js.map +1 -1
  9. package/dist/algorithms/search-engine.d.ts +33 -11
  10. package/dist/algorithms/search-engine.d.ts.map +1 -1
  11. package/dist/algorithms/search-engine.js +157 -62
  12. package/dist/algorithms/search-engine.js.map +1 -1
  13. package/dist/algorithms/search-ranking.d.ts +57 -50
  14. package/dist/algorithms/search-ranking.d.ts.map +1 -1
  15. package/dist/algorithms/search-ranking.js +91 -84
  16. package/dist/algorithms/search-ranking.js.map +1 -1
  17. package/dist/algorithms/spaced-repetition.d.ts +18 -13
  18. package/dist/algorithms/spaced-repetition.d.ts.map +1 -1
  19. package/dist/algorithms/spaced-repetition.js +28 -23
  20. package/dist/algorithms/spaced-repetition.js.map +1 -1
  21. package/dist/algorithms/vector-search-engine-migration.d.ts +8 -6
  22. package/dist/algorithms/vector-search-engine-migration.d.ts.map +1 -1
  23. package/dist/algorithms/vector-search-engine-migration.js +13 -11
  24. package/dist/algorithms/vector-search-engine-migration.js.map +1 -1
  25. package/dist/algorithms/vector-search-engine-refactored.d.ts +7 -7
  26. package/dist/algorithms/vector-search-engine-refactored.d.ts.map +1 -1
  27. package/dist/algorithms/vector-search-engine-refactored.js +7 -7
  28. package/dist/algorithms/vector-search-engine-refactored.js.map +1 -1
  29. package/dist/algorithms/vector-search-engine.d.ts +25 -20
  30. package/dist/algorithms/vector-search-engine.d.ts.map +1 -1
  31. package/dist/algorithms/vector-search-engine.js +47 -43
  32. package/dist/algorithms/vector-search-engine.js.map +1 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/index.js +4 -1
  35. package/dist/config/index.js.map +1 -1
  36. package/dist/database/init.d.ts.map +1 -1
  37. package/dist/database/init.js +20 -0
  38. package/dist/database/init.js.map +1 -1
  39. package/dist/database/migration/migrations/006-fts5-reflection-notes-migration-status.sql +30 -0
  40. package/dist/database/migration/migrations/006-fts5-reflection-notes.d.ts +113 -0
  41. package/dist/database/migration/migrations/006-fts5-reflection-notes.d.ts.map +1 -0
  42. package/dist/database/migration/migrations/006-fts5-reflection-notes.js +435 -0
  43. package/dist/database/migration/migrations/006-fts5-reflection-notes.js.map +1 -0
  44. package/dist/database/migration/migrations/006-fts5-reflection-notes.sql +26 -0
  45. package/dist/database/schema.sql +11 -9
  46. package/dist/server/bootstrap.d.ts +4 -0
  47. package/dist/server/bootstrap.d.ts.map +1 -1
  48. package/dist/server/bootstrap.js +11 -1
  49. package/dist/server/bootstrap.js.map +1 -1
  50. package/dist/server/context.d.ts.map +1 -1
  51. package/dist/server/context.js +3 -1
  52. package/dist/server/context.js.map +1 -1
  53. package/dist/server/http-server.d.ts.map +1 -1
  54. package/dist/server/http-server.js +2 -1
  55. package/dist/server/http-server.js.map +1 -1
  56. package/dist/server/index.js +3 -1
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/services/async-optimizer.d.ts +2 -1
  59. package/dist/services/async-optimizer.d.ts.map +1 -1
  60. package/dist/services/async-optimizer.js +28 -1
  61. package/dist/services/async-optimizer.js.map +1 -1
  62. package/dist/services/batch-scheduler.d.ts +5 -1
  63. package/dist/services/batch-scheduler.d.ts.map +1 -1
  64. package/dist/services/batch-scheduler.js +13 -1
  65. package/dist/services/batch-scheduler.js.map +1 -1
  66. package/dist/services/cache-service.js +1 -1
  67. package/dist/services/cache-service.js.map +1 -1
  68. package/dist/services/failure-detector.d.ts +120 -0
  69. package/dist/services/failure-detector.d.ts.map +1 -0
  70. package/dist/services/failure-detector.js +370 -0
  71. package/dist/services/failure-detector.js.map +1 -0
  72. package/dist/services/llm-based-relation-extractor.js +1 -1
  73. package/dist/services/llm-based-relation-extractor.js.map +1 -1
  74. package/dist/services/reflexion-worker.d.ts +170 -0
  75. package/dist/services/reflexion-worker.d.ts.map +1 -0
  76. package/dist/services/reflexion-worker.js +636 -0
  77. package/dist/services/reflexion-worker.js.map +1 -0
  78. package/dist/services/relation-graph.d.ts +2 -2
  79. package/dist/services/relation-graph.js +3 -3
  80. package/dist/services/relation-graph.js.map +1 -1
  81. package/dist/tools/base-tool.d.ts +5 -0
  82. package/dist/tools/base-tool.d.ts.map +1 -1
  83. package/dist/tools/base-tool.js +39 -0
  84. package/dist/tools/base-tool.js.map +1 -1
  85. package/dist/tools/recall-tool.d.ts.map +1 -1
  86. package/dist/tools/recall-tool.js +36 -2
  87. package/dist/tools/recall-tool.js.map +1 -1
  88. package/dist/tools/remember-tool.d.ts +24 -0
  89. package/dist/tools/remember-tool.d.ts.map +1 -1
  90. package/dist/tools/remember-tool.js +445 -273
  91. package/dist/tools/remember-tool.js.map +1 -1
  92. package/dist/tools/types.d.ts +5 -1
  93. package/dist/tools/types.d.ts.map +1 -1
  94. package/dist/tools/types.js +1 -1
  95. package/dist/tools/types.js.map +1 -1
  96. package/dist/types/index.d.ts +2 -0
  97. package/dist/types/index.d.ts.map +1 -1
  98. package/dist/types/index.js.map +1 -1
  99. package/dist/utils/database.d.ts.map +1 -1
  100. package/dist/utils/database.js +34 -10
  101. package/dist/utils/database.js.map +1 -1
  102. package/dist/utils/fts5-migration-status.d.ts +72 -0
  103. package/dist/utils/fts5-migration-status.d.ts.map +1 -0
  104. package/dist/utils/fts5-migration-status.js +304 -0
  105. package/dist/utils/fts5-migration-status.js.map +1 -0
  106. package/dist/utils/reflection-notes-merge.d.ts +58 -0
  107. package/dist/utils/reflection-notes-merge.d.ts.map +1 -0
  108. package/dist/utils/reflection-notes-merge.js +227 -0
  109. package/dist/utils/reflection-notes-merge.js.map +1 -0
  110. package/dist/utils/reflection-notes-normalize.d.ts +43 -0
  111. package/dist/utils/reflection-notes-normalize.d.ts.map +1 -0
  112. package/dist/utils/reflection-notes-normalize.js +164 -0
  113. package/dist/utils/reflection-notes-normalize.js.map +1 -0
  114. package/dist/utils/reflection-notes-schema.d.ts +84 -0
  115. package/dist/utils/reflection-notes-schema.d.ts.map +1 -0
  116. package/dist/utils/reflection-notes-schema.js +215 -0
  117. package/dist/utils/reflection-notes-schema.js.map +1 -0
  118. package/package.json +3 -1
  119. package/src/database/schema.sql +11 -9
@@ -17,6 +17,8 @@ import { KnowledgeVaultService } from '../services/knowledge-vault-service.js';
17
17
  import { validateTypeParam } from '../utils/type-param-validator.js';
18
18
  import { mementoConfig } from '../config/index.js';
19
19
  import { RelationExtractor } from '../services/relation-extractor.js';
20
+ import { validateReflectionNotes, formatValidationErrors } from '../utils/reflection-notes-schema.js';
21
+ import { mergeReflectionNotes, serializeReflectionNotes } from '../utils/reflection-notes-merge.js';
20
22
  const RememberSchema = z.object({
21
23
  content: CommonSchemas.Content,
22
24
  type: CommonSchemas.MemoryType.optional(), // optional - validateTypeParam에서 기본값 처리
@@ -123,126 +125,286 @@ export class RememberTool extends BaseTool {
123
125
  required: [] // 조건부 필수는 Zod 스키마에서 검증
124
126
  });
125
127
  }
126
- async handle(params, context) {
127
- const { content, type: rawType, key, value, always_load, immutable, task_goal, steps, reflection_notes, tags, importance, source, privacy_scope } = RememberSchema.parse(params);
128
- // type 파라미터 롤아웃 모드 검증
129
- const typeParamMode = mementoConfig.typeParamMode;
130
- const typeValidation = validateTypeParam(rawType, typeParamMode, 'remember');
131
- // 에러 모드인 경우 에러 발생
132
- if (!typeValidation.isValid) {
133
- throw new Error(typeValidation.message || "type 파라미터는 필수입니다.");
128
+ /**
129
+ * reflection_notes 파라미터의 JSON 형식 스키마 검증
130
+ * 단일 객체 또는 배열 형식 모두 허용
131
+ *
132
+ * @param reflectionNotes - 검증할 reflection_notes 문자열
133
+ * @throws Error - JSON 형식이 유효하지 않거나 스키마 검증 실패 시
134
+ */
135
+ validateReflectionNotesJson(reflectionNotes) {
136
+ const validationResult = validateReflectionNotes(reflectionNotes);
137
+ if (!validationResult.isValid) {
138
+ const errorMessage = formatValidationErrors(validationResult);
139
+ throw new Error(`reflection_notes 스키마 검증 실패:\n${errorMessage}`);
134
140
  }
135
- // 경고/Deprecation 메시지 출력
136
- if (typeValidation.message) {
137
- if (typeParamMode === 'warn') {
138
- this.logWarning(typeValidation.message);
139
- }
140
- else if (typeParamMode === 'deprecate') {
141
- this.logWarning(typeValidation.message);
141
+ }
142
+ /**
143
+ * 같은 task_goal을 가진 기존 procedural memory 레코드의 reflection_notes 조회
144
+ *
145
+ * @param db - 데이터베이스 인스턴스
146
+ * @param taskGoal - 작업 목표
147
+ * @returns 기존 reflection_notes 조회 결과
148
+ */
149
+ async getExistingReflectionNotes(db, taskGoal) {
150
+ // task_goal이 제공되지 않은 경우 조회 불가
151
+ if (!taskGoal) {
152
+ return {
153
+ exists: false,
154
+ type: 'null',
155
+ value: null,
156
+ rawValue: null
157
+ };
158
+ }
159
+ try {
160
+ // 같은 task_goal을 가진 가장 최근 procedural memory 레코드 조회
161
+ const existingRecord = DatabaseUtils.get(db, `SELECT reflection_notes FROM memory_item
162
+ WHERE type = 'procedural' AND task_goal = ?
163
+ ORDER BY created_at DESC LIMIT 1`, [taskGoal]);
164
+ if (!existingRecord || !existingRecord.reflection_notes) {
165
+ return {
166
+ exists: false,
167
+ type: 'null',
168
+ value: null,
169
+ rawValue: null
170
+ };
142
171
  }
172
+ // reflection_notes 파싱 및 타입 확인
173
+ return this.parseReflectionNotes(existingRecord.reflection_notes);
174
+ }
175
+ catch (error) {
176
+ // 조회 실패 시 빈 결과 반환
177
+ this.logWarning(`기존 reflection_notes 조회 실패: ${error instanceof Error ? error.message : String(error)}`);
178
+ return {
179
+ exists: false,
180
+ type: 'null',
181
+ value: null,
182
+ rawValue: null
183
+ };
143
184
  }
144
- // type 파라미터 결정 (제공된 값 또는 기본값)
145
- const type = (rawType || typeValidation.defaultType || 'episodic');
146
- // 데이터베이스 연결 확인
147
- this.validateDatabase(context);
148
- // origin_source 생성 (JSON 형식)
149
- const origin_source = JSON.stringify({
150
- tool: 'remember',
151
- caller: 'user',
152
- timestamp: new Date().toISOString(),
153
- context: {
154
- type,
155
- has_content: !!content,
156
- has_key: !!key,
157
- has_value: !!value,
158
- type_param_mode: typeParamMode,
159
- type_was_defaulted: !rawType
185
+ }
186
+ /**
187
+ * reflection_notes 문자열을 파싱하고 타입 확인
188
+ * NULL, 단일 객체, 배열 케이스 처리
189
+ *
190
+ * @param reflectionNotes - 파싱할 reflection_notes 문자열
191
+ * @returns 파싱 결과
192
+ */
193
+ parseReflectionNotes(reflectionNotes) {
194
+ if (!reflectionNotes || reflectionNotes.trim() === '') {
195
+ return {
196
+ exists: true,
197
+ type: 'null',
198
+ value: null,
199
+ rawValue: null
200
+ };
201
+ }
202
+ try {
203
+ const parsed = JSON.parse(reflectionNotes);
204
+ if (Array.isArray(parsed)) {
205
+ return {
206
+ exists: true,
207
+ type: 'array',
208
+ value: parsed,
209
+ rawValue: reflectionNotes
210
+ };
160
211
  }
161
- });
162
- // type에 따른 분기 처리
163
- if (type === 'core') {
164
- // Core Memory 저장
165
- if (!key || !value) {
166
- throw new Error("type='core'일 때는 key와 value가 필수입니다");
212
+ if (typeof parsed === 'object' && parsed !== null) {
213
+ return {
214
+ exists: true,
215
+ type: 'object',
216
+ value: parsed,
217
+ rawValue: reflectionNotes
218
+ };
167
219
  }
168
- const coreMemoryRepository = new CoreMemoryRepository(context.db);
169
- const { getCoreMemoryCache } = await import('../services/core-memory-cache-service.js');
170
- const coreMemoryCache = getCoreMemoryCache();
171
- const coreMemoryService = new CoreMemoryService(coreMemoryRepository, coreMemoryCache);
172
- const agent_id = 'default'; // TODO: 향후 context에서 가져오기
173
- const record = await coreMemoryService.create({
174
- agent_id,
175
- key,
176
- value,
177
- always_load: always_load || false,
178
- origin_source
179
- });
180
- return this.createSuccessResult({
181
- memory_id: record.core_id,
182
- type: 'core',
183
- key: record.key,
184
- value: record.value,
185
- always_load: record.always_load,
186
- message: `Core Memory가 저장되었습니다: ${record.core_id}`
187
- });
220
+ // 객체나 배열이 아닌 경우
221
+ return {
222
+ exists: true,
223
+ type: 'null',
224
+ value: null,
225
+ rawValue: reflectionNotes
226
+ };
227
+ }
228
+ catch (error) {
229
+ // 파싱 실패 시 원본 문자열 반환
230
+ this.logWarning(`reflection_notes 파싱 실패: ${error instanceof Error ? error.message : String(error)}`);
231
+ return {
232
+ exists: true,
233
+ type: 'null',
234
+ value: null,
235
+ rawValue: reflectionNotes
236
+ };
188
237
  }
189
- else if (type === 'vault') {
190
- // Knowledge Vault 저장
191
- if (!key || !value) {
192
- throw new Error("type='vault'일 때는 key와 value가 필수입니다");
238
+ }
239
+ async handle(params, context) {
240
+ const startTime = Date.now();
241
+ try {
242
+ const { content, type: rawType, key, value, always_load, immutable, task_goal, steps, reflection_notes, tags, importance, source, privacy_scope } = RememberSchema.parse(params);
243
+ // type 파라미터 롤아웃 모드 검증
244
+ const typeParamMode = mementoConfig.typeParamMode;
245
+ const typeValidation = validateTypeParam(rawType, typeParamMode, 'remember');
246
+ // 에러 모드인 경우 에러 발생
247
+ if (!typeValidation.isValid) {
248
+ throw new Error(typeValidation.message || "type 파라미터는 필수입니다.");
193
249
  }
194
- const knowledgeVaultRepository = new KnowledgeVaultRepository(context.db);
195
- const knowledgeVaultService = new KnowledgeVaultService(knowledgeVaultRepository);
196
- const agent_id = 'default'; // TODO: 향후 context에서 가져오기
197
- const record = await knowledgeVaultService.create({
198
- agent_id,
199
- key,
200
- value,
201
- immutable: immutable !== false, // 기본값 true
202
- origin_source
203
- });
204
- return this.createSuccessResult({
205
- memory_id: record.vault_id,
206
- type: 'vault',
207
- key: record.key,
208
- value: record.value,
209
- immutable: record.immutable,
210
- message: `Knowledge Vault가 저장되었습니다: ${record.vault_id}`
250
+ // 경고/Deprecation 메시지 출력
251
+ if (typeValidation.message) {
252
+ if (typeParamMode === 'warn') {
253
+ this.logWarning(typeValidation.message);
254
+ }
255
+ else if (typeParamMode === 'deprecate') {
256
+ this.logWarning(typeValidation.message);
257
+ }
258
+ }
259
+ // type 파라미터 결정 (제공된 값 또는 기본값)
260
+ const type = (rawType || typeValidation.defaultType || 'episodic');
261
+ // reflection_notes JSON 검증 (type='procedural'이고 reflection_notes가 제공된 경우에만)
262
+ if (type === 'procedural' && reflection_notes !== undefined && reflection_notes !== null) {
263
+ this.validateReflectionNotesJson(reflection_notes);
264
+ }
265
+ // 데이터베이스 연결 확인
266
+ this.validateDatabase(context);
267
+ // origin_source 생성 (JSON 형식)
268
+ const origin_source = JSON.stringify({
269
+ tool: 'remember',
270
+ caller: 'user',
271
+ timestamp: new Date().toISOString(),
272
+ context: {
273
+ type,
274
+ has_content: !!content,
275
+ has_key: !!key,
276
+ has_value: !!value,
277
+ type_param_mode: typeParamMode,
278
+ type_was_defaulted: !rawType
279
+ }
211
280
  });
212
- }
213
- else {
214
- // 기존 memory_item 저장 (episodic, semantic, procedural, working)
215
- if (!content) {
216
- throw new Error("type'core' 또는 'vault'가 아닐 때는 content가 필수입니다");
281
+ // type에 따른 분기 처리
282
+ if (type === 'core') {
283
+ // Core Memory 저장
284
+ if (!key || !value) {
285
+ throw new Error("type='core' 때는 key와 value가 필수입니다");
286
+ }
287
+ const coreMemoryRepository = new CoreMemoryRepository(context.db);
288
+ const { getCoreMemoryCache } = await import('../services/core-memory-cache-service.js');
289
+ const coreMemoryCache = getCoreMemoryCache();
290
+ const coreMemoryService = new CoreMemoryService(coreMemoryRepository, coreMemoryCache);
291
+ const agent_id = 'default'; // TODO: 향후 context에서 가져오기
292
+ const record = await coreMemoryService.create({
293
+ agent_id,
294
+ key,
295
+ value,
296
+ always_load: always_load || false,
297
+ origin_source
298
+ });
299
+ return this.createSuccessResult({
300
+ memory_id: record.core_id,
301
+ type: 'core',
302
+ key: record.key,
303
+ value: record.value,
304
+ always_load: record.always_load,
305
+ message: `Core Memory가 저장되었습니다: ${record.core_id}`
306
+ });
217
307
  }
218
- // 타입 가드로 검증
219
- if (!isMemoryItemType(type)) {
220
- throw new Error(`Invalid memory type: ${type}`);
308
+ else if (type === 'vault') {
309
+ // Knowledge Vault 저장
310
+ if (!key || !value) {
311
+ throw new Error("type='vault'일 때는 key와 value가 필수입니다");
312
+ }
313
+ const knowledgeVaultRepository = new KnowledgeVaultRepository(context.db);
314
+ const knowledgeVaultService = new KnowledgeVaultService(knowledgeVaultRepository);
315
+ const agent_id = 'default'; // TODO: 향후 context에서 가져오기
316
+ const record = await knowledgeVaultService.create({
317
+ agent_id,
318
+ key,
319
+ value,
320
+ immutable: immutable !== false, // 기본값 true
321
+ origin_source
322
+ });
323
+ return this.createSuccessResult({
324
+ memory_id: record.vault_id,
325
+ type: 'vault',
326
+ key: record.key,
327
+ value: record.value,
328
+ immutable: record.immutable,
329
+ message: `Knowledge Vault가 저장되었습니다: ${record.vault_id}`
330
+ });
221
331
  }
222
- // UUID 생성 (임시로 간단한 ID 사용)
223
- const id = `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
224
- try {
225
- // 메모리 저장 (트랜잭션 사용)
226
- await DatabaseUtils.runTransaction(context.db, async () => {
227
- // Consolidation Score System 초기화 값 설정
228
- const createdAt = new Date().toISOString();
229
- const recallCount = mementoConfig.consolidationScoreEnabled ? 1 : 0; // 기능 활성화 시 1, 비활성화 시 0
230
- const gValue = mementoConfig.consolidationScoreEnabled ? 1.0 : null; // 기능 활성화 시 1.0, 비활성화 시 NULL
231
- const lastAccessedAt = mementoConfig.consolidationScoreEnabled ? createdAt : null; // 기능 활성화 시 created_at과 동일, 비활성화 시 NULL
232
- // consolidation_score 계산 (기능 활성화 시)
233
- let consolidationScore = null;
234
- if (mementoConfig.consolidationScoreEnabled && context.services.consolidationScoreService) {
235
- const scoreResult = context.services.consolidationScoreService.calculateScore({
236
- recallCount: 1,
237
- lastAccessedAt: new Date(createdAt),
238
- createdAt: new Date(createdAt),
239
- gValue: 1.0,
240
- type: type,
241
- pinned: false
242
- });
243
- consolidationScore = scoreResult.score;
332
+ else {
333
+ // 기존 memory_item 저장 (episodic, semantic, procedural, working)
334
+ if (!content) {
335
+ throw new Error("type이 'core' 또는 'vault'가 아닐 때는 content가 필수입니다");
336
+ }
337
+ // 타입 가드로 검증
338
+ if (!isMemoryItemType(type)) {
339
+ throw new Error(`Invalid memory type: ${type}`);
340
+ }
341
+ // reflection_notes 처리
342
+ // type='procedural'이고 reflection_notes가 제공된 경우 기존 reflection_notes 조회 및 병합
343
+ // non-procedural 타입에서는 reflection_notes를 무시 (PRD: "Procedural Memory에서만 사용 가능")
344
+ let finalReflectionNotes = null;
345
+ if (type === 'procedural' && reflection_notes !== undefined && reflection_notes !== null) {
346
+ // procedural 타입: 기존 reflection_notes 조회 및 병합
347
+ finalReflectionNotes = reflection_notes;
348
+ const existingReflectionNotes = await this.getExistingReflectionNotes(context.db, task_goal);
349
+ // 기존 reflection_notes가 있는 경우 병합
350
+ if (existingReflectionNotes.exists) {
351
+ try {
352
+ // 병합 유틸리티 함수 사용
353
+ const existing = existingReflectionNotes.type === 'null' ? { type: 'null', value: null } :
354
+ existingReflectionNotes.type === 'object' ? { type: 'object', value: existingReflectionNotes.value } :
355
+ { type: 'array', value: existingReflectionNotes.value };
356
+ const mergeResult = mergeReflectionNotes(existing, reflection_notes);
357
+ // 병합 결과를 JSON 문자열로 변환
358
+ finalReflectionNotes = serializeReflectionNotes(mergeResult.merged);
359
+ // 경고 메시지 처리
360
+ if (mergeResult.warnings.length > 0) {
361
+ mergeResult.warnings.forEach(warning => {
362
+ this.logWarning(`reflection_notes 병합 경고: ${warning}`);
363
+ });
364
+ }
365
+ if (mergeResult.removedCount > 0) {
366
+ this.logWarning(`reflection_notes 크기 제한으로 인해 ${mergeResult.removedCount}개 항목이 제거되었습니다`);
367
+ }
368
+ }
369
+ catch (error) {
370
+ // 병합 실패 시 에러 처리
371
+ const errorMessage = error instanceof Error ? error.message : String(error);
372
+ // 단일 객체 크기 초과 같은 경우는 에러를 던짐 (검증 단계에서 이미 처리되어야 하지만 안전장치)
373
+ if (errorMessage.includes('최대') && errorMessage.includes('바이트')) {
374
+ throw new Error(`reflection_notes 크기 제한 초과: ${errorMessage}. ` +
375
+ `단일 객체는 최대 10KB, 전체 필드는 최대 1MB를 초과할 수 없습니다.`);
376
+ }
377
+ // 기타 병합 실패 시 원본 reflection_notes 사용 (경고 로그)
378
+ this.logWarning(`reflection_notes 병합 실패, 원본 값 사용: ${errorMessage}. ` +
379
+ `기존 reflection_notes는 유지되고 새 reflection_notes만 저장됩니다.`);
380
+ }
244
381
  }
245
- await DatabaseUtils.run(context.db, `
382
+ }
383
+ // non-procedural 타입에서는 reflection_notes를 무시 (null로 설정)
384
+ // UUID 생성 (임시로 간단한 ID 사용)
385
+ const id = `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
386
+ try {
387
+ // 메모리 저장 (트랜잭션 사용)
388
+ await DatabaseUtils.runTransaction(context.db, async () => {
389
+ // Consolidation Score System 초기화 값 설정
390
+ const createdAt = new Date().toISOString();
391
+ const recallCount = mementoConfig.consolidationScoreEnabled ? 1 : 0; // 기능 활성화 시 1, 비활성화 시 0
392
+ const gValue = mementoConfig.consolidationScoreEnabled ? 1.0 : null; // 기능 활성화 시 1.0, 비활성화 시 NULL
393
+ const lastAccessedAt = mementoConfig.consolidationScoreEnabled ? createdAt : null; // 기능 활성화 시 created_at과 동일, 비활성화 시 NULL
394
+ // consolidation_score 계산 (기능 활성화 시)
395
+ let consolidationScore = null;
396
+ if (mementoConfig.consolidationScoreEnabled && context.services.consolidationScoreService) {
397
+ const scoreResult = context.services.consolidationScoreService.calculateScore({
398
+ recallCount: 1,
399
+ lastAccessedAt: new Date(createdAt),
400
+ createdAt: new Date(createdAt),
401
+ gValue: 1.0,
402
+ type: type,
403
+ pinned: false
404
+ });
405
+ consolidationScore = scoreResult.score;
406
+ }
407
+ await DatabaseUtils.run(context.db, `
246
408
  INSERT INTO memory_item (
247
409
  id, type, content, importance, privacy_scope, tags, source, origin_source,
248
410
  task_goal, steps, reflection_notes, created_at,
@@ -250,72 +412,116 @@ export class RememberTool extends BaseTool {
250
412
  )
251
413
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
252
414
  `, [
253
- id,
254
- type,
255
- content,
256
- importance,
257
- privacy_scope,
258
- tags ? JSON.stringify(tags) : null,
259
- source || null,
260
- origin_source,
261
- task_goal || null,
262
- steps || null,
263
- reflection_notes || null,
264
- createdAt,
265
- recallCount,
266
- lastAccessedAt,
267
- gValue,
268
- consolidationScore
269
- ]);
270
- });
271
- // 메모리 저장 완료 후 임베딩 생성, 인접 기억 갱신, 관계 추출 (비동기, 실패해도 메모리 저장은 성공)
272
- // 데이터베이스 참조를 미리 저장하여 비동기 콜백에서 안전하게 사용
273
- const dbRef = context.db;
274
- const embeddingServiceRef = context.services.embeddingService;
275
- const savedMemoryId = id; // 클로저에서 사용할 수 있도록 저장
276
- const savedMemoryType = type; // 클로저에서 사용할 수 있도록 저장
277
- if (dbRef) {
278
- // 비동기 작업을 별도로 실행 (fire-and-forget 패턴)
279
- // 메모리 저장 응답은 즉시 반환하고, 임베딩/인접 기억 갱신/관계 추출은 백그라운드에서 처리
280
- (async () => {
281
- try {
282
- // 트랜잭션이 완전히 커밋되도록 짧은 지연
283
- await new Promise(resolve => setTimeout(resolve, 100));
284
- // 데이터베이스 연결이 여전히 유효한지 확인 (간단한 쿼리로 테스트)
285
- // DatabaseUtils.get은 동기 함수이지만, 비동기 컨텍스트에서 안전하게 실행하기 위해 Promise로 감싸서 await
415
+ id,
416
+ type,
417
+ content,
418
+ importance,
419
+ privacy_scope,
420
+ tags ? JSON.stringify(tags) : null,
421
+ source || null,
422
+ origin_source,
423
+ task_goal || null,
424
+ steps || null,
425
+ finalReflectionNotes,
426
+ createdAt,
427
+ recallCount,
428
+ lastAccessedAt,
429
+ gValue,
430
+ consolidationScore
431
+ ]);
432
+ });
433
+ // 메모리 저장 완료 후 임베딩 생성, 인접 기억 갱신, 관계 추출 (비동기, 실패해도 메모리 저장은 성공)
434
+ // 데이터베이스 참조를 미리 저장하여 비동기 콜백에서 안전하게 사용
435
+ const dbRef = context.db;
436
+ const embeddingServiceRef = context.services.embeddingService;
437
+ const savedMemoryId = id; // 클로저에서 사용할 수 있도록 저장
438
+ const savedMemoryType = type; // 클로저에서 사용할 수 있도록 저장
439
+ if (dbRef) {
440
+ // 비동기 작업을 별도로 실행 (fire-and-forget 패턴)
441
+ // 메모리 저장 응답은 즉시 반환하고, 임베딩/인접 기억 갱신/관계 추출은 백그라운드에서 처리
442
+ (async () => {
286
443
  try {
287
- await new Promise((resolve, reject) => {
444
+ // 트랜잭션이 완전히 커밋되도록 짧은 지연
445
+ await new Promise(resolve => setTimeout(resolve, 100));
446
+ // 데이터베이스 연결이 여전히 유효한지 확인 (간단한 쿼리로 테스트)
447
+ // DatabaseUtils.get은 동기 함수이지만, 비동기 컨텍스트에서 안전하게 실행하기 위해 Promise로 감싸서 await
448
+ try {
449
+ await new Promise((resolve, reject) => {
450
+ try {
451
+ DatabaseUtils.get(dbRef, 'SELECT 1');
452
+ resolve();
453
+ }
454
+ catch (error) {
455
+ reject(error);
456
+ }
457
+ });
458
+ }
459
+ catch (dbError) {
460
+ this.logWarning('데이터베이스 연결이 유효하지 않아 백그라운드 작업을 건너뜁니다', {
461
+ memory_id: savedMemoryId,
462
+ error: dbError instanceof Error ? dbError.message : String(dbError)
463
+ });
464
+ return;
465
+ }
466
+ // 임베딩 생성 (embeddingService가 사용 가능한 경우에만)
467
+ let embeddingResult = null;
468
+ if (embeddingServiceRef?.isAvailable()) {
288
469
  try {
289
- DatabaseUtils.get(dbRef, 'SELECT 1');
290
- resolve();
470
+ embeddingResult = await embeddingServiceRef.createAndStoreEmbedding(dbRef, savedMemoryId, content, savedMemoryType);
291
471
  }
292
472
  catch (error) {
293
- reject(error);
473
+ // 임베딩 생성 실패해도 메모리 저장은 성공했으므로 경고만 출력
474
+ this.logWarning(`임베딩 생성 실패 (${savedMemoryId})`, {
475
+ error: error instanceof Error ? error.message : String(error)
476
+ });
294
477
  }
295
- });
296
- }
297
- catch (dbError) {
298
- this.logWarning('데이터베이스 연결이 유효하지 않아 백그라운드 작업을 건너뜁니다', {
299
- memory_id: savedMemoryId,
300
- error: dbError instanceof Error ? dbError.message : String(dbError)
301
- });
302
- return;
303
- }
304
- // 임베딩 생성 (embeddingService가 사용 가능한 경우에만)
305
- let embeddingResult = null;
306
- if (embeddingServiceRef?.isAvailable()) {
307
- try {
308
- embeddingResult = await embeddingServiceRef.createAndStoreEmbedding(dbRef, savedMemoryId, content, savedMemoryType);
309
478
  }
310
- catch (error) {
311
- // 임베딩 생성 실패해도 메모리 저장은 성공했으므로 경고만 출력
312
- this.logWarning(`임베딩 생성 실패 (${savedMemoryId})`, {
313
- error: error instanceof Error ? error.message : String(error)
314
- });
479
+ // PRD 3.1-3.3: 인접 기억 갱신 (임베딩이 생성된 경우에만)
480
+ if (embeddingResult && embeddingServiceRef) {
481
+ try {
482
+ // 데이터베이스 연결 재확인
483
+ let dbValid = false;
484
+ try {
485
+ await new Promise((resolve, reject) => {
486
+ try {
487
+ DatabaseUtils.get(dbRef, 'SELECT 1');
488
+ resolve();
489
+ }
490
+ catch (error) {
491
+ reject(error);
492
+ }
493
+ });
494
+ dbValid = true;
495
+ }
496
+ catch (dbError) {
497
+ this.logWarning('데이터베이스 연결이 유효하지 않아 인접 기억 갱신을 건너뜁니다', {
498
+ memory_id: savedMemoryId,
499
+ error: dbError instanceof Error ? dbError.message : String(dbError)
500
+ });
501
+ }
502
+ if (dbValid) {
503
+ const vectorSearchEngine = getVectorSearchEngine();
504
+ const neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingServiceRef);
505
+ neighborService.setDatabase(dbRef);
506
+ // 인접 기억 갱신 (기본 유사도 임계값: 0.8)
507
+ const neighborIds = await neighborService.updateNeighborsForNewMemory(savedMemoryId, 0.8);
508
+ if (neighborIds.length > 0) {
509
+ this.logInfo('인접 기억 갱신 완료', {
510
+ memory_id: savedMemoryId,
511
+ neighbor_count: neighborIds.length
512
+ });
513
+ }
514
+ }
515
+ }
516
+ catch (error) {
517
+ // 인접 기억 갱신 실패해도 메모리 저장은 성공했으므로 경고만 출력
518
+ this.logWarning(`인접 기억 갱신 실패 (${savedMemoryId})`, {
519
+ error: error instanceof Error ? error.message : String(error)
520
+ });
521
+ }
315
522
  }
316
- }
317
- // PRD 3.1-3.3: 인접 기억 갱신 (임베딩이 생성된 경우에만)
318
- if (embeddingResult && embeddingServiceRef) {
523
+ // PRD 2.12: 관계 추출 트리거 (비동기 배치 처리)
524
+ // 임베딩 생성 여부와 관계없이 관계 추출 수행 (규칙 기반 추출은 임베딩 불필요)
319
525
  try {
320
526
  // 데이터베이스 연결 재확인
321
527
  let dbValid = false;
@@ -332,138 +538,104 @@ export class RememberTool extends BaseTool {
332
538
  dbValid = true;
333
539
  }
334
540
  catch (dbError) {
335
- this.logWarning('데이터베이스 연결이 유효하지 않아 인접 기억 갱신을 건너뜁니다', {
541
+ this.logWarning('데이터베이스 연결이 유효하지 않아 관계 추출을 건너뜁니다', {
336
542
  memory_id: savedMemoryId,
337
543
  error: dbError instanceof Error ? dbError.message : String(dbError)
338
544
  });
339
545
  }
340
546
  if (dbValid) {
341
- const vectorSearchEngine = getVectorSearchEngine();
342
- const neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingServiceRef);
343
- neighborService.setDatabase(dbRef);
344
- // 인접 기억 갱신 (기본 유사도 임계값: 0.8)
345
- const neighborIds = await neighborService.updateNeighborsForNewMemory(savedMemoryId, 0.8);
346
- if (neighborIds.length > 0) {
347
- this.logInfo('인접 기억 갱신 완료', {
348
- memory_id: savedMemoryId,
349
- neighbor_count: neighborIds.length
350
- });
547
+ // 기존 기억들 조회 (최근 100개로 제한하여 성능 최적화)
548
+ const existingMemories = await this.getExistingMemoriesForRelationExtraction(dbRef, savedMemoryId, 100);
549
+ if (existingMemories.length > 0) {
550
+ // 새로 저장된 기억 정보 조회
551
+ const newMemory = await this.getMemoryById(dbRef, savedMemoryId);
552
+ if (newMemory) {
553
+ // RelationExtractor를 사용하여 관계 추출
554
+ const relationExtractor = new RelationExtractor();
555
+ // 비동기 배치 처리로 관계 추출
556
+ // immediate: true로 설정하여 캐싱 활성화
557
+ const candidates = await relationExtractor.extractRelations(newMemory, existingMemories, {
558
+ method: 'hybrid',
559
+ minConfidence: 0.5,
560
+ candidateLimit: 30, // MiniLM 필터링을 위한 제한
561
+ immediate: true // 캐싱 활성화
562
+ });
563
+ if (candidates.length > 0) {
564
+ this.logInfo('관계 추출 완료', {
565
+ memory_id: savedMemoryId,
566
+ relation_count: candidates.length,
567
+ relations: candidates.map(c => ({
568
+ target_id: c.target_id,
569
+ relation_type: c.relation_type,
570
+ confidence: c.confidence,
571
+ method: c.method
572
+ }))
573
+ });
574
+ // TODO: PRD 3.0에서 RelationGraph가 구현되면 여기서 관계를 저장
575
+ // const relationGraph = new RelationGraph(dbRef);
576
+ // for (const candidate of candidates) {
577
+ // await relationGraph.addRelation(candidate);
578
+ // }
579
+ }
580
+ else {
581
+ this.logInfo('관계 추출 완료 (관계 없음)', {
582
+ memory_id: savedMemoryId
583
+ });
584
+ }
585
+ }
351
586
  }
352
587
  }
353
588
  }
354
589
  catch (error) {
355
- // 인접 기억 갱신 실패해도 메모리 저장은 성공했으므로 경고만 출력
356
- this.logWarning(`인접 기억 갱신 실패 (${savedMemoryId})`, {
590
+ // 관계 추출 실패해도 메모리 저장은 성공했으므로 경고만 출력
591
+ this.logWarning(`관계 추출 실패 (${savedMemoryId})`, {
357
592
  error: error instanceof Error ? error.message : String(error)
358
593
  });
359
594
  }
360
595
  }
361
- // PRD 2.12: 관계 추출 트리거 (비동기 배치 처리)
362
- // 임베딩 생성 여부와 관계없이 관계 추출 수행 (규칙 기반 추출은 임베딩 불필요)
363
- try {
364
- // 데이터베이스 연결 재확인
365
- let dbValid = false;
366
- try {
367
- await new Promise((resolve, reject) => {
368
- try {
369
- DatabaseUtils.get(dbRef, 'SELECT 1');
370
- resolve();
371
- }
372
- catch (error) {
373
- reject(error);
374
- }
375
- });
376
- dbValid = true;
377
- }
378
- catch (dbError) {
379
- this.logWarning('데이터베이스 연결이 유효하지 않아 관계 추출을 건너뜁니다', {
380
- memory_id: savedMemoryId,
381
- error: dbError instanceof Error ? dbError.message : String(dbError)
382
- });
383
- }
384
- if (dbValid) {
385
- // 기존 기억들 조회 (최근 100개로 제한하여 성능 최적화)
386
- const existingMemories = await this.getExistingMemoriesForRelationExtraction(dbRef, savedMemoryId, 100);
387
- if (existingMemories.length > 0) {
388
- // 새로 저장된 기억 정보 조회
389
- const newMemory = await this.getMemoryById(dbRef, savedMemoryId);
390
- if (newMemory) {
391
- // RelationExtractor를 사용하여 관계 추출
392
- const relationExtractor = new RelationExtractor();
393
- // 비동기 배치 처리로 관계 추출
394
- // immediate: true로 설정하여 캐싱 활성화
395
- const candidates = await relationExtractor.extractRelations(newMemory, existingMemories, {
396
- method: 'hybrid',
397
- minConfidence: 0.5,
398
- candidateLimit: 30, // MiniLM 필터링을 위한 제한
399
- immediate: true // 캐싱 활성화
400
- });
401
- if (candidates.length > 0) {
402
- this.logInfo('관계 추출 완료', {
403
- memory_id: savedMemoryId,
404
- relation_count: candidates.length,
405
- relations: candidates.map(c => ({
406
- target_id: c.target_id,
407
- relation_type: c.relation_type,
408
- confidence: c.confidence,
409
- method: c.method
410
- }))
411
- });
412
- // TODO: PRD 3.0에서 RelationGraph가 구현되면 여기서 관계를 저장
413
- // const relationGraph = new RelationGraph(dbRef);
414
- // for (const candidate of candidates) {
415
- // await relationGraph.addRelation(candidate);
416
- // }
417
- }
418
- else {
419
- this.logInfo('관계 추출 완료 (관계 없음)', {
420
- memory_id: savedMemoryId
421
- });
422
- }
423
- }
424
- }
425
- }
426
- }
427
596
  catch (error) {
428
- // 관계 추출 실패해도 메모리 저장은 성공했으므로 경고만 출력
429
- this.logWarning(`관계 추출 실패 (${savedMemoryId})`, {
597
+ // 백그라운드 작업 실패해도 메모리 저장은 성공했으므로 경고만 출력
598
+ this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
430
599
  error: error instanceof Error ? error.message : String(error)
431
600
  });
432
601
  }
433
- }
434
- catch (error) {
435
- // 백그라운드 작업 실패해도 메모리 저장은 성공했으므로 경고만 출력
602
+ })().catch((error) => {
603
+ // 예상치 못한 에러 처리
436
604
  this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
437
605
  error: error instanceof Error ? error.message : String(error)
438
606
  });
439
- }
440
- })().catch((error) => {
441
- // 예상치 못한 에러 처리
442
- this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
443
- error: error instanceof Error ? error.message : String(error)
444
607
  });
608
+ }
609
+ return this.createSuccessResult({
610
+ memory_id: id,
611
+ type: type,
612
+ message: `기억이 저장되었습니다: ${id}`,
613
+ embedding_created: context.services.embeddingService?.isAvailable() || false
445
614
  });
446
615
  }
447
- return this.createSuccessResult({
448
- memory_id: id,
449
- type: type,
450
- message: `기억이 저장되었습니다: ${id}`,
451
- embedding_created: context.services.embeddingService?.isAvailable() || false
452
- });
453
- }
454
- catch (error) {
455
- // 데이터베이스 락 문제인 경우 WAL 체크포인트 시도
456
- if (error.code === 'SQLITE_BUSY') {
457
- try {
458
- await DatabaseUtils.checkpointWAL(context.db);
459
- }
460
- catch (checkpointError) {
461
- // WAL 체크포인트 실패
616
+ catch (error) {
617
+ // 데이터베이스 락 문제인 경우 WAL 체크포인트 시도
618
+ if (error.code === 'SQLITE_BUSY') {
619
+ try {
620
+ await DatabaseUtils.checkpointWAL(context.db);
621
+ }
622
+ catch (checkpointError) {
623
+ // WAL 체크포인트 실패
624
+ }
462
625
  }
626
+ // 실패 감지 훅 호출
627
+ const executionTime = Date.now() - startTime;
628
+ await this.handleFailure(error instanceof Error ? error : new Error(String(error)), params, context, executionTime);
629
+ throw error;
463
630
  }
464
- throw error;
465
631
  }
466
632
  }
633
+ catch (error) {
634
+ // 최상위 에러 처리 (내부 catch에서 처리되지 않은 에러)
635
+ const executionTime = Date.now() - startTime;
636
+ await this.handleFailure(error instanceof Error ? error : new Error(String(error)), params, context, executionTime);
637
+ throw error;
638
+ }
467
639
  }
468
640
  /**
469
641
  * 관계 추출을 위한 기존 기억들 조회