memento-mcp-server 1.13.1-a4 → 1.14.0-C
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/schema.sql +9 -2
- package/dist/domains/memory/tools/recall-tool.d.ts +190 -0
- package/dist/domains/memory/tools/recall-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/recall-tool.js +488 -14
- package/dist/domains/memory/tools/recall-tool.js.map +1 -1
- package/dist/domains/memory/tools/remember-tool.d.ts +10 -0
- package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/remember-tool.js +264 -35
- package/dist/domains/memory/tools/remember-tool.js.map +1 -1
- package/dist/domains/relation/services/relation-quality-validator.d.ts.map +1 -1
- package/dist/domains/relation/services/relation-quality-validator.js +15 -10
- package/dist/domains/relation/services/relation-quality-validator.js.map +1 -1
- package/dist/domains/relation/services/rule-based-relation-extractor.d.ts.map +1 -1
- package/dist/domains/relation/services/rule-based-relation-extractor.js +18 -0
- package/dist/domains/relation/services/rule-based-relation-extractor.js.map +1 -1
- package/dist/domains/relation/tools/add-relation-tool.d.ts +3 -3
- package/dist/domains/relation/tools/add-relation-tool.js +2 -2
- package/dist/domains/relation/tools/add-relation-tool.js.map +1 -1
- package/dist/domains/relation/tools/get-relations-tool.d.ts +3 -3
- package/dist/domains/relation/tools/get-relations-tool.js +2 -2
- package/dist/domains/relation/tools/get-relations-tool.js.map +1 -1
- package/dist/domains/search/algorithms/hybrid-search-engine.d.ts +20 -0
- package/dist/domains/search/algorithms/hybrid-search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/hybrid-search-engine.js +168 -5
- package/dist/domains/search/algorithms/hybrid-search-engine.js.map +1 -1
- package/dist/domains/search/algorithms/search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/search-engine.js +37 -17
- package/dist/domains/search/algorithms/search-engine.js.map +1 -1
- package/dist/domains/search/algorithms/search-ranking.d.ts +15 -2
- package/dist/domains/search/algorithms/search-ranking.d.ts.map +1 -1
- package/dist/domains/search/algorithms/search-ranking.js +46 -15
- package/dist/domains/search/algorithms/search-ranking.js.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.js +180 -89
- package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
- package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts +63 -0
- package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts.map +1 -0
- package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js +257 -0
- package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js.map +1 -0
- package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.sql +66 -0
- package/dist/infrastructure/reflexion-worker.d.ts +18 -0
- package/dist/infrastructure/reflexion-worker.d.ts.map +1 -1
- package/dist/infrastructure/reflexion-worker.js +216 -0
- package/dist/infrastructure/reflexion-worker.js.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.d.ts +51 -8
- package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.js +299 -205
- package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
- package/dist/infrastructure/scheduler/file-logger.d.ts +82 -0
- package/dist/infrastructure/scheduler/file-logger.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/file-logger.js +133 -0
- package/dist/infrastructure/scheduler/file-logger.js.map +1 -0
- package/dist/infrastructure/scheduler/health-checker.d.ts +54 -0
- package/dist/infrastructure/scheduler/health-checker.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/health-checker.js +96 -0
- package/dist/infrastructure/scheduler/health-checker.js.map +1 -0
- package/dist/infrastructure/scheduler/job-queue.d.ts +85 -0
- package/dist/infrastructure/scheduler/job-queue.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/job-queue.js +125 -0
- package/dist/infrastructure/scheduler/job-queue.js.map +1 -0
- package/dist/infrastructure/scheduler/relation-validator-executor.d.ts +37 -0
- package/dist/infrastructure/scheduler/relation-validator-executor.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/relation-validator-executor.js +120 -0
- package/dist/infrastructure/scheduler/relation-validator-executor.js.map +1 -0
- package/dist/infrastructure/scheduler/retry-manager.d.ts +62 -0
- package/dist/infrastructure/scheduler/retry-manager.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/retry-manager.js +91 -0
- package/dist/infrastructure/scheduler/retry-manager.js.map +1 -0
- package/dist/npm-client/utils.d.ts.map +1 -1
- package/dist/npm-client/utils.js +2 -1
- package/dist/npm-client/utils.js.map +1 -1
- package/dist/scripts/copy-assets.js +4 -4
- package/dist/scripts/copy-assets.js.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +15 -17
- package/dist/server/http-server.js.map +1 -1
- package/dist/services/anchor-manager.d.ts.map +1 -1
- package/dist/services/anchor-manager.js.map +1 -1
- package/dist/shared/types/index.d.ts +36 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/shared/types/index.js.map +1 -1
- package/dist/shared/types/relation.d.ts +1 -1
- package/dist/shared/types/relation.d.ts.map +1 -1
- package/dist/shared/types/relation.js +7 -4
- package/dist/shared/types/relation.js.map +1 -1
- package/dist/shared/utils/database.d.ts.map +1 -1
- package/dist/shared/utils/database.js +9 -2
- package/dist/shared/utils/database.js.map +1 -1
- package/dist/shared/utils/procedural-memory-extractor.d.ts +108 -0
- package/dist/shared/utils/procedural-memory-extractor.d.ts.map +1 -0
- package/dist/shared/utils/procedural-memory-extractor.js +581 -0
- package/dist/shared/utils/procedural-memory-extractor.js.map +1 -0
- package/dist/shared/utils/relation-type-converter.d.ts +52 -0
- package/dist/shared/utils/relation-type-converter.d.ts.map +1 -0
- package/dist/shared/utils/relation-type-converter.js +106 -0
- package/dist/shared/utils/relation-type-converter.js.map +1 -0
- package/dist/shared/utils/type-param-validator.d.ts +31 -0
- package/dist/shared/utils/type-param-validator.d.ts.map +1 -1
- package/dist/shared/utils/type-param-validator.js +90 -2
- package/dist/shared/utils/type-param-validator.js.map +1 -1
- package/dist/tools/base-tool.d.ts.map +1 -1
- package/dist/tools/types.d.ts +4 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +5 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/workers/consolidation-score-worker.d.ts.map +1 -1
- package/dist/workers/consolidation-score-worker.js +0 -2
- package/dist/workers/consolidation-score-worker.js.map +1 -1
- package/package.json +3 -3
- package/scripts/auto-setup.js +1 -1
- /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-core-memory.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-knowledge-vault.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-memory-item.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-schema-version.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/003-consolidation-score-fields.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/005-relation-engine-schema.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/006-fts5-reflection-notes-migration-status.sql +0 -0
- /package/dist/{database → infrastructure/database/database}/migration/migrations/006-fts5-reflection-notes.sql +0 -0
|
@@ -14,6 +14,9 @@ import { KnowledgeVaultService } from '../services/knowledge-vault-service.js';
|
|
|
14
14
|
import { validateTypeParam } from '../../../shared/utils/type-param-validator.js';
|
|
15
15
|
import { mementoConfig } from '../../../shared/config/index.js';
|
|
16
16
|
import { DatabaseUtils } from '../../../shared/utils/database.js';
|
|
17
|
+
import { MemoryNeighborService } from '../services/memory-neighbor-service.js';
|
|
18
|
+
import { getVectorSearchEngine } from '../../search/algorithms/vector-search-engine.js';
|
|
19
|
+
import { MemoryEmbeddingService } from '../services/memory-embedding-service.js';
|
|
17
20
|
/**
|
|
18
21
|
* Provider 필터 정규화 유틸리티
|
|
19
22
|
* 빈 배열인 경우 undefined로 변환하여 모든 provider 검색을 의미
|
|
@@ -41,12 +44,25 @@ const RecallSchema = z.object({
|
|
|
41
44
|
importance_min: z.number().min(0).max(1).optional(),
|
|
42
45
|
importance_max: z.number().min(0).max(1).optional(),
|
|
43
46
|
has_reflection_notes: z.boolean().optional(), // reflection_notes IS NOT NULL 필터링
|
|
47
|
+
// Procedural Memory Enhancement (v7.0) 필드
|
|
48
|
+
workflow_name: z.string().optional(),
|
|
49
|
+
skill_name: z.string().optional(),
|
|
50
|
+
match_trigger_conditions: z.boolean().optional().default(false),
|
|
51
|
+
context: z.record(z.any()).optional(), // 구조화된 컨텍스트 정보 (trigger_conditions 매칭용, 예: {tool_name, error_type, params})
|
|
52
|
+
trigger_context: z.record(z.any()).optional(), // context의 별칭 (하위 호환성)
|
|
53
|
+
return_format: z.enum(['full', 'steps_only']).optional().default('full'),
|
|
44
54
|
limit: CommonSchemas.Limit,
|
|
45
55
|
vector_weight: z.number().min(0).max(1).optional(),
|
|
46
56
|
text_weight: z.number().min(0).max(1).optional(),
|
|
47
57
|
enable_hybrid: z.boolean().optional(),
|
|
48
58
|
include_metadata: z.boolean().optional(),
|
|
49
|
-
provider_filter: z.array(z.enum(['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'])).optional()
|
|
59
|
+
provider_filter: z.array(z.enum(['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'])).optional(),
|
|
60
|
+
// 자동 앵커 설정 및 이웃 기억 포함 파라미터
|
|
61
|
+
auto_set_anchor: z.boolean().optional().default(false),
|
|
62
|
+
include_neighbors: z.boolean().optional().default(false),
|
|
63
|
+
neighbors_limit: z.number().min(1).max(10).optional().default(3),
|
|
64
|
+
neighbors_per_item: z.number().min(1).max(50).optional().default(5),
|
|
65
|
+
neighbors_similarity_threshold: z.number().min(0).max(1).optional().default(0.8)
|
|
50
66
|
}).refine((data) => {
|
|
51
67
|
// 조건부 필수 검증
|
|
52
68
|
if (data.type === 'core' || data.type === 'vault') {
|
|
@@ -128,6 +144,26 @@ export class RecallTool extends BaseTool {
|
|
|
128
144
|
type: 'boolean',
|
|
129
145
|
description: 'reflection_notes가 있는 메모리만 조회 (true: IS NOT NULL, false: IS NULL, 선택사항)'
|
|
130
146
|
},
|
|
147
|
+
// Procedural Memory Enhancement (v7.0) 필드
|
|
148
|
+
workflow_name: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: '프로세스 이름으로 필터링 (선택사항)'
|
|
151
|
+
},
|
|
152
|
+
skill_name: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: '기술/능력 이름으로 필터링 (선택사항)'
|
|
155
|
+
},
|
|
156
|
+
match_trigger_conditions: {
|
|
157
|
+
type: 'boolean',
|
|
158
|
+
default: false,
|
|
159
|
+
description: 'trigger_conditions 매칭 여부 (기본값: false)'
|
|
160
|
+
},
|
|
161
|
+
return_format: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: ['full', 'steps_only'],
|
|
164
|
+
default: 'full',
|
|
165
|
+
description: '반환 형식 선택: full (모든 필드), steps_only (steps만 반환)'
|
|
166
|
+
},
|
|
131
167
|
limit: {
|
|
132
168
|
type: 'number',
|
|
133
169
|
minimum: 1,
|
|
@@ -163,6 +199,37 @@ export class RecallTool extends BaseTool {
|
|
|
163
199
|
type: 'array',
|
|
164
200
|
items: { type: 'string', enum: ['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'] },
|
|
165
201
|
description: '검색할 임베딩 provider 필터 (선택사항, 미지정 시 모든 provider 검색)'
|
|
202
|
+
},
|
|
203
|
+
auto_set_anchor: {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
default: false,
|
|
206
|
+
description: '가장 관련성 높은 기억(첫 번째 결과)을 슬롯 A에 자동으로 앵커로 설정 (기본값: false)'
|
|
207
|
+
},
|
|
208
|
+
include_neighbors: {
|
|
209
|
+
type: 'boolean',
|
|
210
|
+
default: false,
|
|
211
|
+
description: '검색 결과의 상위 항목에 대해 이웃 기억을 자동으로 포함 (기본값: false)'
|
|
212
|
+
},
|
|
213
|
+
neighbors_limit: {
|
|
214
|
+
type: 'number',
|
|
215
|
+
minimum: 1,
|
|
216
|
+
maximum: 10,
|
|
217
|
+
default: 3,
|
|
218
|
+
description: '이웃 기억을 포함할 상위 결과의 개수 (각 결과당 이웃 개수는 neighbors_per_item으로 제어, 기본값: 3)'
|
|
219
|
+
},
|
|
220
|
+
neighbors_per_item: {
|
|
221
|
+
type: 'number',
|
|
222
|
+
minimum: 1,
|
|
223
|
+
maximum: 50,
|
|
224
|
+
default: 5,
|
|
225
|
+
description: '각 검색 결과 항목당 조회할 이웃 기억의 최대 개수 (기본값: 5)'
|
|
226
|
+
},
|
|
227
|
+
neighbors_similarity_threshold: {
|
|
228
|
+
type: 'number',
|
|
229
|
+
minimum: 0,
|
|
230
|
+
maximum: 1,
|
|
231
|
+
default: 0.8,
|
|
232
|
+
description: '이웃 기억 조회 시 유사도 임계값 (이 값 이상인 기억만 반환, 기본값: 0.8)'
|
|
166
233
|
}
|
|
167
234
|
},
|
|
168
235
|
required: [] // 조건부 필수는 런타임 검증 (RecallSchema.refine()에서 처리)
|
|
@@ -173,7 +240,9 @@ export class RecallTool extends BaseTool {
|
|
|
173
240
|
this.logInfo('Recall 도구 호출됨', { params });
|
|
174
241
|
try {
|
|
175
242
|
// 파라미터 검증 및 파싱
|
|
176
|
-
const { query, type, key, agent_id, memory_types, tags, privacy_scope, time_from, time_to, pinned, importance_min, importance_max, limit, vector_weight, text_weight, enable_hybrid, include_metadata, provider_filter } = RecallSchema.parse(params);
|
|
243
|
+
const { query, type, key, agent_id, memory_types, tags, privacy_scope, time_from, time_to, pinned, importance_min, importance_max, workflow_name, skill_name, match_trigger_conditions, context: triggerContext, trigger_context, return_format, limit, vector_weight, text_weight, enable_hybrid, include_metadata, provider_filter, auto_set_anchor, include_neighbors, neighbors_limit, neighbors_per_item, neighbors_similarity_threshold } = RecallSchema.parse(params);
|
|
244
|
+
// trigger_context가 제공되면 context로 사용 (하위 호환성)
|
|
245
|
+
const actualTriggerContext = triggerContext || trigger_context;
|
|
177
246
|
// type 파라미터 롤아웃 모드 검증
|
|
178
247
|
// PRD 요구사항: Phase 1/2에서는 type 파라미터가 없으면 항상 경고/Deprecated 메시지를 띄워야 함
|
|
179
248
|
// memory_types만 있어도 경고를 띄워야 하므로, type이 없으면 항상 검증 수행
|
|
@@ -219,7 +288,7 @@ export class RecallTool extends BaseTool {
|
|
|
219
288
|
});
|
|
220
289
|
// 데이터베이스 연결 확인
|
|
221
290
|
this.validateDatabase(context);
|
|
222
|
-
const
|
|
291
|
+
const searchStartTime = Date.now();
|
|
223
292
|
const agentId = agent_id || 'default';
|
|
224
293
|
// type 파라미터에 따른 분기 처리
|
|
225
294
|
if (validatedType === 'core') {
|
|
@@ -244,7 +313,7 @@ export class RecallTool extends BaseTool {
|
|
|
244
313
|
// 전체 Core Memory 조회
|
|
245
314
|
records = await coreMemoryService.findByAgentId(agentId);
|
|
246
315
|
}
|
|
247
|
-
const executionTime = Date.now() -
|
|
316
|
+
const executionTime = Date.now() - searchStartTime;
|
|
248
317
|
const processedResults = records.map(record => ({
|
|
249
318
|
memory_id: record.core_id,
|
|
250
319
|
type: 'core',
|
|
@@ -282,7 +351,7 @@ export class RecallTool extends BaseTool {
|
|
|
282
351
|
// 전체 Vault 조회 (활성 버전만)
|
|
283
352
|
records = await knowledgeVaultService.findActiveByAgentId(agentId);
|
|
284
353
|
}
|
|
285
|
-
const executionTime = Date.now() -
|
|
354
|
+
const executionTime = Date.now() - searchStartTime;
|
|
286
355
|
const processedResults = records.map(record => ({
|
|
287
356
|
memory_id: record.vault_id,
|
|
288
357
|
type: 'vault',
|
|
@@ -321,8 +390,24 @@ export class RecallTool extends BaseTool {
|
|
|
321
390
|
});
|
|
322
391
|
}
|
|
323
392
|
// memory_types 배열 전처리 ('core'/'vault' 제거)
|
|
324
|
-
//
|
|
325
|
-
|
|
393
|
+
// validatedType이 존재하면 항상 [validatedType]로 시작 (기본값 포함)
|
|
394
|
+
// originalTypeProvided가 false이고 memory_types가 제공되면 둘 다 고려하되, validatedType 우선
|
|
395
|
+
let filteredMemoryTypes;
|
|
396
|
+
if (validatedType) {
|
|
397
|
+
// validatedType이 있으면 항상 사용 (기본값이든 명시적 값이든)
|
|
398
|
+
filteredMemoryTypes = [validatedType];
|
|
399
|
+
// originalTypeProvided가 false이고 memory_types도 제공되면 경고
|
|
400
|
+
if (!originalTypeProvided && memory_types && memory_types.length > 0) {
|
|
401
|
+
this.logWarning('type 파라미터가 미지정되어 기본값이 적용되었지만, memory_types도 제공되었습니다. 기본 타입을 우선 적용하고 memory_types는 무시합니다.', {
|
|
402
|
+
default_type: validatedType,
|
|
403
|
+
memory_types
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// validatedType이 없으면 memory_types 사용
|
|
409
|
+
filteredMemoryTypes = memory_types;
|
|
410
|
+
}
|
|
326
411
|
if (filteredMemoryTypes && filteredMemoryTypes.length > 0) {
|
|
327
412
|
const invalidTypes = filteredMemoryTypes.filter(t => t === 'core' || t === 'vault');
|
|
328
413
|
if (invalidTypes.length > 0) {
|
|
@@ -357,7 +442,10 @@ export class RecallTool extends BaseTool {
|
|
|
357
442
|
pinned,
|
|
358
443
|
importance_min,
|
|
359
444
|
importance_max,
|
|
360
|
-
has_reflection_notes: params.has_reflection_notes
|
|
445
|
+
has_reflection_notes: params.has_reflection_notes,
|
|
446
|
+
// Procedural Memory Enhancement (v7.0) 필터
|
|
447
|
+
workflow_name,
|
|
448
|
+
skill_name
|
|
361
449
|
};
|
|
362
450
|
// 검색 옵션 설정
|
|
363
451
|
const vectorWeight = vector_weight ?? 0.6;
|
|
@@ -386,7 +474,9 @@ export class RecallTool extends BaseTool {
|
|
|
386
474
|
limit,
|
|
387
475
|
vectorWeight: normalizedVectorWeight,
|
|
388
476
|
textWeight: normalizedTextWeight,
|
|
389
|
-
provider_filter: providerFilter
|
|
477
|
+
provider_filter: providerFilter,
|
|
478
|
+
match_trigger_conditions: match_trigger_conditions,
|
|
479
|
+
context: actualTriggerContext // 구조화된 컨텍스트 정보 전달
|
|
390
480
|
});
|
|
391
481
|
}
|
|
392
482
|
else {
|
|
@@ -406,20 +496,52 @@ export class RecallTool extends BaseTool {
|
|
|
406
496
|
this.logError(searchError, '검색 실행 중 오류', { query, enableHybrid });
|
|
407
497
|
throw new Error(`검색 실행 실패: ${searchError.message}`);
|
|
408
498
|
}
|
|
409
|
-
const executionTime = Date.now() -
|
|
499
|
+
const executionTime = Date.now() - searchStartTime;
|
|
410
500
|
// 검색 결과 가져오기
|
|
411
|
-
|
|
501
|
+
let searchItems = searchResult?.items || [];
|
|
502
|
+
// trigger_conditions 매칭 필터링 (match_trigger_conditions=true일 때)
|
|
503
|
+
if (match_trigger_conditions && searchItems.length > 0) {
|
|
504
|
+
searchItems = this.filterByTriggerConditions(searchItems, query, actualTriggerContext);
|
|
505
|
+
}
|
|
412
506
|
// Consolidation Score System 업데이트 (기능 플래그 확인)
|
|
413
507
|
if (mementoConfig.consolidationScoreEnabled && context.services.consolidationScoreService && searchItems.length > 0) {
|
|
414
508
|
await this.updateConsolidationScoreMetadata(context.db, context.services.consolidationScoreService, context.services.writeCoalescingManager, searchItems);
|
|
415
509
|
}
|
|
416
510
|
// 결과 후처리 - searchResult가 undefined인 경우 처리
|
|
417
|
-
const processedResults = this.processSearchResults(searchItems, includeMetadata);
|
|
511
|
+
const processedResults = this.processSearchResults(searchItems, includeMetadata, return_format);
|
|
512
|
+
// 자동 앵커 설정 처리 (auto_set_anchor=true이고 검색 결과가 있을 때)
|
|
513
|
+
let anchorSetResult = null;
|
|
514
|
+
if (auto_set_anchor && searchItems.length > 0) {
|
|
515
|
+
anchorSetResult = await this.handleAutoSetAnchor(searchItems, agentId, context);
|
|
516
|
+
}
|
|
517
|
+
// 자동 이웃 기억 포함 처리 (include_neighbors=true이고 검색 결과가 있을 때)
|
|
518
|
+
let neighborsResults = [];
|
|
519
|
+
if (include_neighbors && searchItems.length > 0) {
|
|
520
|
+
neighborsResults = await this.handleIncludeNeighbors(searchItems, neighbors_limit, neighbors_per_item, neighbors_similarity_threshold, context);
|
|
521
|
+
// 검색 결과 항목에 neighbors 필드 추가
|
|
522
|
+
// neighbors_limit보다 많은 결과는 neighbors 필드 없음 (handleIncludeNeighbors가 상위 neighbors_limit개만 처리)
|
|
523
|
+
for (let i = 0; i < Math.min(neighborsResults.length, processedResults.length); i++) {
|
|
524
|
+
processedResults[i].neighbors = neighborsResults[i];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
418
527
|
this.logInfo('검색 완료', {
|
|
419
528
|
resultCount: processedResults.length,
|
|
420
529
|
executionTime,
|
|
421
530
|
searchType: enableHybrid ? 'hybrid' : 'text'
|
|
422
531
|
});
|
|
532
|
+
// 메타데이터 구성 (앵커 설정 결과 포함)
|
|
533
|
+
const metadata = {
|
|
534
|
+
anchor_set: anchorSetResult?.anchor_set || null
|
|
535
|
+
};
|
|
536
|
+
// 앵커 설정 실패 시
|
|
537
|
+
if (anchorSetResult && anchorSetResult.error) {
|
|
538
|
+
metadata.anchor_set_error = true;
|
|
539
|
+
}
|
|
540
|
+
// 앵커 설정 건너뜀 시
|
|
541
|
+
if (anchorSetResult && anchorSetResult.skipped) {
|
|
542
|
+
metadata.anchor_set_skipped = true;
|
|
543
|
+
metadata.anchor_set_skipped_reason = anchorSetResult.skipped_reason;
|
|
544
|
+
}
|
|
423
545
|
return this.createSuccessResult({
|
|
424
546
|
items: processedResults,
|
|
425
547
|
total_count: searchResult?.total_count || processedResults.length,
|
|
@@ -431,7 +553,8 @@ export class RecallTool extends BaseTool {
|
|
|
431
553
|
vector_weight: normalizedVectorWeight,
|
|
432
554
|
text_weight: normalizedTextWeight,
|
|
433
555
|
enable_hybrid: enableHybrid
|
|
434
|
-
}
|
|
556
|
+
},
|
|
557
|
+
metadata
|
|
435
558
|
});
|
|
436
559
|
}
|
|
437
560
|
}
|
|
@@ -455,10 +578,91 @@ export class RecallTool extends BaseTool {
|
|
|
455
578
|
throw error;
|
|
456
579
|
}
|
|
457
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* trigger_conditions로 필터링
|
|
583
|
+
* match_trigger_conditions=true일 때, 현재 컨텍스트와 trigger_conditions가 매칭되는 항목만 반환
|
|
584
|
+
*
|
|
585
|
+
* PRD 요구사항: 구조화된 컨텍스트(예: tool_name, error_type, params)와 JSON 매칭
|
|
586
|
+
* 구조화된 컨텍스트가 제공되면 이를 우선 사용하고, 없으면 쿼리 텍스트를 사용
|
|
587
|
+
*
|
|
588
|
+
* @param items 검색 결과 항목 배열
|
|
589
|
+
* @param query 검색 쿼리 (컨텍스트로 사용, fallback)
|
|
590
|
+
* @param context 구조화된 컨텍스트 정보 (우선 사용)
|
|
591
|
+
* @returns 필터링된 항목 배열
|
|
592
|
+
*/
|
|
593
|
+
filterByTriggerConditions(items, query, triggerContext) {
|
|
594
|
+
const queryText = query?.toLowerCase() || '';
|
|
595
|
+
return items.filter(item => {
|
|
596
|
+
// trigger_conditions가 없는 항목은 제외
|
|
597
|
+
if (!item.trigger_conditions) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
// JSON 파싱 시도
|
|
602
|
+
const parsed = typeof item.trigger_conditions === 'string'
|
|
603
|
+
? JSON.parse(item.trigger_conditions)
|
|
604
|
+
: item.trigger_conditions;
|
|
605
|
+
// 객체인지 확인 (배열이나 null이 아닌 경우)
|
|
606
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
// 구조화된 컨텍스트가 제공된 경우: 키-값 기반 정확 매칭
|
|
610
|
+
// 모든 키/값 쌍이 매칭되어야 함 (첫 번째 키만 맞으면 통과하는 문제 수정)
|
|
611
|
+
if (triggerContext && Object.keys(triggerContext).length > 0) {
|
|
612
|
+
// trigger_conditions의 모든 키-값 쌍이 컨텍스트와 매칭되는지 확인
|
|
613
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
614
|
+
const contextValue = triggerContext[key];
|
|
615
|
+
// trigger_conditions에 있는 키가 컨텍스트에 없으면 매칭 실패
|
|
616
|
+
if (contextValue === undefined) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// 값이 객체인 경우 재귀적으로 비교
|
|
620
|
+
if (typeof value === 'object' && typeof contextValue === 'object' && value !== null && contextValue !== null) {
|
|
621
|
+
// 중첩 객체 매칭: context의 값이 trigger_conditions의 값과 부분적으로 일치하는지 확인
|
|
622
|
+
const valueStr = JSON.stringify(value).toLowerCase();
|
|
623
|
+
const contextStr = JSON.stringify(contextValue).toLowerCase();
|
|
624
|
+
if (!(valueStr.includes(contextStr) || contextStr.includes(valueStr))) {
|
|
625
|
+
// 하나라도 매칭되지 않으면 실패
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
// 단순 값 매칭: 문자열로 변환하여 비교
|
|
631
|
+
const valueStr = String(value).toLowerCase();
|
|
632
|
+
const contextStr = String(contextValue).toLowerCase();
|
|
633
|
+
if (!(valueStr === contextStr || valueStr.includes(contextStr) || contextStr.includes(valueStr))) {
|
|
634
|
+
// 하나라도 매칭되지 않으면 실패
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// 모든 키/값 쌍이 매칭됨
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
// 구조화된 컨텍스트가 없는 경우: 쿼리 텍스트 기반 매칭 (fallback)
|
|
643
|
+
if (queryText) {
|
|
644
|
+
// 키 매칭: tool_name, error_type, params 등 구조화된 필드명과 매칭
|
|
645
|
+
const triggerKeys = Object.keys(parsed).map(k => k.toLowerCase());
|
|
646
|
+
const triggerValues = Object.values(parsed).map(v => String(v).toLowerCase());
|
|
647
|
+
// 키 또는 값 중 하나라도 쿼리와 매칭되면 통과
|
|
648
|
+
const keyMatch = triggerKeys.some(k => k.includes(queryText) || queryText.includes(k));
|
|
649
|
+
const valueMatch = triggerValues.some(v => v.includes(queryText) || queryText.includes(v));
|
|
650
|
+
return keyMatch || valueMatch;
|
|
651
|
+
}
|
|
652
|
+
// 쿼리와 컨텍스트가 모두 없으면 매칭 기준이 없으므로 필터링
|
|
653
|
+
// PRD: "현재 컨텍스트와 매칭" 요구사항 - 매칭 기준이 없으면 통과하지 않음
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
// JSON 파싱 실패 시 제외
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
458
662
|
/**
|
|
459
663
|
* 검색 결과 후처리
|
|
460
664
|
*/
|
|
461
|
-
processSearchResults(items, includeMetadata) {
|
|
665
|
+
processSearchResults(items, includeMetadata, returnFormat = 'full') {
|
|
462
666
|
return items.map(item => {
|
|
463
667
|
const processed = {
|
|
464
668
|
memory_id: item.id || item.memory_id, // 통일된 필드명 사용
|
|
@@ -491,6 +695,10 @@ export class RecallTool extends BaseTool {
|
|
|
491
695
|
if (item.type === 'procedural') {
|
|
492
696
|
processed.task_goal = item.task_goal || null;
|
|
493
697
|
processed.steps = item.steps || null;
|
|
698
|
+
// Procedural Memory Enhancement (v7.0) 필드 추가
|
|
699
|
+
processed.workflow_name = item.workflow_name || null;
|
|
700
|
+
processed.skill_name = item.skill_name || null;
|
|
701
|
+
processed.trigger_conditions = item.trigger_conditions || null;
|
|
494
702
|
// reflection_notes 필드 추가 (JSON 파싱)
|
|
495
703
|
if (item.reflection_notes) {
|
|
496
704
|
try {
|
|
@@ -507,6 +715,15 @@ export class RecallTool extends BaseTool {
|
|
|
507
715
|
else {
|
|
508
716
|
processed.reflection_notes = null;
|
|
509
717
|
}
|
|
718
|
+
// return_format='steps_only'일 때 steps만 반환
|
|
719
|
+
if (returnFormat === 'steps_only') {
|
|
720
|
+
// steps만 포함하고 나머지 필드는 제거
|
|
721
|
+
return {
|
|
722
|
+
memory_id: processed.memory_id,
|
|
723
|
+
id: processed.id,
|
|
724
|
+
steps: processed.steps
|
|
725
|
+
};
|
|
726
|
+
}
|
|
510
727
|
}
|
|
511
728
|
if (item.textScore !== undefined) {
|
|
512
729
|
processed.text_score = item.textScore;
|
|
@@ -561,6 +778,263 @@ export class RecallTool extends BaseTool {
|
|
|
561
778
|
}
|
|
562
779
|
return applied;
|
|
563
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* 자동 앵커 설정 처리
|
|
783
|
+
* 가장 관련성 높은 기억(첫 번째 결과)을 슬롯 A에 앵커로 설정
|
|
784
|
+
*
|
|
785
|
+
* @param searchItems - 검색 결과 항목 배열
|
|
786
|
+
* @param agentId - 에이전트 ID
|
|
787
|
+
* @param context - 도구 컨텍스트
|
|
788
|
+
* @returns 앵커 설정 결과 (성공/실패/건너뜀 상태 포함)
|
|
789
|
+
*/
|
|
790
|
+
async handleAutoSetAnchor(searchItems, agentId, context) {
|
|
791
|
+
// 검색 결과가 없으면 앵커 설정 불가
|
|
792
|
+
if (!searchItems || searchItems.length === 0) {
|
|
793
|
+
return {
|
|
794
|
+
success: false,
|
|
795
|
+
anchor_set: null
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// 첫 번째 결과의 memory_id 가져오기
|
|
799
|
+
const topMemory = searchItems[0];
|
|
800
|
+
const memoryId = topMemory.id || topMemory.memory_id;
|
|
801
|
+
if (!memoryId) {
|
|
802
|
+
this.logWarning('검색 결과에 memory_id가 없어 앵커 설정을 건너뜁니다', { topMemory });
|
|
803
|
+
return {
|
|
804
|
+
success: false,
|
|
805
|
+
anchor_set: null,
|
|
806
|
+
error: true
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
// AnchorManager 서비스 확인
|
|
810
|
+
if (!context.services.anchorManager) {
|
|
811
|
+
this.logWarning('AnchorManager 서비스가 없어 앵커 설정을 건너뜁니다');
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
anchor_set: null,
|
|
815
|
+
error: true
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
// 슬롯 A의 앵커 조회 및 pinned 상태 확인 (memory_item 테이블과 조인)
|
|
820
|
+
const slotAAnchor = await context.services.anchorManager.getAnchor(agentId, 'A');
|
|
821
|
+
if (slotAAnchor && typeof slotAAnchor === 'object' && 'memory_id' in slotAAnchor) {
|
|
822
|
+
// pinned 상태 확인 (memory_item 테이블과 조인)
|
|
823
|
+
const anchorMemory = context.db.prepare(`
|
|
824
|
+
SELECT pinned FROM memory_item WHERE id = ?
|
|
825
|
+
`).get(slotAAnchor.memory_id);
|
|
826
|
+
const isPinned = anchorMemory && (anchorMemory.pinned === 1 || anchorMemory.pinned === true);
|
|
827
|
+
// 슬롯 A에 pinned 앵커가 있으면 건너뛰기 (보호 정책)
|
|
828
|
+
if (isPinned) {
|
|
829
|
+
this.logInfo('슬롯 A에 pinned 앵커가 있어 앵커 설정을 건너뜁니다', {
|
|
830
|
+
agent_id: agentId,
|
|
831
|
+
existing_memory_id: slotAAnchor.memory_id
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
success: false,
|
|
835
|
+
anchor_set: null,
|
|
836
|
+
skipped: true,
|
|
837
|
+
skipped_reason: 'pinned_anchor_protected'
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
// 슬롯 A에 일반 앵커가 있으면 슬롯 B로 이동
|
|
841
|
+
const slotBAnchor = await context.services.anchorManager.getAnchor(agentId, 'B');
|
|
842
|
+
if (slotBAnchor && typeof slotBAnchor === 'object' && 'memory_id' in slotBAnchor) {
|
|
843
|
+
// 슬롯 B의 pinned 상태 확인
|
|
844
|
+
const slotBMemory = context.db.prepare(`
|
|
845
|
+
SELECT pinned FROM memory_item WHERE id = ?
|
|
846
|
+
`).get(slotBAnchor.memory_id);
|
|
847
|
+
const slotBIsPinned = slotBMemory && (slotBMemory.pinned === 1 || slotBMemory.pinned === true);
|
|
848
|
+
if (slotBIsPinned) {
|
|
849
|
+
this.logWarning('슬롯 B의 pinned 앵커가 덮어써집니다', {
|
|
850
|
+
agent_id: agentId,
|
|
851
|
+
old_memory_id: slotBAnchor.memory_id,
|
|
852
|
+
new_memory_id: slotAAnchor.memory_id
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
// 슬롯 B에 앵커가 있으면 슬롯 C로 이동
|
|
856
|
+
const slotCAnchor = await context.services.anchorManager.getAnchor(agentId, 'C');
|
|
857
|
+
if (slotCAnchor && typeof slotCAnchor === 'object' && 'memory_id' in slotCAnchor) {
|
|
858
|
+
// 슬롯 C의 pinned 상태 확인
|
|
859
|
+
const slotCMemory = context.db.prepare(`
|
|
860
|
+
SELECT pinned FROM memory_item WHERE id = ?
|
|
861
|
+
`).get(slotCAnchor.memory_id);
|
|
862
|
+
const slotCIsPinned = slotCMemory && (slotCMemory.pinned === 1 || slotCMemory.pinned === true);
|
|
863
|
+
if (slotCIsPinned) {
|
|
864
|
+
this.logWarning('슬롯 C의 pinned 앵커가 제거됩니다', {
|
|
865
|
+
agent_id: agentId,
|
|
866
|
+
old_memory_id: slotCAnchor.memory_id
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
// 슬롯 C에 앵커가 있으면 제거 (pinned 여부와 관계없이 회전 규칙에 따라 제거)
|
|
870
|
+
await context.services.anchorManager.clearAnchor(agentId, 'C');
|
|
871
|
+
}
|
|
872
|
+
// 슬롯 B의 기존 앵커를 슬롯 C로 이동 (슬롯 B를 비우기 위해)
|
|
873
|
+
// PRD: 슬롯 B/C의 pinned 앵커도 덮어쓰고 A→B→C→제거 순으로 회전
|
|
874
|
+
// 먼저 슬롯 B를 제거한 후 슬롯 C에 설정
|
|
875
|
+
const slotBMemoryId = slotBAnchor.memory_id;
|
|
876
|
+
await context.services.anchorManager.clearAnchor(agentId, 'B');
|
|
877
|
+
await context.services.anchorManager.setAnchor(agentId, slotBMemoryId, 'C');
|
|
878
|
+
}
|
|
879
|
+
// 슬롯 A의 앵커를 슬롯 B로 이동
|
|
880
|
+
// 먼저 슬롯 A를 제거한 후 슬롯 B에 설정
|
|
881
|
+
const slotAMemoryId = slotAAnchor.memory_id;
|
|
882
|
+
await context.services.anchorManager.clearAnchor(agentId, 'A');
|
|
883
|
+
await context.services.anchorManager.setAnchor(agentId, slotAMemoryId, 'B');
|
|
884
|
+
}
|
|
885
|
+
// 새로운 기억을 슬롯 A에 설정
|
|
886
|
+
await context.services.anchorManager.setAnchor(agentId, memoryId, 'A');
|
|
887
|
+
this.logInfo('앵커가 자동으로 설정되었습니다', {
|
|
888
|
+
agent_id: agentId,
|
|
889
|
+
memory_id: memoryId,
|
|
890
|
+
slot: 'A'
|
|
891
|
+
});
|
|
892
|
+
return {
|
|
893
|
+
success: true,
|
|
894
|
+
anchor_set: {
|
|
895
|
+
memory_id: memoryId,
|
|
896
|
+
slot: 'A',
|
|
897
|
+
agent_id: agentId
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
// 앵커 설정 실패 시 경고만 로그하고 검색 결과는 정상 반환
|
|
903
|
+
this.logError(error, '앵커 자동 설정 실패', {
|
|
904
|
+
agent_id: agentId,
|
|
905
|
+
memory_id: memoryId
|
|
906
|
+
});
|
|
907
|
+
return {
|
|
908
|
+
success: false,
|
|
909
|
+
anchor_set: null,
|
|
910
|
+
error: true
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* 자동 이웃 기억 포함 처리
|
|
916
|
+
* 검색 결과의 상위 항목에 대해 이웃 기억을 자동으로 포함
|
|
917
|
+
*
|
|
918
|
+
* @param searchItems - 검색 결과 항목 배열
|
|
919
|
+
* @param neighborsLimit - 이웃 기억을 포함할 상위 결과의 개수
|
|
920
|
+
* @param neighborsPerItem - 각 검색 결과 항목당 조회할 이웃 기억의 최대 개수
|
|
921
|
+
* @param neighborsSimilarityThreshold - 이웃 기억 조회 시 유사도 임계값
|
|
922
|
+
* @param context - 도구 컨텍스트
|
|
923
|
+
* @returns 각 검색 결과 항목에 대한 이웃 기억 배열 (순서 보존)
|
|
924
|
+
*/
|
|
925
|
+
async handleIncludeNeighbors(searchItems, neighborsLimit, neighborsPerItem, neighborsSimilarityThreshold, context) {
|
|
926
|
+
// 검색 결과가 없으면 빈 배열 반환
|
|
927
|
+
if (!searchItems || searchItems.length === 0) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
// 상위 neighbors_limit개 결과 추출 (검색 결과 개수보다 작으면 검색 결과 개수로 제한)
|
|
931
|
+
const topResults = searchItems.slice(0, Math.min(neighborsLimit, searchItems.length));
|
|
932
|
+
// MemoryNeighborService 인스턴스 생성
|
|
933
|
+
let neighborService;
|
|
934
|
+
try {
|
|
935
|
+
const vectorSearchEngine = getVectorSearchEngine();
|
|
936
|
+
const embeddingService = context.services.embeddingService || new MemoryEmbeddingService();
|
|
937
|
+
neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingService);
|
|
938
|
+
neighborService.setDatabase(context.db);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
this.logError(error, 'MemoryNeighborService 초기화 실패', {});
|
|
942
|
+
// 서비스 초기화 실패 시 빈 배열 반환 (각 요소가 독립적인 배열 인스턴스)
|
|
943
|
+
return Array.from({ length: topResults.length }, () => []);
|
|
944
|
+
}
|
|
945
|
+
// 각 상위 결과에 대해 이웃 기억 조회를 병렬 처리
|
|
946
|
+
const neighborPromises = topResults.map(async (item, index) => {
|
|
947
|
+
const memoryId = item.id || item.memory_id;
|
|
948
|
+
if (!memoryId) {
|
|
949
|
+
this.logWarning('검색 결과에 memory_id가 없어 이웃 기억 조회를 건너뜁니다', { item });
|
|
950
|
+
return { index, neighbors: [] };
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
// 개별 이웃 기억 조회에 타임아웃 적용 (각 조회당 최대 2초)
|
|
954
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
955
|
+
setTimeout(() => reject(new Error('Timeout')), 2000);
|
|
956
|
+
});
|
|
957
|
+
const neighborPromise = neighborService.getNeighbors(memoryId, {
|
|
958
|
+
limit: neighborsPerItem,
|
|
959
|
+
similarity_threshold: neighborsSimilarityThreshold
|
|
960
|
+
}).then(result => ({
|
|
961
|
+
index,
|
|
962
|
+
neighbors: result.neighbors
|
|
963
|
+
}));
|
|
964
|
+
const result = await Promise.race([neighborPromise, timeoutPromise]);
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
// 타임아웃 또는 에러 발생 시 빈 배열 반환
|
|
969
|
+
if (error instanceof Error && error.message === 'Timeout') {
|
|
970
|
+
this.logWarning('이웃 기억 조회 타임아웃', { memoryId, index });
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
this.logError(error, '이웃 기억 조회 실패', { memoryId, index });
|
|
974
|
+
}
|
|
975
|
+
return { index, neighbors: [] };
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
// 전체 요청 타임아웃 적용 (2.5초, 부분 성공 결과 반환)
|
|
979
|
+
// 각 promise의 완료 상태를 추적하여 타임아웃 시 즉시 완료된 것만 반환
|
|
980
|
+
const completedResults = new Map();
|
|
981
|
+
// 각 promise에 대해 완료 시 결과를 저장
|
|
982
|
+
neighborPromises.forEach((promise, idx) => {
|
|
983
|
+
promise
|
|
984
|
+
.then(result => {
|
|
985
|
+
completedResults.set(idx, result);
|
|
986
|
+
})
|
|
987
|
+
.catch(() => {
|
|
988
|
+
// 에러는 무시하고 빈 배열로 처리
|
|
989
|
+
completedResults.set(idx, { index: idx, neighbors: [] });
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
let timeoutId = null;
|
|
993
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
994
|
+
timeoutId = setTimeout(() => {
|
|
995
|
+
// 타임아웃 시 현재까지 완료된 결과만 즉시 반환 (Promise.allSettled를 기다리지 않음)
|
|
996
|
+
const partialResults = [];
|
|
997
|
+
// 완료된 결과 수집
|
|
998
|
+
for (let i = 0; i < topResults.length; i++) {
|
|
999
|
+
if (completedResults.has(i)) {
|
|
1000
|
+
partialResults.push(completedResults.get(i));
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
// 완료되지 않은 항목은 빈 배열로 채움
|
|
1004
|
+
partialResults.push({ index: i, neighbors: [] });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// 인덱스 순서로 정렬하여 반환
|
|
1008
|
+
resolve(partialResults.sort((a, b) => a.index - b.index));
|
|
1009
|
+
}, 2500); // 전체 타임아웃: 2.5초
|
|
1010
|
+
});
|
|
1011
|
+
try {
|
|
1012
|
+
const allNeighbors = await Promise.race([
|
|
1013
|
+
Promise.all(neighborPromises),
|
|
1014
|
+
timeoutPromise
|
|
1015
|
+
]);
|
|
1016
|
+
// 타임아웃 취소
|
|
1017
|
+
if (timeoutId)
|
|
1018
|
+
clearTimeout(timeoutId);
|
|
1019
|
+
// 결과를 원래 순서로 정렬 (인덱스 기준)
|
|
1020
|
+
const sortedNeighbors = allNeighbors
|
|
1021
|
+
.sort((a, b) => a.index - b.index)
|
|
1022
|
+
.map(r => r.neighbors);
|
|
1023
|
+
return sortedNeighbors;
|
|
1024
|
+
}
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
// 타임아웃 취소
|
|
1027
|
+
if (timeoutId)
|
|
1028
|
+
clearTimeout(timeoutId);
|
|
1029
|
+
// 타임아웃 시에도 부분 완료 결과는 반환됨 (timeoutPromise에서 처리)
|
|
1030
|
+
// 완료된 결과만 반환
|
|
1031
|
+
const settledResults = await Promise.allSettled(neighborPromises);
|
|
1032
|
+
return settledResults.map((r, idx) => r.status === 'fulfilled'
|
|
1033
|
+
? r.value.neighbors
|
|
1034
|
+
: [] // 실패한 항목은 빈 배열
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
564
1038
|
/**
|
|
565
1039
|
* 검색 쿼리 검증
|
|
566
1040
|
*/
|