memento-mcp-server 1.10.1 → 1.11.0

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 (70) hide show
  1. package/dist/algorithms/hybrid-search-engine.d.ts +6 -1
  2. package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
  3. package/dist/algorithms/hybrid-search-engine.js +57 -5
  4. package/dist/algorithms/hybrid-search-engine.js.map +1 -1
  5. package/dist/algorithms/vector-search-engine.d.ts +6 -0
  6. package/dist/algorithms/vector-search-engine.d.ts.map +1 -1
  7. package/dist/algorithms/vector-search-engine.js +49 -6
  8. package/dist/algorithms/vector-search-engine.js.map +1 -1
  9. package/dist/database/migration/migrations/004-anchor-table.d.ts +51 -0
  10. package/dist/database/migration/migrations/004-anchor-table.d.ts.map +1 -0
  11. package/dist/database/migration/migrations/004-anchor-table.js +198 -0
  12. package/dist/database/migration/migrations/004-anchor-table.js.map +1 -0
  13. package/dist/server/bootstrap.d.ts +2 -0
  14. package/dist/server/bootstrap.d.ts.map +1 -1
  15. package/dist/server/bootstrap.js +12 -1
  16. package/dist/server/bootstrap.js.map +1 -1
  17. package/dist/server/http-server.d.ts.map +1 -1
  18. package/dist/server/http-server.js +337 -4
  19. package/dist/server/http-server.js.map +1 -1
  20. package/dist/server/index.js +3 -2
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/services/anchor-manager.d.ts +294 -0
  23. package/dist/services/anchor-manager.d.ts.map +1 -0
  24. package/dist/services/anchor-manager.js +1270 -0
  25. package/dist/services/anchor-manager.js.map +1 -0
  26. package/dist/services/memory-embedding-service.d.ts +1 -0
  27. package/dist/services/memory-embedding-service.d.ts.map +1 -1
  28. package/dist/services/memory-embedding-service.js +15 -6
  29. package/dist/services/memory-embedding-service.js.map +1 -1
  30. package/dist/services/memory-neighbor-service.d.ts.map +1 -1
  31. package/dist/services/memory-neighbor-service.js +4 -2
  32. package/dist/services/memory-neighbor-service.js.map +1 -1
  33. package/dist/services/minilm-embedding-service.d.ts +1 -0
  34. package/dist/services/minilm-embedding-service.d.ts.map +1 -1
  35. package/dist/services/minilm-embedding-service.js +45 -4
  36. package/dist/services/minilm-embedding-service.js.map +1 -1
  37. package/dist/services/vector-compatibility-service.js +1 -1
  38. package/dist/services/vector-compatibility-service.js.map +1 -1
  39. package/dist/tools/clear-anchor-tool.d.ts +10 -0
  40. package/dist/tools/clear-anchor-tool.d.ts.map +1 -0
  41. package/dist/tools/clear-anchor-tool.js +64 -0
  42. package/dist/tools/clear-anchor-tool.js.map +1 -0
  43. package/dist/tools/get-anchor-tool.d.ts +10 -0
  44. package/dist/tools/get-anchor-tool.d.ts.map +1 -0
  45. package/dist/tools/get-anchor-tool.js +91 -0
  46. package/dist/tools/get-anchor-tool.js.map +1 -0
  47. package/dist/tools/index.d.ts +6 -1
  48. package/dist/tools/index.d.ts.map +1 -1
  49. package/dist/tools/index.js +15 -2
  50. package/dist/tools/index.js.map +1 -1
  51. package/dist/tools/restore-anchors-tool.d.ts +11 -0
  52. package/dist/tools/restore-anchors-tool.d.ts.map +1 -0
  53. package/dist/tools/restore-anchors-tool.js +117 -0
  54. package/dist/tools/restore-anchors-tool.js.map +1 -0
  55. package/dist/tools/search-local-tool.d.ts +10 -0
  56. package/dist/tools/search-local-tool.d.ts.map +1 -0
  57. package/dist/tools/search-local-tool.js +90 -0
  58. package/dist/tools/search-local-tool.js.map +1 -0
  59. package/dist/tools/set-anchor-tool.d.ts +10 -0
  60. package/dist/tools/set-anchor-tool.d.ts.map +1 -0
  61. package/dist/tools/set-anchor-tool.js +70 -0
  62. package/dist/tools/set-anchor-tool.js.map +1 -0
  63. package/dist/tools/types.d.ts +2 -0
  64. package/dist/tools/types.d.ts.map +1 -1
  65. package/dist/tools/types.js.map +1 -1
  66. package/dist/utils/write-coalescing.d.ts +3 -1
  67. package/dist/utils/write-coalescing.d.ts.map +1 -1
  68. package/dist/utils/write-coalescing.js +58 -6
  69. package/dist/utils/write-coalescing.js.map +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,1270 @@
