memento-mcp-server 1.11.0-a1 → 1.11.1-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.
- package/README.md +7 -0
- package/dist/server/context.d.ts +36 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +45 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/handlers/anchor-map.handler.d.ts +60 -0
- package/dist/server/handlers/anchor-map.handler.d.ts.map +1 -0
- package/dist/server/handlers/anchor-map.handler.js +190 -0
- package/dist/server/handlers/anchor-map.handler.js.map +1 -0
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +41 -1131
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/middleware/error-handler.middleware.d.ts +25 -0
- package/dist/server/middleware/error-handler.middleware.d.ts.map +1 -0
- package/dist/server/middleware/error-handler.middleware.js +97 -0
- package/dist/server/middleware/error-handler.middleware.js.map +1 -0
- package/dist/server/middleware/index.d.ts +8 -0
- package/dist/server/middleware/index.d.ts.map +1 -0
- package/dist/server/middleware/index.js +8 -0
- package/dist/server/middleware/index.js.map +1 -0
- package/dist/server/middleware/service-injector.middleware.d.ts +30 -0
- package/dist/server/middleware/service-injector.middleware.d.ts.map +1 -0
- package/dist/server/middleware/service-injector.middleware.js +20 -0
- package/dist/server/middleware/service-injector.middleware.js.map +1 -0
- package/dist/server/middleware/tool-context.middleware.d.ts +29 -0
- package/dist/server/middleware/tool-context.middleware.d.ts.map +1 -0
- package/dist/server/middleware/tool-context.middleware.js +34 -0
- package/dist/server/middleware/tool-context.middleware.js.map +1 -0
- package/dist/server/routes/admin.routes.d.ts +12 -0
- package/dist/server/routes/admin.routes.d.ts.map +1 -0
- package/dist/server/routes/admin.routes.js +338 -0
- package/dist/server/routes/admin.routes.js.map +1 -0
- package/dist/server/routes/api.routes.d.ts +13 -0
- package/dist/server/routes/api.routes.d.ts.map +1 -0
- package/dist/server/routes/api.routes.js +122 -0
- package/dist/server/routes/api.routes.js.map +1 -0
- package/dist/server/routes/mcp.routes.d.ts +22 -0
- package/dist/server/routes/mcp.routes.d.ts.map +1 -0
- package/dist/server/routes/mcp.routes.js +383 -0
- package/dist/server/routes/mcp.routes.js.map +1 -0
- package/dist/server/routes/tools.routes.d.ts +13 -0
- package/dist/server/routes/tools.routes.d.ts.map +1 -0
- package/dist/server/routes/tools.routes.js +99 -0
- package/dist/server/routes/tools.routes.js.map +1 -0
- package/dist/services/anchor/anchor-cache-service.d.ts +77 -0
- package/dist/services/anchor/anchor-cache-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-cache-service.js +193 -0
- package/dist/services/anchor/anchor-cache-service.js.map +1 -0
- package/dist/services/anchor/anchor-interfaces.d.ts +143 -0
- package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -0
- package/dist/services/anchor/anchor-interfaces.js +24 -0
- package/dist/services/anchor/anchor-interfaces.js.map +1 -0
- package/dist/services/anchor/anchor-manager.d.ts +71 -0
- package/dist/services/anchor/anchor-manager.d.ts.map +1 -0
- package/dist/services/anchor/anchor-manager.js +205 -0
- package/dist/services/anchor/anchor-manager.js.map +1 -0
- package/dist/services/anchor/anchor-search-service.d.ts +115 -0
- package/dist/services/anchor/anchor-search-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-search-service.js +799 -0
- package/dist/services/anchor/anchor-search-service.js.map +1 -0
- package/dist/services/anchor/index.d.ts +11 -0
- package/dist/services/anchor/index.d.ts.map +1 -0
- package/dist/services/anchor/index.js +10 -0
- package/dist/services/anchor/index.js.map +1 -0
- package/dist/services/anchor-manager.d.ts +22 -208
- package/dist/services/anchor-manager.d.ts.map +1 -1
- package/dist/services/anchor-manager.js +72 -1088
- package/dist/services/anchor-manager.js.map +1 -1
- package/dist/services/error-logging-service.d.ts +1 -0
- package/dist/services/error-logging-service.d.ts.map +1 -1
- package/dist/services/error-logging-service.js +2 -0
- package/dist/services/error-logging-service.js.map +1 -1
- package/dist/tools/forget-tool.js +1 -1
- package/dist/tools/forget-tool.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor Search Service
|
|
3
|
+
* 검색 관련 로직 담당
|
|
4
|
+
* Phase 1.1: anchor-manager.ts 리팩토링
|
|
5
|
+
*/
|
|
6
|
+
import { UnifiedEmbeddingService } from '../unified-embedding-service.js';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Anchor Search Service 구현
|
|
10
|
+
*/
|
|
11
|
+
export class AnchorSearchService {
|
|
12
|
+
db = null;
|
|
13
|
+
cacheService;
|
|
14
|
+
hybridSearchEngine = null;
|
|
15
|
+
vectorSearchEngine = null;
|
|
16
|
+
queryEmbeddingService = new UnifiedEmbeddingService();
|
|
17
|
+
/**
|
|
18
|
+
* 생성자
|
|
19
|
+
*/
|
|
20
|
+
constructor(cacheService) {
|
|
21
|
+
this.cacheService = cacheService;
|
|
22
|
+
logger.info('AnchorSearchService 초기화 완료');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 데이터베이스 설정
|
|
26
|
+
*/
|
|
27
|
+
setDatabase(db) {
|
|
28
|
+
if (!db) {
|
|
29
|
+
throw new Error('Database instance is required');
|
|
30
|
+
}
|
|
31
|
+
this.db = db;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 하이브리드 검색 엔진 설정
|
|
35
|
+
*/
|
|
36
|
+
setHybridSearchEngine(hybridSearchEngine) {
|
|
37
|
+
if (!hybridSearchEngine) {
|
|
38
|
+
throw new Error('HybridSearchEngine is required');
|
|
39
|
+
}
|
|
40
|
+
this.hybridSearchEngine = hybridSearchEngine;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 벡터 검색 엔진 설정
|
|
44
|
+
*/
|
|
45
|
+
setVectorSearchEngine(vectorSearchEngine) {
|
|
46
|
+
if (!vectorSearchEngine) {
|
|
47
|
+
throw new Error('VectorSearchEngine is required');
|
|
48
|
+
}
|
|
49
|
+
this.vectorSearchEngine = vectorSearchEngine;
|
|
50
|
+
// 데이터베이스가 이미 설정되어 있으면 초기화
|
|
51
|
+
if (this.db) {
|
|
52
|
+
this.vectorSearchEngine.initialize(this.db);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 국소 검색
|
|
57
|
+
* 앵커 메모리를 기준으로 N-hop 제한 검색 수행
|
|
58
|
+
*/
|
|
59
|
+
async searchLocal(agentId, slot, query, hopLimit, options, anchorMemoryId, anchorEmbedding, startTime) {
|
|
60
|
+
if (!this.db) {
|
|
61
|
+
throw new Error('Database is not set. Call setDatabase() first.');
|
|
62
|
+
}
|
|
63
|
+
// 슬롯별 설정 가져오기
|
|
64
|
+
const slotConfig = this.getSlotConfig(slot);
|
|
65
|
+
const finalHopLimit = hopLimit ?? slotConfig.hop_limit;
|
|
66
|
+
const vectorThreshold = slotConfig.vector_threshold;
|
|
67
|
+
// 검색 옵션 기본값
|
|
68
|
+
const limit = options?.limit ?? 10;
|
|
69
|
+
const minResults = options?.min_results ?? 3;
|
|
70
|
+
// VectorSearchEngine이 없으면 에러
|
|
71
|
+
if (!this.vectorSearchEngine) {
|
|
72
|
+
throw new Error('VectorSearchEngine is not set. Call setVectorSearchEngine() first.');
|
|
73
|
+
}
|
|
74
|
+
// N-hop 검색 구현
|
|
75
|
+
const allHopResults = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchorMemoryId, vectorThreshold, finalHopLimit, limit * 2 // 더 많이 가져와서 필터링 후 최종 limit 적용
|
|
76
|
+
);
|
|
77
|
+
// 쿼리가 있는 경우 쿼리 기반 필터링
|
|
78
|
+
let filteredResults = allHopResults;
|
|
79
|
+
let queryEmbeddingForReanchor;
|
|
80
|
+
if (query && query.trim().length > 0) {
|
|
81
|
+
filteredResults = await this.filterByQuery(query, allHopResults, anchorEmbedding.provider);
|
|
82
|
+
// 자동 앵커 이동을 위한 쿼리 임베딩 생성 (선택적, 비동기)
|
|
83
|
+
try {
|
|
84
|
+
const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
|
|
85
|
+
if (queryEmbeddingResult && queryEmbeddingResult.embedding) {
|
|
86
|
+
queryEmbeddingForReanchor = queryEmbeddingResult.embedding;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// 쿼리 임베딩 생성 실패는 무시 (자동 이동은 선택적)
|
|
91
|
+
logger.debug('Query embedding generation failed (for auto anchor move)', {
|
|
92
|
+
error: error instanceof Error ? error.message : String(error)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 결과 포맷팅
|
|
97
|
+
const formattedResults = filteredResults.map(result => ({
|
|
98
|
+
id: result.memory_id,
|
|
99
|
+
content: result.content,
|
|
100
|
+
type: result.type,
|
|
101
|
+
similarity: result.similarity,
|
|
102
|
+
hop_distance: result.hop_distance,
|
|
103
|
+
importance: result.importance,
|
|
104
|
+
created_at: result.created_at,
|
|
105
|
+
tags: result.tags
|
|
106
|
+
}));
|
|
107
|
+
// 최종 limit 적용
|
|
108
|
+
const localResults = formattedResults.slice(0, limit);
|
|
109
|
+
const localCount = localResults.length;
|
|
110
|
+
// Fallback 체크 (query가 있을 때만, min_results 미만 시)
|
|
111
|
+
let fallbackUsed = false;
|
|
112
|
+
let finalResults = localResults;
|
|
113
|
+
let totalCount = localCount;
|
|
114
|
+
if (query && query.trim().length > 0 && localCount < minResults) {
|
|
115
|
+
try {
|
|
116
|
+
logger.info('Fallback to global search', {
|
|
117
|
+
localCount,
|
|
118
|
+
minResults
|
|
119
|
+
});
|
|
120
|
+
// Fallback 수행
|
|
121
|
+
const fallbackResult = await this.fallbackToGlobalSearch(query, { ...options, limit: limit - localCount }, // 부족한 만큼만 가져오기
|
|
122
|
+
startTime);
|
|
123
|
+
fallbackUsed = true;
|
|
124
|
+
// Local 결과와 Fallback 결과 병합
|
|
125
|
+
// Local 결과를 우선하고, 중복 제거 (memory_id 기준)
|
|
126
|
+
const localMemoryIds = new Set(localResults.map(r => r.id));
|
|
127
|
+
const fallbackItems = fallbackResult.items
|
|
128
|
+
.filter(item => !localMemoryIds.has(item.id))
|
|
129
|
+
.map(item => ({
|
|
130
|
+
id: item.id,
|
|
131
|
+
content: item.content,
|
|
132
|
+
type: item.type,
|
|
133
|
+
similarity: item.similarity ?? 0,
|
|
134
|
+
hop_distance: item.hop_distance ?? 999, // fallback 결과는 hop_distance가 없으므로 큰 값으로 설정
|
|
135
|
+
importance: item.importance ?? 0.5,
|
|
136
|
+
created_at: item.created_at ?? new Date().toISOString(),
|
|
137
|
+
tags: item.tags ?? undefined
|
|
138
|
+
}));
|
|
139
|
+
// Local 결과 + Fallback 결과 (중복 제거된 것만)
|
|
140
|
+
finalResults = [...localResults, ...fallbackItems].slice(0, limit);
|
|
141
|
+
totalCount = finalResults.length;
|
|
142
|
+
logger.info('Fallback completed', {
|
|
143
|
+
localCount,
|
|
144
|
+
fallbackCount: fallbackItems.length,
|
|
145
|
+
totalCount
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
logger.error('Fallback failed', {
|
|
150
|
+
error: error instanceof Error ? error.message : String(error)
|
|
151
|
+
});
|
|
152
|
+
// Fallback 실패 시 local 결과만 반환
|
|
153
|
+
fallbackUsed = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const queryTime = Date.now() - startTime;
|
|
157
|
+
return {
|
|
158
|
+
items: finalResults,
|
|
159
|
+
total_count: totalCount,
|
|
160
|
+
local_results_count: localCount,
|
|
161
|
+
fallback_used: fallbackUsed,
|
|
162
|
+
query_time: queryTime,
|
|
163
|
+
anchor_info: {
|
|
164
|
+
agent_id: agentId,
|
|
165
|
+
slot: slot,
|
|
166
|
+
memory_id: anchorMemoryId
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 1-hop 검색: 앵커와 직접적으로 유사한 메모리 검색
|
|
172
|
+
*/
|
|
173
|
+
async searchOneHop(anchorEmbedding, provider, anchorMemoryId, threshold, limit) {
|
|
174
|
+
if (!this.vectorSearchEngine || !this.db) {
|
|
175
|
+
throw new Error('VectorSearchEngine or Database is not set.');
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
// VectorSearchEngine 초기화 확인
|
|
179
|
+
if (typeof this.vectorSearchEngine.initialize === 'function') {
|
|
180
|
+
this.vectorSearchEngine.initialize(this.db);
|
|
181
|
+
}
|
|
182
|
+
// 벡터 검색 실행 (임계값은 낮게 설정하고 나중에 필터링)
|
|
183
|
+
const searchResults = await this.vectorSearchEngine.search(anchorEmbedding, {
|
|
184
|
+
limit: limit + 1, // 자기 자신 제외를 위해 +1
|
|
185
|
+
threshold: 0.0, // 임계값은 나중에 필터링에서 적용
|
|
186
|
+
includeContent: true,
|
|
187
|
+
includeMetadata: true
|
|
188
|
+
}, provider);
|
|
189
|
+
// 결과 필터링: 앵커 메모리 제외, 유사도 임계값 이상만 반환
|
|
190
|
+
const filteredResults = searchResults
|
|
191
|
+
.filter(result => {
|
|
192
|
+
// 앵커 메모리 제외
|
|
193
|
+
if (result.memory_id === anchorMemoryId) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
// 유사도 임계값 이상만 반환
|
|
197
|
+
return result.similarity >= threshold;
|
|
198
|
+
})
|
|
199
|
+
.slice(0, limit); // 최종 limit 적용
|
|
200
|
+
return filteredResults;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logger.error('1-hop search failed', {
|
|
204
|
+
error: error instanceof Error ? error.message : String(error)
|
|
205
|
+
});
|
|
206
|
+
throw new Error(`Failed to perform 1-hop search: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* N-hop 검색: 앵커를 기준으로 최대 N-hop까지 확장 검색
|
|
211
|
+
*/
|
|
212
|
+
async searchNHop(anchorEmbedding, provider, anchorMemoryId, threshold, maxHops, limit) {
|
|
213
|
+
if (!this.vectorSearchEngine || !this.db) {
|
|
214
|
+
throw new Error('VectorSearchEngine or Database is not set.');
|
|
215
|
+
}
|
|
216
|
+
// VectorSearchEngine 초기화 확인
|
|
217
|
+
if (typeof this.vectorSearchEngine.initialize === 'function') {
|
|
218
|
+
this.vectorSearchEngine.initialize(this.db);
|
|
219
|
+
}
|
|
220
|
+
// 이미 발견된 메모리 ID 추적 (중복 방지)
|
|
221
|
+
const discoveredMemoryIds = new Set([anchorMemoryId]);
|
|
222
|
+
// 각 hop 레벨의 결과를 저장
|
|
223
|
+
const allResults = [];
|
|
224
|
+
// 현재 hop 레벨의 메모리들 (임베딩 포함)
|
|
225
|
+
// 1-hop: 앵커 임베딩을 사용
|
|
226
|
+
let currentHopMemories = [
|
|
227
|
+
{ memory_id: anchorMemoryId, embedding: anchorEmbedding }
|
|
228
|
+
];
|
|
229
|
+
// 각 hop 레벨별로 검색 수행
|
|
230
|
+
for (let hop = 1; hop <= maxHops; hop++) {
|
|
231
|
+
const nextHopMemories = [];
|
|
232
|
+
const hopResults = [];
|
|
233
|
+
// 현재 hop의 각 메모리에 대해 검색 수행
|
|
234
|
+
for (const currentMemory of currentHopMemories) {
|
|
235
|
+
try {
|
|
236
|
+
// memory_link를 활용한 직접 연결된 메모리 조회 (최적화)
|
|
237
|
+
const linkedMemories = await this.getLinkedMemories(currentMemory.memory_id);
|
|
238
|
+
// 벡터 검색 실행
|
|
239
|
+
const vectorSearchResults = await this.vectorSearchEngine.search(currentMemory.embedding, {
|
|
240
|
+
limit: Math.ceil(limit / maxHops) + 10, // 각 hop당 충분한 결과 가져오기
|
|
241
|
+
threshold: 0.0, // 임계값은 나중에 필터링에서 적용
|
|
242
|
+
includeContent: true,
|
|
243
|
+
includeMetadata: true
|
|
244
|
+
}, provider);
|
|
245
|
+
// memory_link 결과와 벡터 검색 결과를 병합
|
|
246
|
+
const allCandidates = new Map();
|
|
247
|
+
// memory_link 결과 추가 (우선순위 높음)
|
|
248
|
+
for (const linked of linkedMemories) {
|
|
249
|
+
if (!discoveredMemoryIds.has(linked.memory_id)) {
|
|
250
|
+
allCandidates.set(linked.memory_id, {
|
|
251
|
+
...linked,
|
|
252
|
+
isLinked: true
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// 벡터 검색 결과 추가
|
|
257
|
+
const relaxedThreshold = threshold * 0.5;
|
|
258
|
+
for (const result of vectorSearchResults) {
|
|
259
|
+
if (!allCandidates.has(result.memory_id) && !discoveredMemoryIds.has(result.memory_id)) {
|
|
260
|
+
if (result.similarity >= relaxedThreshold) {
|
|
261
|
+
allCandidates.set(result.memory_id, {
|
|
262
|
+
...result,
|
|
263
|
+
isLinked: false
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (allCandidates.has(result.memory_id)) {
|
|
268
|
+
// memory_link로 이미 추가된 경우, 유사도 정보 업데이트
|
|
269
|
+
const existing = allCandidates.get(result.memory_id);
|
|
270
|
+
existing.similarity = Math.max(existing.similarity, result.similarity);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// 결과 필터링 및 추가
|
|
274
|
+
for (const [memoryId, candidate] of allCandidates.entries()) {
|
|
275
|
+
if (discoveredMemoryIds.has(memoryId)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const effectiveThreshold = candidate.isLinked
|
|
279
|
+
? threshold * 0.8
|
|
280
|
+
: threshold;
|
|
281
|
+
if (candidate.similarity < effectiveThreshold) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
discoveredMemoryIds.add(memoryId);
|
|
285
|
+
hopResults.push({
|
|
286
|
+
memory_id: candidate.memory_id,
|
|
287
|
+
content: candidate.content,
|
|
288
|
+
type: candidate.type,
|
|
289
|
+
similarity: candidate.similarity,
|
|
290
|
+
importance: candidate.importance,
|
|
291
|
+
created_at: candidate.created_at,
|
|
292
|
+
tags: candidate.tags
|
|
293
|
+
});
|
|
294
|
+
// 다음 hop을 위한 임베딩 조회
|
|
295
|
+
if (hop < maxHops) {
|
|
296
|
+
try {
|
|
297
|
+
const nextEmbedding = await this.cacheService.getAnchorEmbedding(candidate.memory_id);
|
|
298
|
+
if (nextEmbedding && nextEmbedding.embedding) {
|
|
299
|
+
nextHopMemories.push({
|
|
300
|
+
memory_id: candidate.memory_id,
|
|
301
|
+
embedding: nextEmbedding.embedding
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
// 임베딩 조회 실패 시 다음 hop에서 제외
|
|
307
|
+
logger.debug('Skipping memory for next hop (no embedding)', {
|
|
308
|
+
memoryId: candidate.memory_id,
|
|
309
|
+
error: error instanceof Error ? error.message : String(error)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
logger.error('Hop search failed', {
|
|
317
|
+
hop,
|
|
318
|
+
memoryId: currentMemory.memory_id,
|
|
319
|
+
error: error instanceof Error ? error.message : String(error)
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// 현재 hop의 결과를 전체 결과에 추가
|
|
325
|
+
for (const result of hopResults) {
|
|
326
|
+
allResults.push({
|
|
327
|
+
...result,
|
|
328
|
+
hop_distance: hop
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// limit에 도달했으면 중단
|
|
332
|
+
if (allResults.length >= limit) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
// 다음 hop을 위한 메모리가 없으면 중단
|
|
336
|
+
if (nextHopMemories.length === 0) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
// 다음 hop을 위한 메모리로 업데이트
|
|
340
|
+
currentHopMemories = nextHopMemories;
|
|
341
|
+
}
|
|
342
|
+
// 랭킹 점수 계산 및 적용
|
|
343
|
+
const rankedResults = allResults.map(result => {
|
|
344
|
+
const rankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
|
|
345
|
+
return {
|
|
346
|
+
...result,
|
|
347
|
+
similarity: rankingScore
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
// 랭킹 점수 기준으로 정렬
|
|
351
|
+
rankedResults.sort((a, b) => {
|
|
352
|
+
if (Math.abs(a.similarity - b.similarity) < 0.001) {
|
|
353
|
+
return a.hop_distance - b.hop_distance;
|
|
354
|
+
}
|
|
355
|
+
return b.similarity - a.similarity;
|
|
356
|
+
});
|
|
357
|
+
// 최종 limit 적용
|
|
358
|
+
return rankedResults.slice(0, limit);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 쿼리 기반 필터링
|
|
362
|
+
*/
|
|
363
|
+
async filterByQuery(query, results, provider) {
|
|
364
|
+
if (results.length === 0) {
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
// 쿼리 임베딩 생성
|
|
369
|
+
const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
|
|
370
|
+
if (!queryEmbeddingResult || !queryEmbeddingResult.embedding) {
|
|
371
|
+
logger.warn('Query embedding generation failed, skipping filter');
|
|
372
|
+
return results;
|
|
373
|
+
}
|
|
374
|
+
const queryEmbedding = queryEmbeddingResult.embedding;
|
|
375
|
+
// 각 결과 메모리의 임베딩 조회 및 쿼리 유사도 계산
|
|
376
|
+
const resultsWithQuerySimilarity = await Promise.all(results.map(async (result) => {
|
|
377
|
+
try {
|
|
378
|
+
const memoryEmbedding = await this.cacheService.getAnchorEmbedding(result.memory_id);
|
|
379
|
+
if (!memoryEmbedding || !memoryEmbedding.embedding) {
|
|
380
|
+
return {
|
|
381
|
+
...result,
|
|
382
|
+
query_similarity: 0,
|
|
383
|
+
combined_similarity: result.similarity * 0.5
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// 쿼리 임베딩과 메모리 임베딩 간 유사도 계산
|
|
387
|
+
let querySim = 0;
|
|
388
|
+
if (queryEmbedding.length === memoryEmbedding.embedding.length) {
|
|
389
|
+
querySim = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// 차원이 다르면 텍스트 기반 간단한 매칭
|
|
393
|
+
const queryLower = query.toLowerCase();
|
|
394
|
+
const contentLower = result.content.toLowerCase();
|
|
395
|
+
const queryWords = queryLower.split(/\s+/);
|
|
396
|
+
const matchCount = queryWords.filter(word => contentLower.includes(word)).length;
|
|
397
|
+
querySim = matchCount / Math.max(queryWords.length, 1);
|
|
398
|
+
}
|
|
399
|
+
const baseRankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
|
|
400
|
+
const combinedSimilarity = baseRankingScore * 0.6 + querySim * 0.4;
|
|
401
|
+
return {
|
|
402
|
+
...result,
|
|
403
|
+
query_similarity: querySim,
|
|
404
|
+
combined_similarity: combinedSimilarity
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
logger.error('Query filtering failed for memory', {
|
|
409
|
+
memoryId: result.memory_id,
|
|
410
|
+
error: error instanceof Error ? error.message : String(error)
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
...result,
|
|
414
|
+
query_similarity: 0,
|
|
415
|
+
combined_similarity: result.similarity * 0.5
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
// 쿼리 유사도 임계값 적용
|
|
420
|
+
const queryThreshold = 0.3;
|
|
421
|
+
const filtered = resultsWithQuerySimilarity.filter(r => r.query_similarity >= queryThreshold || r.combined_similarity >= 0.5);
|
|
422
|
+
// 결합 유사도 기준으로 재정렬
|
|
423
|
+
filtered.sort((a, b) => {
|
|
424
|
+
if (Math.abs(a.combined_similarity - b.combined_similarity) < 0.001) {
|
|
425
|
+
return a.hop_distance - b.hop_distance;
|
|
426
|
+
}
|
|
427
|
+
return b.combined_similarity - a.combined_similarity;
|
|
428
|
+
});
|
|
429
|
+
// 원본 similarity를 combined_similarity로 업데이트하여 반환
|
|
430
|
+
return filtered.map(r => ({
|
|
431
|
+
...r,
|
|
432
|
+
similarity: r.combined_similarity
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
logger.error('Query-based filtering failed', {
|
|
437
|
+
error: error instanceof Error ? error.message : String(error)
|
|
438
|
+
});
|
|
439
|
+
return results;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 전역 검색으로 Fallback
|
|
444
|
+
*/
|
|
445
|
+
async fallbackToGlobalSearch(query, options, startTime) {
|
|
446
|
+
if (!this.hybridSearchEngine) {
|
|
447
|
+
throw new Error('HybridSearchEngine is not set. Call setHybridSearchEngine() first.');
|
|
448
|
+
}
|
|
449
|
+
if (!this.db) {
|
|
450
|
+
throw new Error('Database is not set.');
|
|
451
|
+
}
|
|
452
|
+
const limit = options?.limit ?? 10;
|
|
453
|
+
const fallbackStartTime = Date.now();
|
|
454
|
+
try {
|
|
455
|
+
// HybridSearchEngine을 사용한 전역 검색
|
|
456
|
+
const globalSearchResult = await this.hybridSearchEngine.search(this.db, {
|
|
457
|
+
query: query,
|
|
458
|
+
limit: limit,
|
|
459
|
+
vectorWeight: options?.vector_weight,
|
|
460
|
+
textWeight: options?.text_weight
|
|
461
|
+
});
|
|
462
|
+
// HybridSearchResult를 SearchResult 형식으로 변환
|
|
463
|
+
const convertedItems = globalSearchResult.items.map(item => ({
|
|
464
|
+
id: item.id,
|
|
465
|
+
content: item.content,
|
|
466
|
+
type: item.type,
|
|
467
|
+
similarity: item.finalScore,
|
|
468
|
+
importance: item.importance,
|
|
469
|
+
created_at: item.created_at,
|
|
470
|
+
tags: item.tags,
|
|
471
|
+
hop_distance: undefined
|
|
472
|
+
}));
|
|
473
|
+
const queryTime = startTime ? Date.now() - startTime : Date.now() - fallbackStartTime;
|
|
474
|
+
return {
|
|
475
|
+
items: convertedItems,
|
|
476
|
+
total_count: convertedItems.length,
|
|
477
|
+
local_results_count: 0,
|
|
478
|
+
fallback_used: true,
|
|
479
|
+
query_time: queryTime
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
logger.error('Global search fallback failed', {
|
|
484
|
+
error: error instanceof Error ? error.message : String(error)
|
|
485
|
+
});
|
|
486
|
+
const queryTime = startTime ? Date.now() - startTime : 0;
|
|
487
|
+
return {
|
|
488
|
+
items: [],
|
|
489
|
+
total_count: 0,
|
|
490
|
+
local_results_count: 0,
|
|
491
|
+
fallback_used: true,
|
|
492
|
+
query_time: queryTime
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* memory_link 테이블을 활용한 직접 연결된 메모리 조회
|
|
498
|
+
*/
|
|
499
|
+
async getLinkedMemories(memoryId) {
|
|
500
|
+
if (!this.db) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const linkedRecords = this.db.prepare(`
|
|
505
|
+
SELECT
|
|
506
|
+
ml.target_id as memory_id,
|
|
507
|
+
mi.content,
|
|
508
|
+
mi.type,
|
|
509
|
+
mi.importance,
|
|
510
|
+
mi.created_at,
|
|
511
|
+
mi.tags,
|
|
512
|
+
ml.relation_type
|
|
513
|
+
FROM memory_link ml
|
|
514
|
+
JOIN memory_item mi ON mi.id = ml.target_id
|
|
515
|
+
WHERE ml.source_id = ?
|
|
516
|
+
ORDER BY ml.created_at DESC
|
|
517
|
+
`).all(memoryId);
|
|
518
|
+
return linkedRecords.map(record => ({
|
|
519
|
+
memory_id: record.memory_id,
|
|
520
|
+
content: record.content,
|
|
521
|
+
type: record.type,
|
|
522
|
+
similarity: 0.9,
|
|
523
|
+
importance: record.importance,
|
|
524
|
+
created_at: record.created_at,
|
|
525
|
+
tags: record.tags ? (typeof record.tags === 'string' ? JSON.parse(record.tags) : record.tags) : undefined
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
logger.error('memory_link retrieval failed', {
|
|
530
|
+
memoryId,
|
|
531
|
+
error: error instanceof Error ? error.message : String(error)
|
|
532
|
+
});
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* 검색 결과 랭킹 점수 계산
|
|
538
|
+
*/
|
|
539
|
+
calculateRankingScore(similarity, hopDistance, importance = 0.5) {
|
|
540
|
+
const hopDecayFactor = 1.0 / (1.0 + (hopDistance - 1) * 0.3);
|
|
541
|
+
const anchorProximityBoost = hopDistance === 1 ? 1.2 : 1.0;
|
|
542
|
+
const importanceWeight = 0.1;
|
|
543
|
+
const importanceBoost = 1.0 + (importance - 0.5) * importanceWeight;
|
|
544
|
+
const rankingScore = Math.min(1.0, similarity * hopDecayFactor * anchorProximityBoost * importanceBoost);
|
|
545
|
+
return rankingScore;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* 코사인 유사도 계산
|
|
549
|
+
*/
|
|
550
|
+
cosineSimilarity(a, b) {
|
|
551
|
+
if (a.length !== b.length) {
|
|
552
|
+
throw new Error('벡터 차원이 일치하지 않습니다');
|
|
553
|
+
}
|
|
554
|
+
let dotProduct = 0;
|
|
555
|
+
let normA = 0;
|
|
556
|
+
let normB = 0;
|
|
557
|
+
for (let i = 0; i < a.length; i++) {
|
|
558
|
+
const aVal = a[i] ?? 0;
|
|
559
|
+
const bVal = b[i] ?? 0;
|
|
560
|
+
dotProduct += aVal * bVal;
|
|
561
|
+
normA += aVal * aVal;
|
|
562
|
+
normB += bVal * bVal;
|
|
563
|
+
}
|
|
564
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
565
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* 슬롯별 설정 조회
|
|
569
|
+
*/
|
|
570
|
+
getSlotConfig(slot) {
|
|
571
|
+
const slotConfig = {
|
|
572
|
+
A: { hop_limit: 1, vector_threshold: 0.8 },
|
|
573
|
+
B: { hop_limit: 2, vector_threshold: 0.6 },
|
|
574
|
+
C: { hop_limit: 3, vector_threshold: 0.4 }
|
|
575
|
+
};
|
|
576
|
+
return slotConfig[slot];
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* 자동 앵커 이동 점수 계산
|
|
580
|
+
*/
|
|
581
|
+
async calculateReanchorScore(memoryId, queryEmbedding, anchorEmbedding) {
|
|
582
|
+
if (!this.db) {
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const memory = this.db.prepare(`
|
|
587
|
+
SELECT
|
|
588
|
+
view_count,
|
|
589
|
+
cite_count,
|
|
590
|
+
edit_count,
|
|
591
|
+
last_accessed,
|
|
592
|
+
created_at,
|
|
593
|
+
importance
|
|
594
|
+
FROM memory_item
|
|
595
|
+
WHERE id = ?
|
|
596
|
+
`).get(memoryId);
|
|
597
|
+
if (!memory) {
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
const usageScore = Math.min(1.0, (Math.log(1 + memory.view_count) +
|
|
601
|
+
2 * Math.log(1 + memory.cite_count) +
|
|
602
|
+
0.5 * Math.log(1 + memory.edit_count)) / 10);
|
|
603
|
+
let recencyScore = 0.5;
|
|
604
|
+
if (memory.last_accessed) {
|
|
605
|
+
const lastAccessed = new Date(memory.last_accessed);
|
|
606
|
+
const now = new Date();
|
|
607
|
+
const daysSinceAccess = (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
|
|
608
|
+
recencyScore = Math.max(0, 1.0 - daysSinceAccess / 30);
|
|
609
|
+
}
|
|
610
|
+
const importanceScore = memory.importance || 0.5;
|
|
611
|
+
let semanticScore = 0.5;
|
|
612
|
+
if (queryEmbedding) {
|
|
613
|
+
const memoryEmbedding = await this.cacheService.getAnchorEmbedding(memoryId);
|
|
614
|
+
if (memoryEmbedding && memoryEmbedding.embedding) {
|
|
615
|
+
const similarity = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
|
|
616
|
+
semanticScore = similarity;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
let anchorComparisonScore = 0.5;
|
|
620
|
+
if (anchorEmbedding) {
|
|
621
|
+
const memoryEmbedding = await this.cacheService.getAnchorEmbedding(memoryId);
|
|
622
|
+
if (memoryEmbedding && memoryEmbedding.embedding) {
|
|
623
|
+
const similarity = this.cosineSimilarity(anchorEmbedding, memoryEmbedding.embedding);
|
|
624
|
+
anchorComparisonScore = 1.0 - similarity;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const finalScore = usageScore * 0.3 +
|
|
628
|
+
recencyScore * 0.2 +
|
|
629
|
+
importanceScore * 0.2 +
|
|
630
|
+
semanticScore * 0.2 +
|
|
631
|
+
anchorComparisonScore * 0.1;
|
|
632
|
+
return Math.min(1.0, Math.max(0.0, finalScore));
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
logger.error('Reanchor score calculation failed', {
|
|
636
|
+
memoryId,
|
|
637
|
+
error: error instanceof Error ? error.message : String(error)
|
|
638
|
+
});
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* 앵커 주변 메모리 사용 패턴 분석
|
|
644
|
+
*/
|
|
645
|
+
async analyzeAnchorUsage(agentId, slot, anchorMemoryId, anchorEmbedding, queryEmbedding) {
|
|
646
|
+
if (!this.db) {
|
|
647
|
+
throw new Error('Database is not set.');
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const slotConfig = this.getSlotConfig(slot);
|
|
651
|
+
const nearbyMemories = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchorMemoryId, slotConfig.vector_threshold * 0.8, slotConfig.hop_limit, 20);
|
|
652
|
+
const candidates = [];
|
|
653
|
+
for (const memory of nearbyMemories) {
|
|
654
|
+
const score = await this.calculateReanchorScore(memory.memory_id, queryEmbedding, anchorEmbedding.embedding);
|
|
655
|
+
if (score > 0.5) {
|
|
656
|
+
const reason = this.generateReanchorReason(memory, score);
|
|
657
|
+
candidates.push({
|
|
658
|
+
memory_id: memory.memory_id,
|
|
659
|
+
score,
|
|
660
|
+
reason
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
665
|
+
return candidates;
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
logger.error('Anchor usage analysis failed', {
|
|
669
|
+
agentId,
|
|
670
|
+
slot,
|
|
671
|
+
error: error instanceof Error ? error.message : String(error)
|
|
672
|
+
});
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* 앵커 이동 이유 생성
|
|
678
|
+
*/
|
|
679
|
+
generateReanchorReason(memory, score) {
|
|
680
|
+
const reasons = [];
|
|
681
|
+
if (score > 0.7) {
|
|
682
|
+
reasons.push('높은 사용 빈도');
|
|
683
|
+
}
|
|
684
|
+
if (memory.similarity && memory.similarity > 0.8) {
|
|
685
|
+
reasons.push('쿼리와 높은 유사도');
|
|
686
|
+
}
|
|
687
|
+
if (memory.hop_distance === 1) {
|
|
688
|
+
reasons.push('앵커와 직접 연결');
|
|
689
|
+
}
|
|
690
|
+
return reasons.length > 0 ? reasons.join(', ') : '종합 점수 우수';
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* 자동 앵커 이동 실행
|
|
694
|
+
*/
|
|
695
|
+
async autoReanchor(agentId, slot, anchorManager, queryEmbedding, threshold = 0.7, strategy = 'gradual') {
|
|
696
|
+
if (!this.db) {
|
|
697
|
+
throw new Error('Database is not set.');
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
const currentAnchor = await anchorManager.getAnchor(agentId, slot);
|
|
701
|
+
if (!currentAnchor || Array.isArray(currentAnchor) || !currentAnchor.memory_id) {
|
|
702
|
+
return {
|
|
703
|
+
moved: false,
|
|
704
|
+
old_anchor: null,
|
|
705
|
+
new_anchor: null,
|
|
706
|
+
score: 0,
|
|
707
|
+
reason: '앵커가 설정되지 않았습니다'
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const anchorEmbedding = await this.cacheService.getAnchorEmbedding(currentAnchor.memory_id);
|
|
711
|
+
if (!anchorEmbedding) {
|
|
712
|
+
return {
|
|
713
|
+
moved: false,
|
|
714
|
+
old_anchor: currentAnchor.memory_id,
|
|
715
|
+
new_anchor: null,
|
|
716
|
+
score: 0,
|
|
717
|
+
reason: '앵커 임베딩을 찾을 수 없습니다'
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const candidates = await this.analyzeAnchorUsage(agentId, slot, currentAnchor.memory_id, anchorEmbedding, queryEmbedding);
|
|
721
|
+
if (candidates.length === 0 || !candidates[0] || candidates[0].score < threshold) {
|
|
722
|
+
return {
|
|
723
|
+
moved: false,
|
|
724
|
+
old_anchor: currentAnchor.memory_id,
|
|
725
|
+
new_anchor: null,
|
|
726
|
+
score: candidates[0]?.score || 0,
|
|
727
|
+
reason: `임계값(${threshold}) 미만 또는 후보 없음`
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const bestCandidate = candidates[0];
|
|
731
|
+
if (!bestCandidate) {
|
|
732
|
+
return {
|
|
733
|
+
moved: false,
|
|
734
|
+
old_anchor: currentAnchor.memory_id,
|
|
735
|
+
new_anchor: null,
|
|
736
|
+
score: 0,
|
|
737
|
+
reason: '후보 없음'
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (strategy === 'gradual') {
|
|
741
|
+
if (slot === 'A') {
|
|
742
|
+
const bAnchor = await anchorManager.getAnchor(agentId, 'B');
|
|
743
|
+
if (bAnchor && !Array.isArray(bAnchor) && bAnchor.memory_id) {
|
|
744
|
+
await anchorManager.setAnchor(agentId, bAnchor.memory_id, 'C');
|
|
745
|
+
}
|
|
746
|
+
await anchorManager.setAnchor(agentId, currentAnchor.memory_id, 'B');
|
|
747
|
+
}
|
|
748
|
+
else if (slot === 'B') {
|
|
749
|
+
await anchorManager.setAnchor(agentId, currentAnchor.memory_id, 'C');
|
|
750
|
+
}
|
|
751
|
+
await anchorManager.setAnchor(agentId, bestCandidate.memory_id, slot);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
await anchorManager.setAnchor(agentId, bestCandidate.memory_id, slot);
|
|
755
|
+
}
|
|
756
|
+
logger.info('Auto reanchor completed', {
|
|
757
|
+
agentId,
|
|
758
|
+
slot,
|
|
759
|
+
oldAnchor: currentAnchor.memory_id,
|
|
760
|
+
newAnchor: bestCandidate.memory_id,
|
|
761
|
+
score: bestCandidate.score,
|
|
762
|
+
reason: bestCandidate.reason
|
|
763
|
+
});
|
|
764
|
+
return {
|
|
765
|
+
moved: true,
|
|
766
|
+
old_anchor: currentAnchor.memory_id,
|
|
767
|
+
new_anchor: bestCandidate.memory_id,
|
|
768
|
+
score: bestCandidate.score,
|
|
769
|
+
reason: bestCandidate.reason
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
logger.error('Auto reanchor failed', {
|
|
774
|
+
agentId,
|
|
775
|
+
slot,
|
|
776
|
+
error: error instanceof Error ? error.message : String(error)
|
|
777
|
+
});
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* 검색 후 자동 앵커 이동 체크
|
|
783
|
+
*/
|
|
784
|
+
async checkAndAutoReanchor(agentId, slot, anchorManager, queryEmbedding, autoMoveEnabled = false) {
|
|
785
|
+
if (!autoMoveEnabled) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
return await this.autoReanchor(agentId, slot, anchorManager, queryEmbedding, 0.7, 'gradual');
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
logger.debug('Auto reanchor check failed (ignored)', {
|
|
793
|
+
error: error instanceof Error ? error.message : String(error)
|
|
794
|
+
});
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
//# sourceMappingURL=anchor-search-service.js.map
|