1
+ /**
2
+ * Anchor Manager Service
3
+ * 앵커 상태 관리 및 국소 검색 기능 제공
4
+ *
5
+ * 클린코드 원칙:
6
+ * - 단일 책임 원칙: 앵커 상태 관리 및 국소 검색만 담당
7
+ * - 의존성 역전: Database와 다른 서비스에 의존
8
+ * - 캐시 최적화: 메모리 캐시를 통한 빠른 읽기 접근
9
+ */
10
+ import { getVectorSearchEngine } from '../algorithms/vector-search-engine.js';
11
+ import { UnifiedEmbeddingService } from './unified-embedding-service.js';
12
+ /**
13
+ * 앵커 설정 에러
14
+ */
15
+ export class AnchorError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'AnchorError';
19
+ }
20
+ }
21
+ /**
22
+ * 메모리를 찾을 수 없을 때 발생하는 에러
23
+ */
24
+ export class MemoryNotFoundError extends Error {
25
+ constructor(memoryId) {
26
+ super(`Memory with ID '${memoryId}' not found`);
27
+ this.name = 'MemoryNotFoundError';
28
+ }
29
+ }
30
+ /**
31
+ * Anchor Manager Service
32
+ * 앵커 상태 관리 및 국소 검색 기능 제공
33
+ */
34
+ export class AnchorManager {
35
+ /**
36
+ * 메모리 캐시: agent_id별 슬롯 상태 관리
37
+ * Map<agent_id, {A: memory_id | null, B: memory_id | null, C: memory_id | null}>
38
+ */
39
+ cache = new Map();
40
+ /**
41
+ * 쿼리 임베딩 서비스
42
+ */
43
+ queryEmbeddingService = new UnifiedEmbeddingService();
44
+ /**
45
+ * 슬롯별 설정
46
+ */
47
+ slotConfig = {
48
+ A: { hop_limit: 1, vector_threshold: 0.8 },
49
+ B: { hop_limit: 2, vector_threshold: 0.6 },
50
+ C: { hop_limit: 3, vector_threshold: 0.4 }
51
+ };
52
+ db = null;
53
+ embeddingService = null;
54
+ hybridSearchEngine = null;
55
+ vectorSearchEngine = null;
56
+ /**
57
+ * 생성자
58
+ */
59
+ constructor() {
60
+ console.log('✅ AnchorManager 서비스 초기화 완료');
61
+ }
62
+ /**
63
+ * 데이터베이스 설정
64
+ * @param db - 데이터베이스 인스턴스
65
+ */
66
+ setDatabase(db) {
67
+ if (!db) {
68
+ throw new Error('Database instance is required');
69
+ }
70
+ this.db = db;
71
+ // VectorSearchEngine이 설정되어 있으면 초기화
72
+ if (this.vectorSearchEngine) {
73
+ this.vectorSearchEngine.initialize(db);
74
+ }
75
+ }
76
+ /**
77
+ * 임베딩 서비스 설정
78
+ * @param embeddingService - 메모리 임베딩 서비스 인스턴스
79
+ */
80
+ setEmbeddingService(embeddingService) {
81
+ if (!embeddingService) {
82
+ throw new Error('MemoryEmbeddingService is required');
83
+ }
84
+ this.embeddingService = embeddingService;
85
+ }
86
+ /**
87
+ * 하이브리드 검색 엔진 설정
88
+ * @param hybridSearchEngine - 하이브리드 검색 엔진 인스턴스
89
+ */
90
+ setHybridSearchEngine(hybridSearchEngine) {
91
+ if (!hybridSearchEngine) {
92
+ throw new Error('HybridSearchEngine is required');
93
+ }
94
+ this.hybridSearchEngine = hybridSearchEngine;
95
+ }
96
+ /**
97
+ * 벡터 검색 엔진 설정
98
+ * @param vectorSearchEngine - 벡터 검색 엔진 인스턴스
99
+ */
100
+ setVectorSearchEngine(vectorSearchEngine) {
101
+ if (!vectorSearchEngine) {
102
+ throw new Error('VectorSearchEngine is required');
103
+ }
104
+ this.vectorSearchEngine = vectorSearchEngine;
105
+ // 데이터베이스가 이미 설정되어 있으면 초기화
106
+ if (this.db) {
107
+ this.vectorSearchEngine.initialize(this.db);
108
+ }
109
+ }
110
+ /**
111
+ * 앵커 설정
112
+ * @param agentId - 에이전트 ID
113
+ * @param memoryId - 메모리 ID
114
+ * @param slot - 슬롯 (A, B, C)
115
+ * @throws {MemoryNotFoundError} 메모리가 존재하지 않는 경우
116
+ * @throws {AnchorError} 동일한 memory_id를 다른 슬롯에 이미 설정한 경우
117
+ */
118
+ async setAnchor(agentId, memoryId, slot) {
119
+ if (!this.db) {
120
+ throw new Error('Database is not set. Call setDatabase() first.');
121
+ }
122
+ // 메모리 존재 확인
123
+ const memory = this.db.prepare(`
124
+ SELECT id FROM memory_item WHERE id = ?
125
+ `).get(memoryId);
126
+ if (!memory) {
127
+ throw new MemoryNotFoundError(memoryId);
128
+ }
129
+ // 동일한 agent_id가 동일한 memory_id를 다른 슬롯에 이미 설정했는지 확인
130
+ const existingAnchor = this.db.prepare(`
131
+ SELECT slot FROM anchor
132
+ WHERE agent_id = ? AND memory_id = ? AND slot != ?
133
+ `).get(agentId, memoryId, slot);
134
+ if (existingAnchor) {
135
+ throw new AnchorError(`Memory '${memoryId}' is already set as anchor in slot '${existingAnchor.slot}'. ` +
136
+ `An agent cannot set the same memory in multiple slots.`);
137
+ }
138
+ // 기존 앵커가 있으면 업데이트, 없으면 삽입
139
+ const existing = this.db.prepare(`
140
+ SELECT id FROM anchor WHERE agent_id = ? AND slot = ?
141
+ `).get(agentId, slot);
142
+ if (existing) {
143
+ // 업데이트
144
+ this.db.prepare(`
145
+ UPDATE anchor
146
+ SET memory_id = ?, updated_at = CURRENT_TIMESTAMP
147
+ WHERE agent_id = ? AND slot = ?
148
+ `).run(memoryId, agentId, slot);
149
+ }
150
+ else {
151
+ // 삽입
152
+ this.db.prepare(`
153
+ INSERT INTO anchor (agent_id, slot, memory_id)
154
+ VALUES (?, ?, ?)
155
+ `).run(agentId, slot, memoryId);
156
+ }
157
+ // 캐시 업데이트
158
+ this.updateCache(agentId, slot, memoryId);
159
+ }
160
+ /**
161
+ * 앵커 조회
162
+ * @param agentId - 에이전트 ID
163
+ * @param slot - 슬롯 (A, B, C), 선택적. 없으면 모든 슬롯 반환
164
+ * @returns 앵커 정보 또는 null
165
+ */
166
+ async getAnchor(agentId, slot) {
167
+ if (!this.db) {
168
+ throw new Error('Database is not set. Call setDatabase() first.');
169
+ }
170
+ // 캐시에서 먼저 확인
171
+ const cached = this.cache.get(agentId);
172
+ if (slot) {
173
+ // 특정 슬롯 조회
174
+ const cachedMemoryId = cached?.[slot];
175
+ if (cachedMemoryId !== undefined) {
176
+ // 캐시에 있으면 DB에서 상세 정보 조회
177
+ const anchor = this.db.prepare(`
178
+ SELECT agent_id, slot, memory_id, created_at, updated_at
179
+ FROM anchor
180
+ WHERE agent_id = ? AND slot = ?
181
+ `).get(agentId, slot);
182
+ return anchor || null;
183
+ }
184
+ }
185
+ else {
186
+ // 모든 슬롯 조회
187
+ if (cached) {
188
+ // 캐시에 있으면 DB에서 상세 정보 조회
189
+ const anchors = this.db.prepare(`
190
+ SELECT agent_id, slot, memory_id, created_at, updated_at
191
+ FROM anchor
192
+ WHERE agent_id = ?
193
+ ORDER BY slot
194
+ `).all(agentId);
195
+ return anchors;
196
+ }
197
+ }
198
+ // 캐시에 없으면 DB에서 조회 후 캐시 업데이트
199
+ if (slot) {
200
+ const anchor = this.db.prepare(`
201
+ SELECT agent_id, slot, memory_id, created_at, updated_at
202
+ FROM anchor
203
+ WHERE agent_id = ? AND slot = ?
204
+ `).get(agentId, slot);
205
+ if (anchor) {
206
+ this.updateCache(agentId, slot, anchor.memory_id);
207
+ }
208
+ return anchor || null;
209
+ }
210
+ else {
211
+ const anchors = this.db.prepare(`
212
+ SELECT agent_id, slot, memory_id, created_at, updated_at
213
+ FROM anchor
214
+ WHERE agent_id = ?
215
+ ORDER BY slot
216
+ `).all(agentId);
217
+ // 캐시 업데이트
218
+ for (const anchor of anchors) {
219
+ this.updateCache(anchor.agent_id, anchor.slot, anchor.memory_id);
220
+ }
221
+ return anchors.length > 0 ? anchors : null;
222
+ }
223
+ }
224
+ /**
225
+ * 앵커 제거
226
+ * @param agentId - 에이전트 ID
227
+ * @param slot - 슬롯 (A, B, C), 선택적. 없으면 모든 슬롯 제거
228
+ */
229
+ async clearAnchor(agentId, slot) {
230
+ if (!this.db) {
231
+ throw new Error('Database is not set. Call setDatabase() first.');
232
+ }
233
+ if (slot) {
234
+ // 특정 슬롯 제거
235
+ this.db.prepare(`
236
+ DELETE FROM anchor WHERE agent_id = ? AND slot = ?
237
+ `).run(agentId, slot);
238
+ // 캐시 업데이트
239
+ this.updateCache(agentId, slot, null);
240
+ }
241
+ else {
242
+ // 모든 슬롯 제거
243
+ this.db.prepare(`
244
+ DELETE FROM anchor WHERE agent_id = ?
245
+ `).run(agentId);
246
+ // 캐시에서 제거
247
+ this.cache.delete(agentId);
248
+ }
249
+ }
250
+ /**
251
+ * 국소 검색
252
+ * 앵커 메모리를 기준으로 N-hop 제한 검색 수행
253
+ * @param agentId - 에이전트 ID
254
+ * @param slot - 슬롯 (A, B, C)
255
+ * @param query - 검색 쿼리 (선택적)
256
+ * @param hopLimit - Hop 제한 (선택적, 기본값: 슬롯별 설정값)
257
+ * @param options - 검색 옵션
258
+ * @returns 검색 결과
259
+ */
260
+ async searchLocal(agentId, slot, query, hopLimit, options) {
261
+ const startTime = Date.now();
262
+ if (!this.db) {
263
+ throw new Error('Database is not set. Call setDatabase() first.');
264
+ }
265
+ if (!this.embeddingService) {
266
+ throw new Error('MemoryEmbeddingService is not set. Call setEmbeddingService() first.');
267
+ }
268
+ // 앵커 조회
269
+ const anchor = await this.getAnchor(agentId, slot);
270
+ if (!anchor || Array.isArray(anchor) || !anchor.memory_id) {
271
+ // 앵커가 없거나 memory_id가 NULL인 경우 (Edge Case: 앵커 없음 또는 메모리 삭제)
272
+ console.warn(`⚠️ No anchor set for agent '${agentId}' in slot '${slot}' (memory_id is NULL)`);
273
+ if (!query) {
274
+ // query가 없으면 에러 반환 (앵커 기반 리콜은 앵커가 필수)
275
+ throw new AnchorError(`No anchor set for agent '${agentId}' in slot '${slot}'. ` +
276
+ `Anchor is required for anchor-based recall. ` +
277
+ `If the anchor memory was deleted, please set a new anchor.`);
278
+ }
279
+ // query가 있으면 전역 검색으로 fallback
280
+ console.log(`🔄 Anchor missing, falling back to global search for query: "${query}"`);
281
+ return await this.fallbackToGlobalSearch(query, options, startTime);
282
+ }
283
+ // 슬롯별 설정 가져오기
284
+ const slotConfig = this.getSlotConfig(slot);
285
+ const finalHopLimit = hopLimit ?? slotConfig.hop_limit;
286
+ const vectorThreshold = slotConfig.vector_threshold;
287
+ // 검색 옵션 기본값
288
+ const limit = options?.limit ?? 10;
289
+ const minResults = options?.min_results ?? 3;
290
+ // 앵커 메모리 임베딩 조회 (Edge Case: 임베딩 없음, 메모리 삭제)
291
+ let anchorEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
292
+ if (!anchorEmbedding) {
293
+ // 임베딩이 없으면 메모리 존재 확인 후 생성 시도
294
+ const memory = this.db.prepare(`
295
+ SELECT id, content, type FROM memory_item WHERE id = ?
296
+ `).get(anchor.memory_id);
297
+ if (!memory) {
298
+ // Edge Case: 메모리가 삭제된 경우
299
+ console.warn(`⚠️ Anchor memory '${anchor.memory_id}' not found (may have been deleted)`);
300
+ // 앵커를 자동으로 정리 (선택적)
301
+ try {
302
+ await this.clearAnchor(agentId, slot);
303
+ console.log(`🧹 Cleared invalid anchor for agent '${agentId}' in slot '${slot}'`);
304
+ }
305
+ catch (error) {
306
+ console.error(`❌ Failed to clear invalid anchor:`, error);
307
+ }
308
+ // query가 있으면 전역 검색으로 fallback
309
+ if (query) {
310
+ console.log(`🔄 Anchor memory deleted, falling back to global search for query: "${query}"`);
311
+ return await this.fallbackToGlobalSearch(query, options, startTime);
312
+ }
313
+ // query가 없으면 에러 반환
314
+ throw new MemoryNotFoundError(anchor.memory_id +
315
+ ` (Memory may have been deleted. Please set a new anchor.)`);
316
+ }
317
+ // Edge Case: 임베딩 없음 - 생성 시도
318
+ console.log(`🔄 Generating embedding for anchor memory '${anchor.memory_id}'`);
319
+ const embeddingResult = await this.embeddingService.createAndStoreEmbedding(this.db, memory.id, memory.content, memory.type);
320
+ if (!embeddingResult) {
321
+ throw new Error(`Failed to generate embedding for anchor memory '${anchor.memory_id}'. ` +
322
+ `Please check if the embedding service is available.`);
323
+ }
324
+ // 생성된 임베딩 다시 조회
325
+ const newEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
326
+ if (!newEmbedding) {
327
+ throw new Error(`Failed to retrieve newly created embedding for '${anchor.memory_id}'. ` +
328
+ `Please try again or check the database.`);
329
+ }
330
+ // 새로 생성된 임베딩 사용
331
+ anchorEmbedding = newEmbedding;
332
+ console.log(`✅ Embedding generated and retrieved for anchor memory '${anchor.memory_id}'`);
333
+ }
334
+ // VectorSearchEngine이 없으면 생성
335
+ if (!this.vectorSearchEngine) {
336
+ this.vectorSearchEngine = getVectorSearchEngine();
337
+ if (this.db) {
338
+ this.vectorSearchEngine.initialize(this.db);
339
+ }
340
+ }
341
+ // N-hop 검색 구현
342
+ const allHopResults = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchor.memory_id, vectorThreshold, finalHopLimit, limit * 2 // 더 많이 가져와서 필터링 후 최종 limit 적용
343
+ );
344
+ // 쿼리가 있는 경우 쿼리 기반 필터링 (작업 3.7)
345
+ let filteredResults = allHopResults;
346
+ let queryEmbeddingForReanchor;
347
+ if (query && query.trim().length > 0) {
348
+ filteredResults = await this.filterByQuery(query, allHopResults, anchorEmbedding.provider);
349
+ // 자동 앵커 이동을 위한 쿼리 임베딩 생성 (선택적, 비동기)
350
+ try {
351
+ const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
352
+ if (queryEmbeddingResult && queryEmbeddingResult.embedding) {
353
+ queryEmbeddingForReanchor = queryEmbeddingResult.embedding;
354
+ }
355
+ }
356
+ catch (error) {
357
+ // 쿼리 임베딩 생성 실패는 무시 (자동 이동은 선택적)
358
+ console.debug('쿼리 임베딩 생성 실패 (자동 앵커 이동용):', error);
359
+ }
360
+ }
361
+ // 결과 포맷팅
362
+ const formattedResults = filteredResults.map(result => ({
363
+ id: result.memory_id,
364
+ content: result.content,
365
+ type: result.type,
366
+ similarity: result.similarity,
367
+ hop_distance: result.hop_distance,
368
+ importance: result.importance,
369
+ created_at: result.created_at,
370
+ tags: result.tags
371
+ }));
372
+ // 최종 limit 적용
373
+ const localResults = formattedResults.slice(0, limit);
374
+ const localCount = localResults.length;
375
+ // Fallback 체크 (query가 있을 때만, min_results 미만 시)
376
+ let fallbackUsed = false;
377
+ let finalResults = localResults;
378
+ let totalCount = localCount;
379
+ if (query && query.trim().length > 0 && localCount < minResults) {
380
+ try {
381
+ console.log(`🔄 Fallback to global search: local results (${localCount}) < min_results (${minResults})`);
382
+ // Fallback 수행
383
+ const fallbackResult = await this.fallbackToGlobalSearch(query, { ...options, limit: limit - localCount }, // 부족한 만큼만 가져오기
384
+ startTime);
385
+ fallbackUsed = true;
386
+ // Local 결과와 Fallback 결과 병합
387
+ // Local 결과를 우선하고, 중복 제거 (memory_id 기준)
388
+ const localMemoryIds = new Set(localResults.map(r => r.id));
389
+ const fallbackItems = fallbackResult.items
390
+ .filter(item => !localMemoryIds.has(item.id))
391
+ .map(item => ({
392
+ id: item.id,
393
+ content: item.content,
394
+ type: item.type,
395
+ similarity: item.similarity ?? 0,
396
+ hop_distance: item.hop_distance ?? 999, // fallback 결과는 hop_distance가 없으므로 큰 값으로 설정
397
+ importance: item.importance ?? 0.5,
398
+ created_at: item.created_at ?? new Date().toISOString(),
399
+ tags: item.tags ?? undefined
400
+ }));
401
+ // Local 결과 + Fallback 결과 (중복 제거된 것만)
402
+ finalResults = [...localResults, ...fallbackItems].slice(0, limit);
403
+ totalCount = finalResults.length;
404
+ console.log(`✅ Fallback 완료: local ${localCount} + fallback ${fallbackItems.length} = total ${totalCount}`);
405
+ }
406
+ catch (error) {
407
+ console.error('❌ Fallback 실패:', error);
408
+ // Fallback 실패 시 local 결과만 반환
409
+ fallbackUsed = false;
410
+ }
411
+ }
412
+ // 자동 앵커 이동 체크 (query가 있고 queryEmbeddingForReanchor가 생성된 경우)
413
+ // 기본적으로 활성화되어 있으며, options에 autoMoveEnabled: false를 전달하여 비활성화 가능
414
+ if (queryEmbeddingForReanchor && options?.autoMoveEnabled !== false) {
415
+ try {
416
+ await this.checkAndAutoReanchor(agentId, slot, queryEmbeddingForReanchor, true);
417
+ }
418
+ catch (error) {
419
+ // 자동 앵커 이동 실패는 검색 결과에 영향을 주지 않음
420
+ console.debug('⚠️ 자동 앵커 이동 체크 실패 (무시됨):', error);
421
+ }
422
+ }
423
+ const queryTime = Date.now() - startTime;
424
+ return {
425
+ items: finalResults,
426
+ total_count: totalCount,
427
+ local_results_count: localCount,
428
+ fallback_used: fallbackUsed,
429
+ query_time: queryTime,
430
+ anchor_info: {
431
+ agent_id: agentId,
432
+ slot: slot,
433
+ memory_id: anchor.memory_id
434
+ }
435
+ };
436
+ }
437
+ /**
438
+ * 앵커 메모리의 임베딩 조회
439
+ * @param memoryId - 메모리 ID
440
+ * @returns 임베딩 벡터 및 제공자 정보, 없으면 null
441
+ * @throws {MemoryNotFoundError} 메모리가 삭제된 경우
442
+ */
443
+ async getAnchorEmbedding(memoryId) {
444
+ if (!this.db) {
445
+ throw new Error('Database is not set.');
446
+ }
447
+ try {
448
+ // Edge Case: 메모리 존재 확인 (메모리 삭제 체크)
449
+ const memoryExists = this.db.prepare(`
450
+ SELECT id FROM memory_item WHERE id = ?
451
+ `).get(memoryId);
452
+ if (!memoryExists) {
453
+ // 메모리가 삭제된 경우
454
+ console.warn(`⚠️ Memory '${memoryId}' not found (may have been deleted)`);
455
+ return null;
456
+ }
457
+ const embeddingRecord = this.db.prepare(`
458
+ SELECT
459
+ embedding,
460
+ embedding_provider,
461
+ dimensions,
462
+ dim
463
+ FROM memory_embedding
464
+ WHERE memory_id = ?
465
+ ORDER BY created_at DESC
466
+ LIMIT 1
467
+ `).get(memoryId);
468
+ if (!embeddingRecord || !embeddingRecord.embedding) {
469
+ // Edge Case: 임베딩 없음 (메모리는 존재하지만 임베딩이 없음)
470
+ return null;
471
+ }
472
+ // JSON 문자열로 저장된 임베딩을 배열로 파싱
473
+ let embeddingVector;
474
+ try {
475
+ embeddingVector = typeof embeddingRecord.embedding === 'string'
476
+ ? JSON.parse(embeddingRecord.embedding)
477
+ : embeddingRecord.embedding;
478
+ if (!Array.isArray(embeddingVector) || embeddingVector.length === 0) {
479
+ // Edge Case: 유효하지 않은 임베딩
480
+ console.warn(`⚠️ Invalid embedding for memory '${memoryId}' (empty or not an array)`);
481
+ return null;
482
+ }
483
+ }
484
+ catch (error) {
485
+ // Edge Case: 임베딩 파싱 실패
486
+ console.error(`❌ 임베딩 파싱 실패 (${memoryId}):`, error);
487
+ return null;
488
+ }
489
+ const provider = embeddingRecord.embedding_provider || 'tfidf';
490
+ return {
491
+ embedding: embeddingVector,
492
+ provider: provider
493
+ };
494
+ }
495
+ catch (error) {
496
+ // Edge Case: 데이터베이스 오류
497
+ console.error(`❌ 임베딩 조회 실패 (${memoryId}):`, error);
498
+ return null;
499
+ }
500
+ }
501
+ /**
502
+ * 1-hop 검색: 앵커와 직접적으로 유사한 메모리 검색
503
+ * @param anchorEmbedding - 앵커 메모리의 임베딩 벡터
504
+ * @param provider - 임베딩 제공자
505
+ * @param anchorMemoryId - 앵커 메모리 ID (제외할 메모리)
506
+ * @param threshold - 유사도 임계값
507
+ * @param limit - 최대 결과 수
508
+ * @returns 검색 결과
509
+ */
510
+ async searchOneHop(anchorEmbedding, provider, anchorMemoryId, threshold, limit) {
511
+ if (!this.vectorSearchEngine || !this.db) {
512
+ throw new Error('VectorSearchEngine or Database is not set.');
513
+ }
514
+ try {
515
+ // VectorSearchEngine 초기화 확인
516
+ if (typeof this.vectorSearchEngine.initialize === 'function') {
517
+ this.vectorSearchEngine.initialize(this.db);
518
+ }
519
+ // 벡터 검색 실행 (임계값은 낮게 설정하고 나중에 필터링)
520
+ const searchResults = await this.vectorSearchEngine.search(anchorEmbedding, {
521
+ limit: limit + 1, // 자기 자신 제외를 위해 +1
522
+ threshold: 0.0, // 임계값은 나중에 필터링에서 적용
523
+ includeContent: true,
524
+ includeMetadata: true
525
+ }, provider);
526
+ // 결과 필터링: 앵커 메모리 제외, 유사도 임계값 이상만 반환
527
+ const filteredResults = searchResults
528
+ .filter(result => {
529
+ // 앵커 메모리 제외
530
+ if (result.memory_id === anchorMemoryId) {
531
+ return false;
532
+ }
533
+ // 유사도 임계값 이상만 반환
534
+ return result.similarity >= threshold;
535
+ })
536
+ .slice(0, limit); // 최종 limit 적용
537
+ return filteredResults;
538
+ }
539
+ catch (error) {
540
+ console.error(`❌ 1-hop 검색 실패:`, error);
541
+ throw new Error(`Failed to perform 1-hop search: ${error instanceof Error ? error.message : 'Unknown error'}`);
542
+ }
543
+ }
544
+ /**
545
+ * N-hop 검색: 앵커를 기준으로 최대 N-hop까지 확장 검색
546
+ * @param anchorEmbedding - 앵커 메모리의 임베딩 벡터
547
+ * @param provider - 임베딩 제공자
548
+ * @param anchorMemoryId - 앵커 메모리 ID (제외할 메모리)
549
+ * @param threshold - 유사도 임계값
550
+ * @param maxHops - 최대 hop 수
551
+ * @param limit - 최대 결과 수 (전체 hop 합계)
552
+ * @returns 검색 결과 (hop_distance 포함)
553
+ */
554
+ async searchNHop(anchorEmbedding, provider, anchorMemoryId, threshold, maxHops, limit) {
555
+ if (!this.vectorSearchEngine || !this.db) {
556
+ throw new Error('VectorSearchEngine or Database is not set.');
557
+ }
558
+ // VectorSearchEngine 초기화 확인
559
+ if (typeof this.vectorSearchEngine.initialize === 'function') {
560
+ this.vectorSearchEngine.initialize(this.db);
561
+ }
562
+ // 이미 발견된 메모리 ID 추적 (중복 방지)
563
+ const discoveredMemoryIds = new Set([anchorMemoryId]);
564
+ // 각 hop 레벨의 결과를 저장
565
+ const allResults = [];
566
+ // 현재 hop 레벨의 메모리들 (임베딩 포함)
567
+ // 1-hop: 앵커 임베딩을 사용
568
+ let currentHopMemories = [
569
+ { memory_id: anchorMemoryId, embedding: anchorEmbedding }
570
+ ];
571
+ // 각 hop 레벨별로 검색 수행
572
+ for (let hop = 1; hop <= maxHops; hop++) {
573
+ const nextHopMemories = [];
574
+ const hopResults = [];
575
+ // 현재 hop의 각 메모리에 대해 검색 수행
576
+ for (const currentMemory of currentHopMemories) {
577
+ try {
578
+ // memory_link를 활용한 직접 연결된 메모리 조회 (최적화)
579
+ const linkedMemories = await this.getLinkedMemories(currentMemory.memory_id);
580
+ // 벡터 검색 실행
581
+ const vectorSearchResults = await this.vectorSearchEngine.search(currentMemory.embedding, {
582
+ limit: Math.ceil(limit / maxHops) + 10, // 각 hop당 충분한 결과 가져오기
583
+ threshold: 0.0, // 임계값은 나중에 필터링에서 적용
584
+ includeContent: true,
585
+ includeMetadata: true
586
+ }, provider);
587
+ // 디버깅: 벡터 검색 결과 로깅
588
+ if (hop === 1 && currentMemory.memory_id === anchorMemoryId) {
589
+ console.log(`🔍 [Debug] 벡터 검색 결과 (${hop}-hop, 앵커: ${anchorMemoryId}):`, {
590
+ totalResults: vectorSearchResults.length,
591
+ top5Similarities: vectorSearchResults.slice(0, 5).map(r => ({
592
+ memory_id: r.memory_id,
593
+ similarity: r.similarity.toFixed(4)
594
+ })),
595
+ threshold,
596
+ provider
597
+ });
598
+ }
599
+ // memory_link 결과와 벡터 검색 결과를 병합
600
+ // memory_link 결과는 우선순위가 높음 (직접 연결된 관계)
601
+ const allCandidates = new Map();
602
+ // memory_link 결과 추가 (우선순위 높음)
603
+ for (const linked of linkedMemories) {
604
+ if (!discoveredMemoryIds.has(linked.memory_id)) {
605
+ allCandidates.set(linked.memory_id, {
606
+ ...linked,
607
+ isLinked: true
608
+ });
609
+ }
610
+ }
611
+ // 벡터 검색 결과 추가 (memory_link에 없는 경우만)
612
+ // 임계값을 낮춰서 더 많은 결과를 포함 (나중에 effectiveThreshold로 재필터링)
613
+ const relaxedThreshold = threshold * 0.5; // 임계값을 50%로 완화하여 후보 확보
614
+ for (const result of vectorSearchResults) {
615
+ if (!allCandidates.has(result.memory_id) && !discoveredMemoryIds.has(result.memory_id)) {
616
+ // 완화된 임계값으로 후보 추가 (나중에 effectiveThreshold로 재필터링)
617
+ if (result.similarity >= relaxedThreshold) {
618
+ allCandidates.set(result.memory_id, {
619
+ ...result,
620
+ isLinked: false
621
+ });
622
+ }
623
+ }
624
+ else if (allCandidates.has(result.memory_id)) {
625
+ // memory_link로 이미 추가된 경우, 유사도 정보 업데이트
626
+ const existing = allCandidates.get(result.memory_id);
627
+ existing.similarity = Math.max(existing.similarity, result.similarity);
628
+ }
629
+ }
630
+ // 결과 필터링 및 추가
631
+ for (const [memoryId, candidate] of allCandidates.entries()) {
632
+ // 이미 발견된 메모리 제외
633
+ if (discoveredMemoryIds.has(memoryId)) {
634
+ continue;
635
+ }
636
+ // 유사도 임계값 이상만 반환 (memory_link는 임계값 완화 가능)
637
+ const effectiveThreshold = candidate.isLinked
638
+ ? threshold * 0.8 // memory_link 연결은 임계값 20% 완화
639
+ : threshold;
640
+ if (candidate.similarity < effectiveThreshold) {
641
+ continue;
642
+ }
643
+ // 새로 발견된 메모리
644
+ discoveredMemoryIds.add(memoryId);
645
+ hopResults.push({
646
+ memory_id: candidate.memory_id,
647
+ content: candidate.content,
648
+ type: candidate.type,
649
+ similarity: candidate.similarity,
650
+ importance: candidate.importance,
651
+ created_at: candidate.created_at,
652
+ tags: candidate.tags
653
+ });
654
+ // 다음 hop을 위한 임베딩 조회 (다음 hop이 있을 경우)
655
+ // Edge Case: 중간 hop의 메모리가 삭제되거나 임베딩이 없는 경우 무시
656
+ if (hop < maxHops) {
657
+ try {
658
+ const nextEmbedding = await this.getAnchorEmbedding(candidate.memory_id);
659
+ if (nextEmbedding && nextEmbedding.embedding) {
660
+ nextHopMemories.push({
661
+ memory_id: candidate.memory_id,
662
+ embedding: nextEmbedding.embedding
663
+ });
664
+ }
665
+ else {
666
+ // 임베딩이 없으면 다음 hop에서 제외 (경고 없이)
667
+ console.debug(`⚠️ Skipping memory '${candidate.memory_id}' for next hop: no embedding`);
668
+ }
669
+ }
670
+ catch (error) {
671
+ // 메모리 삭제 또는 임베딩 조회 실패 시 다음 hop에서 제외
672
+ console.debug(`⚠️ Skipping memory '${candidate.memory_id}' for next hop: ${error instanceof Error ? error.message : 'Unknown error'}`);
673
+ }
674
+ }
675
+ }
676
+ }
677
+ catch (error) {
678
+ console.error(`❌ ${hop}-hop 검색 실패 (${currentMemory.memory_id}):`, error);
679
+ // 개별 메모리 검색 실패는 무시하고 계속 진행
680
+ continue;
681
+ }
682
+ }
683
+ // 현재 hop의 결과를 전체 결과에 추가 (hop_distance 설정)
684
+ for (const result of hopResults) {
685
+ allResults.push({
686
+ ...result,
687
+ hop_distance: hop
688
+ });
689
+ }
690
+ // limit에 도달했으면 중단
691
+ if (allResults.length >= limit) {
692
+ break;
693
+ }
694
+ // 다음 hop을 위한 메모리가 없으면 중단
695
+ if (nextHopMemories.length === 0) {
696
+ break;
697
+ }
698
+ // 다음 hop을 위한 메모리로 업데이트
699
+ currentHopMemories = nextHopMemories;
700
+ }
701
+ // 랭킹 점수 계산 및 적용 (hop 거리 기반 점수 + 앵커 근처 부스트)
702
+ const rankedResults = allResults.map(result => {
703
+ const rankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
704
+ return {
705
+ ...result,
706
+ similarity: rankingScore // 랭킹 점수로 업데이트
707
+ };
708
+ });
709
+ // 랭킹 점수 기준으로 정렬 (높은 점수 우선)
710
+ rankedResults.sort((a, b) => {
711
+ // 점수가 같으면 hop 거리가 가까운 것 우선
712
+ if (Math.abs(a.similarity - b.similarity) < 0.001) {
713
+ return a.hop_distance - b.hop_distance;
714
+ }
715
+ // 점수가 높은 것 우선
716
+ return b.similarity - a.similarity;
717
+ });
718
+ // 최종 limit 적용
719
+ return rankedResults.slice(0, limit);
720
+ }
721
+ /**
722
+ * 쿼리 기반 필터링: 앵커 주변 검색 결과 중 쿼리와 관련된 메모리만 필터링
723
+ * @param query - 검색 쿼리
724
+ * @param results - 앵커 주변 검색 결과
725
+ * @param provider - 임베딩 제공자
726
+ * @returns 필터링된 결과 (쿼리 관련성 점수 포함)
727
+ */
728
+ async filterByQuery(query, results, provider) {
729
+ if (results.length === 0) {
730
+ return results;
731
+ }
732
+ try {
733
+ // 1. 쿼리 임베딩 생성
734
+ const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
735
+ if (!queryEmbeddingResult || !queryEmbeddingResult.embedding) {
736
+ console.warn('⚠️ 쿼리 임베딩 생성 실패, 필터링 건너뜀');
737
+ return results;
738
+ }
739
+ const queryEmbedding = queryEmbeddingResult.embedding;
740
+ // 2. 각 결과 메모리의 임베딩 조회 및 쿼리 유사도 계산
741
+ const resultsWithQuerySimilarity = await Promise.all(results.map(async (result) => {
742
+ try {
743
+ // 메모리 임베딩 조회
744
+ const memoryEmbedding = await this.getAnchorEmbedding(result.memory_id);
745
+ if (!memoryEmbedding || !memoryEmbedding.embedding) {
746
+ // 임베딩이 없으면 쿼리 유사도 0으로 설정
747
+ return {
748
+ ...result,
749
+ query_similarity: 0,
750
+ combined_similarity: result.similarity * 0.5 // 앵커 유사도만 반영
751
+ };
752
+ }
753
+ // 쿼리 임베딩과 메모리 임베딩 간 유사도 계산
754
+ // 차원이 다를 수 있으므로 호환성 확인
755
+ let querySim = 0;
756
+ if (queryEmbedding.length === memoryEmbedding.embedding.length) {
757
+ querySim = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
758
+ }
759
+ else {
760
+ // 차원이 다르면 텍스트 기반 간단한 매칭 (fallback)
761
+ const queryLower = query.toLowerCase();
762
+ const contentLower = result.content.toLowerCase();
763
+ const queryWords = queryLower.split(/\s+/);
764
+ const matchCount = queryWords.filter(word => contentLower.includes(word)).length;
765
+ querySim = matchCount / Math.max(queryWords.length, 1);
766
+ }
767
+ // hop 거리 기반 랭킹 점수 계산
768
+ const baseRankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
769
+ // 결합 유사도: 랭킹 점수(60%) + 쿼리 유사도(40%)
770
+ const combinedSimilarity = baseRankingScore * 0.6 + querySim * 0.4;
771
+ return {
772
+ ...result,
773
+ query_similarity: querySim,
774
+ combined_similarity: combinedSimilarity
775
+ };
776
+ }
777
+ catch (error) {
778
+ console.error(`❌ 쿼리 필터링 실패 (${result.memory_id}):`, error);
779
+ // 에러 발생 시 원본 similarity 사용
780
+ return {
781
+ ...result,
782
+ query_similarity: 0,
783
+ combined_similarity: result.similarity * 0.5
784
+ };
785
+ }
786
+ }));
787
+ // 3. 쿼리 유사도 임계값 적용 (0.3 이상만 유지)
788
+ const queryThreshold = 0.3;
789
+ const filtered = resultsWithQuerySimilarity.filter(r => r.query_similarity >= queryThreshold || r.combined_similarity >= 0.5);
790
+ // 4. 결합 유사도 기준으로 재정렬
791
+ filtered.sort((a, b) => {
792
+ // 결합 유사도가 같으면 hop 거리가 가까운 것 우선
793
+ if (Math.abs(a.combined_similarity - b.combined_similarity) < 0.001) {
794
+ return a.hop_distance - b.hop_distance;
795
+ }
796
+ // 결합 유사도가 높은 것 우선
797
+ return b.combined_similarity - a.combined_similarity;
798
+ });
799
+ // 5. 원본 similarity를 combined_similarity로 업데이트하여 반환
800
+ return filtered.map(r => ({
801
+ ...r,
802
+ similarity: r.combined_similarity
803
+ }));
804
+ }
805
+ catch (error) {
806
+ console.error('❌ 쿼리 기반 필터링 실패:', error);
807
+ // 에러 발생 시 원본 결과 반환
808
+ return results;
809
+ }
810
+ }
811
+ /**
812
+ * 검색 결과 랭킹 점수 계산 (hop 거리 기반 점수 + 앵커 근처 부스트)
813
+ * @param similarity - 벡터 유사도 (0-1)
814
+ * @param hopDistance - hop 거리 (1부터 시작)
815
+ * @param importance - 메모리 중요도 (0-1)
816
+ * @returns 랭킹 점수 (0-1)
817
+ */
818
+ calculateRankingScore(similarity, hopDistance, importance = 0.5) {
819
+ // 1. Hop 거리 기반 점수 감쇠 (거리가 멀수록 점수 감소)
820
+ // hop_distance=1: 1.0, hop_distance=2: 0.7, hop_distance=3: 0.5
821
+ const hopDecayFactor = 1.0 / (1.0 + (hopDistance - 1) * 0.3);
822
+ // 2. 앵커 근처 부스트 (1-hop은 추가 부스트)
823
+ const anchorProximityBoost = hopDistance === 1 ? 1.2 : 1.0; // 1-hop은 20% 부스트
824
+ // 3. 중요도 가중치 (0.1 가중치)
825
+ const importanceWeight = 0.1;
826
+ const importanceBoost = 1.0 + (importance - 0.5) * importanceWeight;
827
+ // 4. 최종 랭킹 점수 계산
828
+ // 기본 공식: similarity * hop_decay * proximity_boost * importance_boost
829
+ // 점수가 1.0을 초과하지 않도록 클램프
830
+ const rankingScore = Math.min(1.0, similarity * hopDecayFactor * anchorProximityBoost * importanceBoost);
831
+ return rankingScore;
832
+ }
833
+ /**
834
+ * 코사인 유사도 계산
835
+ * @param a - 벡터 A
836
+ * @param b - 벡터 B
837
+ * @returns 코사인 유사도 (0-1)
838
+ */
839
+ cosineSimilarity(a, b) {
840
+ if (a.length !== b.length) {
841
+ throw new Error('벡터 차원이 일치하지 않습니다');
842
+ }
843
+ let dotProduct = 0;
844
+ let normA = 0;
845
+ let normB = 0;
846
+ for (let i = 0; i < a.length; i++) {
847
+ const aVal = a[i] ?? 0;
848
+ const bVal = b[i] ?? 0;
849
+ dotProduct += aVal * bVal;
850
+ normA += aVal * aVal;
851
+ normB += bVal * bVal;
852
+ }
853
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
854
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
855
+ }
856
+ /**
857
+ * memory_link 테이블을 활용한 직접 연결된 메모리 조회
858
+ * @param memoryId - 메모리 ID
859
+ * @returns 연결된 메모리 목록 (임베딩 정보 포함)
860
+ */
861
+ async getLinkedMemories(memoryId) {
862
+ if (!this.db) {
863
+ return [];
864
+ }
865
+ try {
866
+ // memory_link를 통해 연결된 메모리 조회
867
+ const linkedRecords = this.db.prepare(`
868
+ SELECT
869
+ ml.target_id as memory_id,
870
+ mi.content,
871
+ mi.type,
872
+ mi.importance,
873
+ mi.created_at,
874
+ mi.tags,
875
+ ml.relation_type
876
+ FROM memory_link ml
877
+ JOIN memory_item mi ON mi.id = ml.target_id
878
+ WHERE ml.source_id = ?
879
+ ORDER BY ml.created_at DESC
880
+ `).all(memoryId);
881
+ // 결과 포맷팅
882
+ return linkedRecords.map(record => ({
883
+ memory_id: record.memory_id,
884
+ content: record.content,
885
+ type: record.type,
886
+ similarity: 0.9, // memory_link 연결은 높은 유사도로 간주
887
+ importance: record.importance,
888
+ created_at: record.created_at,
889
+ tags: record.tags ? (typeof record.tags === 'string' ? JSON.parse(record.tags) : record.tags) : undefined
890
+ }));
891
+ }
892
+ catch (error) {
893
+ console.error(`❌ memory_link 조회 실패 (${memoryId}):`, error);
894
+ return [];
895
+ }
896
+ }
897
+ /**
898
+ * 전역 검색으로 Fallback
899
+ * @param query - 검색 쿼리
900
+ * @param options - 검색 옵션
901
+ * @param startTime - 시작 시간
902
+ * @returns 검색 결과
903
+ */
904
+ async fallbackToGlobalSearch(query, options, startTime) {
905
+ if (!this.hybridSearchEngine) {
906
+ throw new Error('HybridSearchEngine is not set. Call setHybridSearchEngine() first.');
907
+ }
908
+ if (!this.db) {
909
+ throw new Error('Database is not set.');
910
+ }
911
+ const limit = options?.limit ?? 10;
912
+ const fallbackStartTime = Date.now();
913
+ try {
914
+ // HybridSearchEngine을 사용한 전역 검색
915
+ const globalSearchResult = await this.hybridSearchEngine.search(this.db, {
916
+ query: query,
917
+ limit: limit,
918
+ vectorWeight: options?.vector_weight,
919
+ textWeight: options?.text_weight
920
+ });
921
+ // HybridSearchResult를 SearchResult 형식으로 변환
922
+ const convertedItems = globalSearchResult.items.map(item => ({
923
+ id: item.id,
924
+ content: item.content,
925
+ type: item.type,
926
+ similarity: item.finalScore, // finalScore를 similarity로 사용
927
+ importance: item.importance,
928
+ created_at: item.created_at,
929
+ tags: item.tags,
930
+ // fallback 결과는 hop_distance가 없음 (전역 검색이므로)
931
+ hop_distance: undefined
932
+ }));
933
+ const queryTime = startTime ? Date.now() - startTime : Date.now() - fallbackStartTime;
934
+ return {
935
+ items: convertedItems,
936
+ total_count: convertedItems.length,
937
+ local_results_count: 0, // 전역 검색이므로 local 결과는 0
938
+ fallback_used: true,
939
+ query_time: queryTime
940
+ };
941
+ }
942
+ catch (error) {
943
+ console.error('❌ 전역 검색 Fallback 실패:', error);
944
+ const queryTime = startTime ? Date.now() - startTime : 0;
945
+ // 에러 발생 시 빈 결과 반환
946
+ return {
947
+ items: [],
948
+ total_count: 0,
949
+ local_results_count: 0,
950
+ fallback_used: true,
951
+ query_time: queryTime
952
+ };
953
+ }
954
+ }
955
+ /**
956
+ * 서버 재시작 시 DB에서 캐시 복원
957
+ * @param db - 데이터베이스 인스턴스
958
+ */
959
+ async restoreCacheFromDB(db) {
960
+ if (!db) {
961
+ throw new Error('Database instance is required');
962
+ }
963
+ try {
964
+ // anchor 테이블 존재 여부 확인
965
+ const tableExists = db.prepare(`
966
+ SELECT name FROM sqlite_master
967
+ WHERE type='table' AND name='anchor'
968
+ `).get();
969
+ if (!tableExists) {
970
+ // 테이블이 없으면 빈 캐시로 시작 (마이그레이션이 아직 실행되지 않았을 수 있음)
971
+ this.cache.clear();
972
+ console.log('⚠️ Anchor table does not exist yet, starting with empty cache');
973
+ return;
974
+ }
975
+ const anchors = db.prepare(`
976
+ SELECT agent_id, slot, memory_id
977
+ FROM anchor
978
+ ORDER BY agent_id, slot
979
+ `).all();
980
+ // 캐시 초기화
981
+ this.cache.clear();
982
+ // DB 데이터로 캐시 복원
983
+ for (const anchor of anchors) {
984
+ const agentId = anchor.agent_id;
985
+ const slot = anchor.slot;
986
+ const memoryId = anchor.memory_id;
987
+ if (!this.cache.has(agentId)) {
988
+ this.cache.set(agentId, { A: null, B: null, C: null });
989
+ }
990
+ const agentCache = this.cache.get(agentId);
991
+ agentCache[slot] = memoryId;
992
+ }
993
+ console.log(`✅ Anchor cache restored: ${this.cache.size} agents`);
994
+ }
995
+ catch (error) {
996
+ // 에러 발생 시 빈 캐시로 시작 (테이블이 없거나 다른 문제)
997
+ this.cache.clear();
998
+ console.warn('⚠️ Failed to restore anchor cache from DB, starting with empty cache:', error instanceof Error ? error.message : String(error));
999
+ }
1000
+ }
1001
+ /**
1002
+ * 캐시 업데이트 헬퍼 메서드
1003
+ * @param agentId - 에이전트 ID
1004
+ * @param slot - 슬롯
1005
+ * @param memoryId - 메모리 ID (null이면 제거)
1006
+ */
1007
+ updateCache(agentId, slot, memoryId) {
1008
+ if (!this.cache.has(agentId)) {
1009
+ this.cache.set(agentId, { A: null, B: null, C: null });
1010
+ }
1011
+ const agentCache = this.cache.get(agentId);
1012
+ agentCache[slot] = memoryId;
1013
+ }
1014
+ /**
1015
+ * 슬롯별 설정 조회
1016
+ * @param slot - 슬롯
1017
+ * @returns 슬롯 설정 (hop_limit, vector_threshold)
1018
+ */
1019
+ getSlotConfig(slot) {
1020
+ return this.slotConfig[slot];
1021
+ }
1022
+ /**
1023
+ * 자동 앵커 이동 점수 계산
1024
+ * 사용 빈도와 의미적 거리를 종합한 점수
1025
+ * @param memoryId - 메모리 ID
1026
+ * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1027
+ * @param anchorEmbedding - 현재 앵커 임베딩 (선택적)
1028
+ * @returns 앵커 이동 점수 (0-1)
1029
+ */
1030
+ async calculateReanchorScore(memoryId, queryEmbedding, anchorEmbedding) {
1031
+ if (!this.db) {
1032
+ return 0;
1033
+ }
1034
+ try {
1035
+ // 1. 사용 빈도 점수 계산 (view_count, cite_count, last_accessed 기반)
1036
+ const memory = this.db.prepare(`
1037
+ SELECT
1038
+ view_count,
1039
+ cite_count,
1040
+ edit_count,
1041
+ last_accessed,
1042
+ created_at,
1043
+ importance
1044
+ FROM memory_item
1045
+ WHERE id = ?
1046
+ `).get(memoryId);
1047
+ if (!memory) {
1048
+ return 0;
1049
+ }
1050
+ // 사용 빈도 점수 (로그 스케일)
1051
+ const usageScore = Math.min(1.0, (Math.log(1 + memory.view_count) +
1052
+ 2 * Math.log(1 + memory.cite_count) +
1053
+ 0.5 * Math.log(1 + memory.edit_count)) / 10 // 정규화
1054
+ );
1055
+ // 최근성 점수 (last_accessed 기반, 최근일수록 높음)
1056
+ let recencyScore = 0.5; // 기본값
1057
+ if (memory.last_accessed) {
1058
+ const lastAccessed = new Date(memory.last_accessed);
1059
+ const now = new Date();
1060
+ const daysSinceAccess = (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
1061
+ // 7일 이내면 높은 점수, 그 이후로 감소
1062
+ recencyScore = Math.max(0, 1.0 - daysSinceAccess / 30);
1063
+ }
1064
+ // 중요도 점수
1065
+ const importanceScore = memory.importance || 0.5;
1066
+ // 2. 의미적 거리 점수 계산 (query와의 유사도)
1067
+ let semanticScore = 0.5; // 기본값
1068
+ if (queryEmbedding) {
1069
+ const memoryEmbedding = await this.getAnchorEmbedding(memoryId);
1070
+ if (memoryEmbedding && memoryEmbedding.embedding) {
1071
+ const similarity = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
1072
+ semanticScore = similarity;
1073
+ }
1074
+ }
1075
+ // 3. 현재 앵커와의 비교 (현재 앵커보다 더 나은지)
1076
+ let anchorComparisonScore = 0.5; // 기본값
1077
+ if (anchorEmbedding) {
1078
+ const memoryEmbedding = await this.getAnchorEmbedding(memoryId);
1079
+ if (memoryEmbedding && memoryEmbedding.embedding) {
1080
+ const similarity = this.cosineSimilarity(anchorEmbedding, memoryEmbedding.embedding);
1081
+ // 현재 앵커와 유사하면 낮은 점수, 다르면 높은 점수 (다양성)
1082
+ anchorComparisonScore = 1.0 - similarity;
1083
+ }
1084
+ }
1085
+ // 4. 종합 점수 계산 (가중 평균)
1086
+ // 사용 빈도(30%) + 최근성(20%) + 중요도(20%) + 의미적 거리(20%) + 앵커 비교(10%)
1087
+ const finalScore = usageScore * 0.3 +
1088
+ recencyScore * 0.2 +
1089
+ importanceScore * 0.2 +
1090
+ semanticScore * 0.2 +
1091
+ anchorComparisonScore * 0.1;
1092
+ return Math.min(1.0, Math.max(0.0, finalScore));
1093
+ }
1094
+ catch (error) {
1095
+ console.error(`❌ 앵커 이동 점수 계산 실패 (${memoryId}):`, error);
1096
+ return 0;
1097
+ }
1098
+ }
1099
+ /**
1100
+ * 앵커 주변 메모리 사용 패턴 분석
1101
+ * @param agentId - 에이전트 ID
1102
+ * @param slot - 슬롯
1103
+ * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1104
+ * @returns 더 적합한 앵커 후보 목록 (점수 내림차순)
1105
+ */
1106
+ async analyzeAnchorUsage(agentId, slot, queryEmbedding) {
1107
+ if (!this.db) {
1108
+ throw new Error('Database is not set.');
1109
+ }
1110
+ const anchor = await this.getAnchor(agentId, slot);
1111
+ if (!anchor || Array.isArray(anchor) || !anchor.memory_id) {
1112
+ return [];
1113
+ }
1114
+ try {
1115
+ // 현재 앵커 임베딩 조회
1116
+ const anchorEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
1117
+ if (!anchorEmbedding) {
1118
+ return [];
1119
+ }
1120
+ // 앵커 주변 메모리 검색 (1-hop)
1121
+ const slotConfig = this.getSlotConfig(slot);
1122
+ const nearbyMemories = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchor.memory_id, slotConfig.vector_threshold * 0.8, // 더 넓은 범위
1123
+ slotConfig.hop_limit, 20 // 더 많은 후보
1124
+ );
1125
+ // 각 메모리에 대해 앵커 이동 점수 계산
1126
+ const candidates = [];
1127
+ for (const memory of nearbyMemories) {
1128
+ const score = await this.calculateReanchorScore(memory.memory_id, queryEmbedding, anchorEmbedding.embedding);
1129
+ if (score > 0.5) { // 최소 점수 이상만 후보로
1130
+ const reason = this.generateReanchorReason(memory, score);
1131
+ candidates.push({
1132
+ memory_id: memory.memory_id,
1133
+ score,
1134
+ reason
1135
+ });
1136
+ }
1137
+ }
1138
+ // 점수 내림차순 정렬
1139
+ candidates.sort((a, b) => b.score - a.score);
1140
+ return candidates;
1141
+ }
1142
+ catch (error) {
1143
+ console.error(`❌ 앵커 사용 패턴 분석 실패 (${agentId}/${slot}):`, error);
1144
+ return [];
1145
+ }
1146
+ }
1147
+ /**
1148
+ * 앵커 이동 이유 생성
1149
+ */
1150
+ generateReanchorReason(memory, score) {
1151
+ const reasons = [];
1152
+ if (score > 0.7) {
1153
+ reasons.push('높은 사용 빈도');
1154
+ }
1155
+ if (memory.similarity && memory.similarity > 0.8) {
1156
+ reasons.push('쿼리와 높은 유사도');
1157
+ }
1158
+ if (memory.hop_distance === 1) {
1159
+ reasons.push('앵커와 직접 연결');
1160
+ }
1161
+ return reasons.length > 0 ? reasons.join(', ') : '종합 점수 우수';
1162
+ }
1163
+ /**
1164
+ * 자동 앵커 이동 실행
1165
+ * @param agentId - 에이전트 ID
1166
+ * @param slot - 슬롯
1167
+ * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1168
+ * @param threshold - 이동 임계값 (기본값: 0.7)
1169
+ * @param strategy - 이동 전략 ('gradual' | 'immediate', 기본값: 'gradual')
1170
+ * @returns 이동 결과 (이동 여부, 새 앵커 정보)
1171
+ */
1172
+ async autoReanchor(agentId, slot, queryEmbedding, threshold = 0.7, strategy = 'gradual') {
1173
+ if (!this.db) {
1174
+ throw new Error('Database is not set.');
1175
+ }
1176
+ try {
1177
+ // 현재 앵커 조회
1178
+ const currentAnchor = await this.getAnchor(agentId, slot);
1179
+ if (!currentAnchor || Array.isArray(currentAnchor) || !currentAnchor.memory_id) {
1180
+ return {
1181
+ moved: false,
1182
+ old_anchor: null,
1183
+ new_anchor: null,
1184
+ score: 0,
1185
+ reason: '앵커가 설정되지 않았습니다'
1186
+ };
1187
+ }
1188
+ // 더 적합한 앵커 후보 찾기
1189
+ const candidates = await this.analyzeAnchorUsage(agentId, slot, queryEmbedding);
1190
+ if (candidates.length === 0 || !candidates[0] || candidates[0].score < threshold) {
1191
+ return {
1192
+ moved: false,
1193
+ old_anchor: currentAnchor.memory_id,
1194
+ new_anchor: null,
1195
+ score: candidates[0]?.score || 0,
1196
+ reason: `임계값(${threshold}) 미만 또는 후보 없음`
1197
+ };
1198
+ }
1199
+ const bestCandidate = candidates[0];
1200
+ if (!bestCandidate) {
1201
+ return {
1202
+ moved: false,
1203
+ old_anchor: currentAnchor.memory_id,
1204
+ new_anchor: null,
1205
+ score: 0,
1206
+ reason: '후보 없음'
1207
+ };
1208
+ }
1209
+ // 이동 전략에 따라 처리
1210
+ if (strategy === 'gradual') {
1211
+ // 점진적 이동: 기존 앵커를 B나 C로 이동하고 새로운 메모리를 A에 설정
1212
+ if (slot === 'A') {
1213
+ // A -> B로 이동
1214
+ const bAnchor = await this.getAnchor(agentId, 'B');
1215
+ if (bAnchor && !Array.isArray(bAnchor) && bAnchor.memory_id) {
1216
+ // B -> C로 이동 (인자 순서: agentId, memoryId, slot)
1217
+ await this.setAnchor(agentId, bAnchor.memory_id, 'C');
1218
+ }
1219
+ // 현재 A -> B로 이동
1220
+ await this.setAnchor(agentId, currentAnchor.memory_id, 'B');
1221
+ }
1222
+ else if (slot === 'B') {
1223
+ // B -> C로 이동
1224
+ await this.setAnchor(agentId, currentAnchor.memory_id, 'C');
1225
+ }
1226
+ // 새로운 메모리를 현재 슬롯에 설정
1227
+ await this.setAnchor(agentId, bestCandidate.memory_id, slot);
1228
+ }
1229
+ else {
1230
+ // 급격한 이동: 현재 앵커를 완전히 교체
1231
+ await this.setAnchor(agentId, bestCandidate.memory_id, slot);
1232
+ }
1233
+ console.log(`🔄 자동 앵커 이동 완료: ${agentId}/${slot} ` +
1234
+ `${currentAnchor.memory_id} -> ${bestCandidate.memory_id} ` +
1235
+ `(점수: ${bestCandidate.score.toFixed(3)}, 이유: ${bestCandidate.reason})`);
1236
+ return {
1237
+ moved: true,
1238
+ old_anchor: currentAnchor.memory_id,
1239
+ new_anchor: bestCandidate.memory_id,
1240
+ score: bestCandidate.score,
1241
+ reason: bestCandidate.reason
1242
+ };
1243
+ }
1244
+ catch (error) {
1245
+ console.error(`❌ 자동 앵커 이동 실패 (${agentId}/${slot}):`, error);
1246
+ throw error;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * 검색 후 자동 앵커 이동 체크 (선택적)
1251
+ * @param agentId - 에이전트 ID
1252
+ * @param slot - 슬롯
1253
+ * @param queryEmbedding - 검색 쿼리 임베딩
1254
+ * @param autoMoveEnabled - 자동 이동 활성화 여부 (기본값: false)
1255
+ * @returns 이동 결과
1256
+ */
1257
+ async checkAndAutoReanchor(agentId, slot, queryEmbedding, autoMoveEnabled = false) {
1258
+ if (!autoMoveEnabled) {
1259
+ return null;
1260
+ }
1261
+ try {
1262
+ return await this.autoReanchor(agentId, slot, queryEmbedding, 0.7, 'gradual');
1263
+ }
1264
+ catch (error) {
1265
+ console.error(`❌ 자동 앵커 이동 체크 실패:`, error);
1266
+ return null;
1267
+ }
1268
+ }
1269
+ }
1270
+ //# sourceMappingURL=anchor-manager.js.map