claude-memory-layer 1.0.10 → 1.0.12

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 (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. package/test_access.js +0 -49
@@ -0,0 +1,1436 @@
1
+
2
+ ## 2026-02-25T12:31:26.398Z | 1e97bc37-cfdb-41d2-97fe-8adce20db52d
3
+ - type: session_summary
4
+ - session: import:organized
5
+ # Progressive Disclosure Context
6
+
7
+ > **Version**: 1.0.0
8
+ > **Created**: 2026-02-01
9
+
10
+ ## 1. 배경
11
+
12
+ ### 1.1 claude-mem의 접근 방식
13
+
14
+ claude-mem은 토큰 효율성을 위해 3-Layer Progressive Disclosure 패턴을 사용:
15
+
16
+ ```
17
+ Layer 1: Search Index (~50-100 tokens per result)
18
+ ↓ (필터링)
19
+ Layer 2: Timeline (~200 tokens)
20
+ ↓ (선택)
21
+ Layer 3: Full Details (~500-1000 tokens per result)
22
+ ```
23
+
24
+ **주요 특징**:
25
+ - "필터링 후 상세 조회" 전략
26
+ - 약 10배 토큰 절약
27
+ - 사용자/AI가 필요한 것만 확장
28
+
29
+ **구현 방식**:
30
+ - MCP 도구로 각 레이어 노출
31
+ - `search` → `timeline` → `get_observations` 순서
32
+ - `__IMPORTANT` 도구로 워크플로우 문서화
33
+
34
+ ### 1.2 현재 code-memory의 상황
35
+
36
+ 현재 검색은 단일 레이어:
37
+
38
+ ```typescript
39
+ // 현재 Retriever.search()
40
+ async search(query: string): Promise<SearchResult[]> {
41
+ const vectorResults = await this.vectorStore.search(query, { topK: 5 });
42
+ const events = await this.enrichWithEvents(vectorResults);
43
+ return events; // 전체 내용 반환
44
+ }
45
+ ```
46
+
47
+ **문제점**:
48
+ 1. 모든 결과의 전체 내용을 가져옴
49
+ 2. 컨텍스트 크기가 토큰 제한에 쉽게 도달
50
+ 3. 관련성 낮은 내용도 포함됨
51
+
52
+ ### 1.3 토큰 비용 분석
53
+
54
+ | 시나리오 | 현재 방식 | Progressive 방식 |
55
+ |----------|----------|-----------------|
56
+ | 5개 결과, 1개만 관련 | ~5,000 tokens | ~600 tokens |
57
+ | 10개 결과, 2개만 관련 | ~10,000 tokens | ~1,200 tokens |
58
+ | 20개 결과, 3개만 관련 | ~20,000 tokens | ~2,000 tokens |
59
+
60
+ **절약 효과**: 평균 80-90% 토큰 감소
61
+
62
+ ## 2. MCP 도구 설계 참고
63
+
64
+ ### 2.1 claude-mem의 MCP 도구
65
+
66
+ ```typescript
67
+ // claude-mem MCP tools (추정)
68
+ {
69
+ tools: [
70
+ {
71
+ name: 'search',
72
+ description: 'Search memories, returns index only',
73
+ input_schema: {
74
+ query: 'string',
75
+ filters: { type: 'string', date: 'string' }
76
+ },
77
+ output: 'SearchIndexItem[]'
78
+ },
79
+ {
80
+ name: 'timeline',
81
+ description: 'Get timeline context around observations',
82
+ input_schema: {
83
+ observation_ids: 'string[]',
84
+ window_size: 'number'
85
+ },
86
+ output: 'TimelineItem[]'
87
+ },
88
+ {
89
+ name: 'get_observations',
90
+ description: 'Get full observation details by IDs',
91
+ input_schema: {
92
+ ids: 'string[]'
93
+ },
94
+ output: 'Observation[]'
95
+ },
96
+ {
97
+ name: '__IMPORTANT',
98
+ description: 'Workflow documentation for Claude',
99
+ // Claude가 이 도구를 보고 검색 워크플로우를 이해
100
+ }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ ### 2.2 워크플로우 문서화
106
+
107
+ ```markdown
108
+ # Memory Search Workflow
109
+
110
+ 1. **Always start with `search`** to get compact index
111
+ 2. **Review scores** before expanding
112
+ 3. **Use `timeline`** if context is needed
113
+ 4. **Only call `get_observations`** for selected IDs
114
+ 5. **Never** fetch all details at once
115
+ ```
116
+
117
+ ## 3. 기존 코드와의 관계
118
+
119
+ ### 3.1 retriever.ts
120
+
121
+ 현재 Retriever 구조:
122
+
123
+ ```typescript
124
+ export class Retriever {
125
+ async search(query: string): Promise<SearchResult[]> {
126
+ // 1. 벡터 검색
127
+ const vectorResults = await this.vectorStore.search(query);
128
+
129
+ // 2. 이벤트 enrichment (전체 로드)
130
+ const enriched = await Promise.all(
131
+ vectorResults.map(async (r) => {
132
+ const event = await this.eventStore.findById(r.id);
133
+ return { ...r, content: event.payload.content }; // 전체 내용
134
+ })
135
+ );
136
+
137
+ return enriched;
138
+ }
139
+ }
140
+ ```
141
+
142
+ **수정 방향**:
143
+ - `search()` → `searchIndex()` (Layer 1)
144
+ - `getTimeline()` 추가 (Layer 2)
145
+ - `getDetails()` 추가 (Layer 3)
146
+ - `smartSearch()` 추가 (자동 확장)
147
+
148
+ ### 3.2 matcher.ts
149
+
150
+ 현재 Matcher는 confidence 기반 분류:
151
+
152
+ ```typescript
153
+ export function matchSearchResults(results: SearchResult[]): MatchResult {
154
+ const high = results.filter(r => r.score >= 0.92);
155
+ const suggested = results.filter(r => r.score >= 0.75 && r.score < 0.92);
156
+
157
+ return { high, suggested, none: [] };
158
+ }
159
+ ```
160
+
161
+ **확장 방향**:
162
+ - 기존 Matcher 로직을 확장 규칙에 통합
163
+ - `high` → 자동 확장 대상
164
+ - `suggested` → Layer 1만 표시
165
+
166
+ ### 3.3 vector-store.ts
167
+
168
+ 현재 VectorStore 검색:
169
+
170
+ ```typescript
171
+ async search(query: string, options: { topK: number }): Promise<VectorSearchResult[]> {
172
+ const queryVector = await this.embedder.embed(query);
173
+ return this.db.search(queryVector, options.topK);
174
+ }
175
+ ```
176
+
177
+ **변경 불필요** - 기존 벡터 검색 그대로 사용
178
+
179
+ ### 3.4 event-store.ts
180
+
181
+ 필요한 추가 메서드:
182
+
183
+ ```typescript
184
+ // 주변 이벤트 조회 (타임라인용)
185
+ async findSurrounding(
186
+ sessionId: string,
187
+ timestamp: Date,
188
+ windowSize: number
189
+ ): Promise<Event[]> {
190
+ return this.db.query(`
191
+ SELECT * FROM events
192
+ WHERE session_id = ?
193
+ AND timestamp BETWEEN
194
+ datetime(?, '-${windowSize} hours') AND
195
+ datetime(?, '+${windowSize} hours')
196
+ ORDER BY timestamp
197
+ `, [sessionId, timestamp, timestamp]);
198
+ }
199
+ ```
200
+
201
+ ## 4. 설계 결정 사항
202
+
203
+ ### 4.1 왜 3개 레이어인가?
204
+
205
+ **대안 1: 2개 레이어 (Index + Detail)**
206
+ - 단점: 시간 맥락 파악 어려움
207
+ - 단점: 모호한 결과 처리 어려움
208
+
209
+ **대안 2: 4개 이상 레이어**
210
+ - 단점: 복잡도 증가
211
+ - 단점: 실용적 이점 미미
212
+
213
+ **선택: 3개 레이어**
214
+ - Layer 1: What (무엇이 있는지)
215
+ - Layer 2: When (언제 발생했는지)
216
+ - Layer 3: How (구체적으로 어떻게)
217
+
218
+ ### 4.2 자동 확장 vs 수동 확장
219
+
220
+ **자동 확장 장점**:
221
+ - 사용자 경험 향상
222
+ - "자세히 알려줘" 명령 불필요
223
+ - 높은 신뢰도 결과 즉시 제공
224
+
225
+ **자동 확장 단점**:
226
+ - 토큰 예측 어려움
227
+ - 때로는 불필요한 확장
228
+
229
+ **결론: 하이브리드 접근**
230
+ - 높은 신뢰도 → 자동 확장
231
+ - 중간 신뢰도 → Index만 제공 + 힌트
232
+ - 낮은 신뢰도 → Index만 제공
233
+
234
+ ### 4.3 요약 생성 전략
235
+
236
+ **Option 1: LLM 요약**
237
+ - 장점: 고품질 요약
238
+ - 단점: 비용, 지연시간
239
+
240
+ **Option 2: 규칙 기반 추출**
241
+ - 장점: 빠름, 무료
242
+ - 단점: 품질 제한
243
+
244
+ **선택: 규칙 기반 + 캐싱**
245
+ - 첫 문장 추출
246
+ - 코드 블록 축약
247
+ - 결과 캐싱
248
+
249
+ ### 4.4 토큰 추정 방식
250
+
251
+ ```typescript
252
+ // 간단한 추정 (정확도 ~85%)
253
+ function estimateTokens(text: string): number {
254
+ return Math.ceil(text.length / 4);
255
+ }
256
+
257
+ // 또는 정확한 추정 (tiktoken 사용)
258
+ import { encoding_for_model } from 'tiktoken';
259
+ const enc = encoding_for_model('gpt-4');
260
+ function estimateTokens(text: string): number {
261
+ return enc.encode(text).length;
262
+ }
263
+ ```
264
+
265
+ **결론**: 간단한 추정 사용 (성능 우선)
266
+
267
+ ## 5. 성능 고려사항
268
+
269
+ ### 5.1 검색 지연시간
270
+
271
+ | 레이어 | 목표 지연시간 | 병목 |
272
+ |--------|-------------|------|
273
+ | Layer 1 | < 100ms | 벡터 검색 |
274
+ | Layer 2 | < 200ms | DB 쿼리 |
275
+ | Layer 3 | < 500ms | 다중 조회 |
276
+
277
+ **최적화 전략**:
278
+ - Layer 1: 벡터 인덱스 최적화
279
+ - Layer 2: 세션별 인덱스 활용
280
+ - Layer 3: 배치 조회
281
+
282
+ ### 5.2 캐싱 전략
283
+
284
+ ```typescript
285
+ // 레이어별 캐시 TTL
286
+ const CACHE_CONFIG = {
287
+ layer1: {
288
+ ttl: 60 * 1000, // 1분 (검색 결과는 자주 변함)
289
+ maxSize: 100
290
+ },
291
+ layer2: {
292
+ ttl: 5 * 60 * 1000, // 5분 (타임라인은 안정적)
293
+ maxSize: 500
294
+ },
295
+ layer3: {
296
+ ttl: 30 * 60 * 1000, // 30분 (상세 내용은 거의 안 변함)
297
+ maxSize: 200
298
+ }
299
+ };
300
+ ```
301
+
302
+ ### 5.3 메모리 사용
303
+
304
+ - Layer 1 캐시: ~10KB per entry × 100 = ~1MB
305
+ - Layer 2 캐시: ~2KB per entry × 500 = ~1MB
306
+ - Layer 3 캐시: ~10KB per entry × 200 = ~2MB
307
+ - **총 메모리**: ~4MB (허용 범위)
308
+
309
+ ## 6. UI/UX 고려사항
310
+
311
+ ### 6.1 CLI 출력 포맷
312
+
313
+ ```
314
+ 🔍 Search Results (5 matches)
315
+
316
+ #1 [mem_abc] DuckDB 스키마 설계 논의 (0.94)
317
+ #2 [mem_def] 타입 시스템 리팩토링 (0.87)
318
+ #3 [mem_ghi] 벡터 저장소 설정 (0.82)
319
+
320
+ 💡 Tip: Use "show mem_abc" for details
321
+
322
+ ---
323
+
324
+ 📅 Timeline (auto-expanded for high confidence)
325
+
326
+ 14:00 → User asked about schema design
327
+ 14:05 → **[mem_abc]** Discussed DuckDB approach
328
+ 14:15 → Follow-up on indexing
329
+ ```
330
+
331
+ ### 6.2 확장 힌트
332
+
333
+ ```typescript
334
+ function formatExpansionHint(result: ProgressiveSearchResult): string {
335
+ if (result.meta.expandedCount === 0) {
336
+ return `Use "show [id]" to see details`;
337
+ }
338
+ if (result.meta.expansionReason === 'ambiguous_multiple_high') {
339
+ return `Multiple matches found. Use "show [id]" for specific details`;
340
+ }
341
+ return '';
342
+ }
343
+ ```
344
+
345
+ ## 7. 참고 자료
346
+
347
+ - **claude-mem README**: Progressive disclosure pattern, MCP tools
348
+ - **OpenAI Cookbook**: Token counting and optimization
349
+ - **AXIOMMIND**: Principle 7 (Standard JSON) - 포맷 일관성
350
+ - **기존 specs**: retriever.ts, matcher.ts 구현
351
+
352
+ ## 2026-02-25T12:31:26.406Z | 65bf8cff-b5ca-4506-b45b-abb5fc746a5c
353
+ - type: session_summary
354
+ - session: import:organized
355
+ # Progressive Disclosure Implementation Plan
356
+
357
+ > **Version**: 1.0.0
358
+ > **Status**: Draft
359
+ > **Created**: 2026-02-01
360
+
361
+ ## Phase 1: 타입 및 인터페이스 정의 (P0)
362
+
363
+ ### 1.1 스키마 정의
364
+
365
+ **파일**: `src/core/types.ts` 수정
366
+
367
+ ```typescript
368
+ // Layer 1: 검색 인덱스
369
+ export const SearchIndexItemSchema = z.object({
370
+ id: z.string(),
371
+ summary: z.string().max(100),
372
+ score: z.number(),
373
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
374
+ timestamp: z.date(),
375
+ sessionId: z.string()
376
+ });
377
+
378
+ // Layer 2: 타임라인
379
+ export const TimelineItemSchema = z.object({
380
+ id: z.string(),
381
+ timestamp: z.date(),
382
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
383
+ preview: z.string().max(200),
384
+ isTarget: z.boolean()
385
+ });
386
+
387
+ // Layer 3: 상세
388
+ export const FullDetailSchema = z.object({
389
+ id: z.string(),
390
+ content: z.string(),
391
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
392
+ timestamp: z.date(),
393
+ sessionId: z.string(),
394
+ metadata: z.object({
395
+ tokenCount: z.number(),
396
+ hasCode: z.boolean(),
397
+ files: z.array(z.string()).optional(),
398
+ tools: z.array(z.string()).optional()
399
+ }),
400
+ relations: z.object({
401
+ parentId: z.string().optional(),
402
+ childIds: z.array(z.string()),
403
+ relatedIds: z.array(z.string())
404
+ }).optional()
405
+ });
406
+ ```
407
+
408
+ **작업 항목**:
409
+ - [ ] SearchIndexItemSchema 추가
410
+ - [ ] TimelineItemSchema 추가
411
+ - [ ] FullDetailSchema 추가
412
+ - [ ] ProgressiveSearchResultSchema 추가
413
+
414
+ ### 1.2 설정 스키마 확장
415
+
416
+ **파일**: `src/core/types.ts` 수정
417
+
418
+ ```typescript
419
+ export const ProgressiveDisclosureConfigSchema = z.object({
420
+ enabled: z.boolean().default(true),
421
+ layer1: z.object({
422
+ topK: z.number().default(10),
423
+ minScore: z.number().default(0.7)
424
+ }),
425
+ autoExpand: z.object({
426
+ enabled: z.boolean().default(true),
427
+ highConfidenceThreshold: z.number().default(0.92),
428
+ scoreGapThreshold: z.number().default(0.1),
429
+ maxAutoExpandCount: z.number().default(3)
430
+ }),
431
+ tokenBudget: z.object({
432
+ maxTotalTokens: z.number().default(2000),
433
+ layer1PerItem: z.number().default(50),
434
+ layer2PerItem: z.number().default(40),
435
+ layer3PerItem: z.number().default(500)
436
+ }),
437
+ cache: z.object({
438
+ layer1Ttl: z.number().default(60000),
439
+ layer2Ttl: z.number().default(300000),
440
+ layer3Ttl: z.number().default(1800000)
441
+ })
442
+ });
443
+ ```
444
+
445
+ **작업 항목**:
446
+ - [ ] ProgressiveDisclosureConfigSchema 추가
447
+ - [ ] ConfigSchema에 progressiveDisclosure 필드 추가
448
+
449
+ ## Phase 2: ProgressiveRetriever 구현 (P0)
450
+
451
+ ### 2.1 기본 클래스 구조
452
+
453
+ **파일**: `src/core/progressive-retriever.ts` (신규)
454
+
455
+ ```typescript
456
+ export class ProgressiveRetriever {
457
+ constructor(
458
+ private eventStore: EventStore,
459
+ private vectorStore: VectorStore,
460
+ private config: ProgressiveDisclosureConfig
461
+ ) {}
462
+
463
+ // Layer 1: 검색 인덱스
464
+ async searchIndex(
465
+ query: string,
466
+ options?: { topK?: number; filter?: SearchFilter }
467
+ ): Promise<SearchIndexItem[]> {
468
+ const { topK = this.config.layer1.topK } = options || {};
469
+
470
+ // 벡터 검색
471
+ const vectorResults = await this.vectorStore.search(query, { topK });
472
+
473
+ // 요약 생성 및 변환
474
+ return vectorResults.map(r => ({
475
+ id: r.id,
476
+ summary: this.generateSummary(r.content),
477
+ score: r.score,
478
+ type: r.metadata.type,
479
+ timestamp: r.metadata.timestamp,
480
+ sessionId: r.metadata.sessionId
481
+ }));
482
+ }
483
+
484
+ // Layer 2: 타임라인
485
+ async getTimeline(
486
+ targetIds: string[],
487
+ options?: { windowSize?: number }
488
+ ): Promise<TimelineItem[]> {
489
+ const { windowSize = 3 } = options || {};
490
+
491
+ const items: TimelineItem[] = [];
492
+
493
+ for (const targetId of targetIds) {
494
+ const event = await this.eventStore.findById(targetId);
495
+ if (!event) continue;
496
+
497
+ // 주변 이벤트 조회
498
+ const surrounding = await this.eventStore.findSurrounding(
499
+ event.sessionId,
500
+ event.timestamp,
501
+ windowSize
502
+ );
503
+
504
+ items.push(...surrounding.map(e => ({
505
+ id: e.eventId,
506
+ timestamp: e.timestamp,
507
+ type: e.eventType,
508
+ preview: this.generatePreview(e.payload),
509
+ isTarget: e.eventId === targetId
510
+ })));
511
+ }
512
+
513
+ return this.deduplicateTimeline(items);
514
+ }
515
+
516
+ // Layer 3: 상세 정보
517
+ async getDetails(ids: string[]): Promise<FullDetail[]> {
518
+ const details: FullDetail[] = [];
519
+
520
+ for (const id of ids) {
521
+ const event = await this.eventStore.findById(id);
522
+ if (!event) continue;
523
+
524
+ details.push({
525
+ id: event.eventId,
526
+ content: event.payload.content,
527
+ type: event.eventType,
528
+ timestamp: event.timestamp,
529
+ sessionId: event.sessionId,
530
+ metadata: this.extractMetadata(event),
531
+ relations: await this.getRelations(event)
532
+ });
533
+ }
534
+
535
+ return details;
536
+ }
537
+ }
538
+ ```
539
+
540
+ **작업 항목**:
541
+ - [ ] searchIndex 메서드 구현
542
+ - [ ] getTimeline 메서드 구현
543
+ - [ ] getDetails 메서드 구현
544
+ - [ ] generateSummary 헬퍼 구현
545
+ - [ ] generatePreview 헬퍼 구현
546
+
547
+ ### 2.2 스마트 검색 구현
548
+
549
+ **파일**: `src/core/progressive-retriever.ts` 계속
550
+
551
+ ```typescript
552
+ async smartSearch(
553
+ query: string,
554
+ options?: SmartSearchOptions
555
+ ): Promise<ProgressiveSearchResult> {
556
+ const config = { ...this.config, ...options };
557
+
558
+ // Layer 1: 항상 실행
559
+ const index = await this.searchIndex(query, {
560
+ topK: config.layer1.topK,
561
+ filter: options?.filter
562
+ });
563
+
564
+ const result: ProgressiveSearchResult = {
565
+ index,
566
+ meta: {
567
+ totalMatches: index.length,
568
+ expandedCount: 0,
569
+ estimatedTokens: this.estimateTokens(index, 'layer1')
570
+ }
571
+ };
572
+
573
+ // 자동 확장 결정
574
+ if (config.autoExpand.enabled) {
575
+ const decision = this.shouldAutoExpand(index, config);
576
+
577
+ if (decision.expandTimeline && decision.ids) {
578
+ result.timeline = await this.getTimeline(decision.ids);
579
+ result.meta.estimatedTokens += this.estimateTokens(result.timeline, 'layer2');
580
+ }
581
+
582
+ if (decision.expandDetails && decision.ids) {
583
+ // 토큰 예산 체크
584
+ const remainingBudget = config.tokenBudget.maxTotalTokens - result.meta.estimatedTokens;
585
+ const idsToExpand = this.selectWithinBudget(decision.ids, remainingBudget);
586
+
587
+ result.details = await this.getDetails(idsToExpand);
588
+ result.meta.expandedCount = idsToExpand.length;
589
+ result.meta.estimatedTokens += this.estimateTokens(result.details, 'layer3');
590
+ }
591
+
592
+ result.meta.expansionReason = decision.reason;
593
+ }
594
+
595
+ return result;
596
+ }
597
+ ```
598
+
599
+ **작업 항목**:
600
+ - [ ] smartSearch 메서드 구현
601
+ - [ ] shouldAutoExpand 로직 구현
602
+ - [ ] selectWithinBudget 토큰 예산 관리
603
+
604
+ ### 2.3 확장 규칙 엔진
605
+
606
+ **파일**: `src/core/expansion-rules.ts` (신규)
607
+
608
+ ```typescript
609
+ interface ExpansionDecision {
610
+ expand: boolean;
611
+ expandTimeline?: boolean;
612
+ expandDetails?: boolean;
613
+ ids?: string[];
614
+ reason: string;
615
+ }
616
+
617
+ export function shouldAutoExpand(
618
+ results: SearchIndexItem[],
619
+ config: ProgressiveDisclosureConfig
620
+ ): ExpansionDecision {
621
+ if (results.length === 0) {
622
+ return { expand: false, reason: 'no_results' };
623
+ }
624
+
625
+ const topScore = results[0].score;
626
+
627
+ // Rule 1: 높은 신뢰도 단일 결과
628
+ if (topScore >= config.autoExpand.highConfidenceThreshold && results.length === 1) {
629
+ return {
630
+ expand: true,
631
+ expandTimeline: true,
632
+ expandDetails: true,
633
+ ids: [results[0].id],
634
+ reason: 'high_confidence_single'
635
+ };
636
+ }
637
+
638
+ // Rule 2: 명확한 1등
639
+ if (results.length >= 2) {
640
+ const gap = results[0].score - results[1].score;
641
+ if (topScore >= 0.85 && gap >= config.autoExpand.scoreGapThreshold) {
642
+ return {
643
+ expand: true,
644
+ expandTimeline: true,
645
+ expandDetails: true,
646
+ ids: [results[0].id],
647
+ reason: 'clear_winner'
648
+ };
649
+ }
650
+ }
651
+
652
+ // Rule 3: 모호한 결과 → 타임라인만
653
+ const highScoreCount = results.filter(r => r.score >= 0.8).length;
654
+ if (highScoreCount >= 3) {
655
+ return {
656
+ expand: true,
657
+ expandTimeline: true,
658
+ expandDetails: false,
659
+ ids: results.slice(0, 3).map(r => r.id),
660
+ reason: 'ambiguous_multiple_high'
661
+ };
662
+ }
663
+
664
+ // Rule 4: 낮은 점수
665
+ return { expand: false, reason: 'low_confidence' };
666
+ }
667
+ ```
668
+
669
+ **작업 항목**:
670
+ - [ ] 확장 규칙 함수 구현
671
+ - [ ] 규칙별 테스트 케이스
672
+
673
+ ## Phase 3: 요약 및 미리보기 생성 (P0)
674
+
675
+ ### 3.1 요약 생성기
676
+
677
+ **파일**: `src/core/summarizer.ts` (신규)
678
+
679
+ ```typescript
680
+ export function generateSummary(content: string, maxLength: number = 100): string {
681
+ // 1. 코드 블록 제거
682
+ const withoutCode = content.replace(/```[\s\S]*?```/g, '[code]');
683
+
684
+ // 2. 첫 문장 추출
685
+ const firstSentence = withoutCode.match(/^[^.!?]+[.!?]/)?.[0] || '';
686
+
687
+ // 3. 길이 제한
688
+ if (firstSentence.length <= maxLength) {
689
+ return firstSentence.trim();
690
+ }
691
+
692
+ // 4. 단어 경계에서 자르기
693
+ return withoutCode.slice(0, maxLength).replace(/\s+\S*$/, '') + '...';
694
+ }
695
+
696
+ export function generatePreview(content: string, maxLength: number = 200): string {
697
+ // 1. 코드 블록 축약
698
+ const withCodeSummary = content.replace(
699
+ /```(\w+)[\s\S]*?```/g,
700
+ (_, lang) => `[${lang} code]`
701
+ );
702
+
703
+ // 2. 줄바꿈 정리
704
+ const singleLine = withCodeSummary.replace(/\n+/g, ' ').trim();
705
+
706
+ // 3. 길이 제한
707
+ if (singleLine.length <= maxLength) {
708
+ return singleLine;
709
+ }
710
+
711
+ return singleLine.slice(0, maxLength).replace(/\s+\S*$/, '') + '...';
712
+ }
713
+ ```
714
+
715
+ **작업 항목**:
716
+ - [ ] generateSummary 함수 구현
717
+ - [ ] generatePreview 함수 구현
718
+ - [ ] 코드 블록 처리 로직
719
+ - [ ] 특수 문자 처리
720
+
721
+ ### 3.2 토큰 추정기
722
+
723
+ **파일**: `src/core/token-estimator.ts` (신규)
724
+
725
+ ```typescript
726
+ // 간단한 토큰 추정 (GPT tokenizer 근사)
727
+ export function estimateTokens(text: string): number {
728
+ // 평균적으로 4자 = 1토큰
729
+ return Math.ceil(text.length / 4);
730
+ }
731
+
732
+ export function estimateLayerTokens(
733
+ items: unknown[],
734
+ layer: 'layer1' | 'layer2' | 'layer3',
735
+ config: ProgressiveDisclosureConfig
736
+ ): number {
737
+ switch (layer) {
738
+ case 'layer1':
739
+ return items.length * config.tokenBudget.layer1PerItem;
740
+ case 'layer2':
741
+ return items.length * config.tokenBudget.layer2PerItem;
742
+ case 'layer3':
743
+ // Layer 3는 실제 콘텐츠 기반
744
+ return (items as FullDetail[]).reduce(
745
+ (sum, item) => sum + estimateTokens(item.content),
746
+ 0
747
+ );
748
+ }
749
+ }
750
+ ```
751
+
752
+ **작업 항목**:
753
+ - [ ] estimateTokens 함수 구현
754
+ - [ ] estimateLayerTokens 함수 구현
755
+
756
+ ## Phase 4: 캐싱 (P1)
757
+
758
+ ### 4.1 캐시 매니저
759
+
760
+ **파일**: `src/core/cache-manager.ts` (신규)
761
+
762
+ ```typescript
763
+ import { LRUCache } from 'lru-cache';
764
+
765
+ export class ProgressiveCache {
766
+ private layer1Cache: LRUCache<string, SearchIndexItem[]>;
767
+ private layer2Cache: LRUCache<string, TimelineItem[]>;
768
+ private layer3Cache: LRUCache<string, FullDetail>;
769
+
770
+ constructor(config: ProgressiveDisclosureConfig) {
771
+ this.layer1Cache = new LRUCache({
772
+ max: 100,
773
+ ttl: config.cache.layer1Ttl
774
+ });
775
+
776
+ this.layer2Cache = new LRUCache({
777
+ max: 500,
778
+ ttl: config.cache.layer2Ttl
779
+ });
780
+
781
+ this.layer3Cache = new LRUCache({
782
+ max: 200,
783
+ ttl: config.cache.layer3Ttl
784
+ });
785
+ }
786
+
787
+ // Layer 1 캐시
788
+ getLayer1(query: string, options: SearchOptions): SearchIndexItem[] | undefined {
789
+ const key = this.buildLayer1Key(query, options);
790
+ return this.layer1Cache.get(key);
791
+ }
792
+
793
+ setLayer1(query: string, options: SearchOptions, results: SearchIndexItem[]): void {
794
+ const key = this.buildLayer1Key(query, options);
795
+ this.layer1Cache.set(key, results);
796
+ }
797
+
798
+ // ... Layer 2, 3 유사 구현
799
+ }
800
+ ```
801
+
802
+ **작업 항목**:
803
+ - [ ] ProgressiveCache 클래스 구현
804
+ - [ ] Layer별 캐시 키 생성
805
+ - [ ] TTL 및 크기 제한 적용
806
+
807
+ ## Phase 5: 포맷터 (P0)
808
+
809
+ ### 5.1 컨텍스트 포맷터
810
+
811
+ **파일**: `src/core/context-formatter.ts` (신규)
812
+
813
+ ```typescript
814
+ export class ContextFormatter {
815
+ formatProgressiveResult(result: ProgressiveSearchResult): string {
816
+ const parts: string[] = [];
817
+
818
+ // Layer 1: 항상 포함
819
+ parts.push(this.formatLayer1(result.index));
820
+
821
+ // Layer 2: 타임라인
822
+ if (result.timeline) {
823
+ parts.push(this.formatLayer2(result.timeline));
824
+ }
825
+
826
+ // Layer 3: 상세
827
+ if (result.details) {
828
+ parts.push(this.formatLayer3(result.details));
829
+ }
830
+
831
+ // 메타 정보
832
+ parts.push(this.formatMeta(result.meta));
833
+
834
+ return parts.join('\n\n');
835
+ }
836
+
837
+ private formatLayer1(items: SearchIndexItem[]): string {
838
+ const header = `## Related Memories (${items.length} matches)\n`;
839
+ const table = items.map((item, i) =>
840
+ `${i + 1}. [${item.id}] ${item.summary} (score: ${item.score.toFixed(2)})`
841
+ ).join('\n');
842
+
843
+ return header + table;
844
+ }
845
+
846
+ private formatLayer2(items: TimelineItem[]): string {
847
+ const header = '## Timeline Context\n';
848
+ const timeline = items.map(item => {
849
+ const marker = item.isTarget ? '**→**' : ' ';
850
+ const time = item.timestamp.toLocaleTimeString();
851
+ return `${marker} ${time}: ${item.preview}`;
852
+ }).join('\n');
853
+
854
+ return header + timeline;
855
+ }
856
+
857
+ private formatLayer3(items: FullDetail[]): string {
858
+ return items.map(item => {
859
+ const header = `## Detail: ${item.id}\n`;
860
+ const meta = `*Session: ${item.sessionId} | ${item.timestamp.toLocaleDateString()}*\n`;
861
+ return header + meta + '\n' + item.content;
862
+ }).join('\n\n---\n\n');
863
+ }
864
+ }
865
+ ```
866
+
867
+ **작업 항목**:
868
+ - [ ] ContextFormatter 클래스 구현
869
+ - [ ] Layer별 포맷 함수
870
+ - [ ] Markdown 출력 최적화
871
+
872
+ ## Phase 6: 통합 (P0)
873
+
874
+ ### 6.1 Retriever 교체
875
+
876
+ **파일**: `src/core/retriever.ts` 수정
877
+
878
+ ```typescript
879
+ export class Retriever {
880
+ private progressiveRetriever: ProgressiveRetriever;
881
+
882
+ constructor(/* ... */) {
883
+ this.progressiveRetriever = new ProgressiveRetriever(
884
+ this.eventStore,
885
+ this.vectorStore,
886
+ config.progressiveDisclosure
887
+ );
888
+ }
889
+
890
+ // 기존 메서드를 progressive로 위임
891
+ async search(query: string): Promise<SearchResult[]> {
892
+ if (this.config.progressiveDisclosure?.enabled) {
893
+ const result = await this.progressiveRetriever.smartSearch(query);
894
+ return this.convertToLegacyFormat(result);
895
+ }
896
+
897
+ // 기존 로직 유지 (fallback)
898
+ return this.legacySearch(query);
899
+ }
900
+
901
+ // 새로운 progressive 검색
902
+ async progressiveSearch(query: string, options?: SmartSearchOptions): Promise<ProgressiveSearchResult> {
903
+ return this.progressiveRetriever.smartSearch(query, options);
904
+ }
905
+ }
906
+ ```
907
+
908
+ **작업 항목**:
909
+ - [ ] Retriever에 progressiveRetriever 통합
910
+ - [ ] 기존 API 호환성 유지
911
+ - [ ] 새 API 추가
912
+
913
+ ### 6.2 user-prompt-submit 훅 수정
914
+
915
+ **파일**: `src/hooks/user-prompt-submit.ts` 수정
916
+
917
+ ```typescript
918
+ async function handleUserPromptSubmit(input: UserPromptInput): Promise<HookOutput> {
919
+ const memoryService = await MemoryService.getInstance();
920
+ const config = memoryService.getConfig();
921
+
922
+ // Progressive 검색 사용
923
+ const searchResult = await memoryService.progressiveSearch(input.prompt, {
924
+ maxTotalTokens: config.retrieval.maxTokens
925
+ });
926
+
927
+ // 포맷팅
928
+ const formatter = new ContextFormatter();
929
+ const context = formatter.formatProgressiveResult(searchResult);
930
+
931
+ return {
932
+ context,
933
+ meta: {
934
+ matchCount: searchResult.meta.totalMatches,
935
+ expandedCount: searchResult.meta.expandedCount,
936
+ estimatedTokens: searchResult.meta.estimatedTokens
937
+ }
938
+ };
939
+ }
940
+ ```
941
+
942
+ **작업 항목**:
943
+ - [ ] 훅에서 progressive 검색 사용
944
+ - [ ] 메타 정보 반환
945
+
946
+ ## 파일 목록
947
+
948
+ ### 신규 파일
949
+ ```
950
+ src/core/progressive-retriever.ts # 메인 검색 클래스
951
+ src/core/expansion-rules.ts # 확장 규칙 엔진
952
+ src/core/summarizer.ts # 요약/미리보기 생성
953
+ src/core/token-estimator.ts # 토큰 추정
954
+ src/core/cache-manager.ts # 캐시 관리
955
+ src/core/context-formatter.ts # 출력 포맷팅
956
+ ```
957
+
958
+ ### 수정 파일
959
+ ```
960
+ src/core/types.ts # 스키마 추가
961
+ src/core/retriever.ts # Progressive 통합
962
+ src/hooks/user-prompt-submit.ts # 검색 방식 변경
963
+ src/services/memory-service.ts # 서비스 메서드 추가
964
+ ```
965
+
966
+ ## 테스트
967
+
968
+ ### 필수 테스트 케이스
969
+
970
+ 1. **Layer 1 검색**
971
+ ```typescript
972
+ test('should return index with summaries', async () => {
973
+ const result = await retriever.searchIndex('DuckDB 스키마');
974
+ expect(result[0]).toHaveProperty('summary');
975
+ expect(result[0].summary.length).toBeLessThanOrEqual(100);
976
+ });
977
+ ```
978
+
979
+ 2. **자동 확장 규칙**
980
+ ```typescript
981
+ test('should expand on high confidence', async () => {
982
+ const result = await retriever.smartSearch('unique term');
983
+ expect(result.details).toBeDefined();
984
+ expect(result.meta.expansionReason).toBe('high_confidence_single');
985
+ });
986
+ ```
987
+
988
+ 3. **토큰 예산**
989
+ ```typescript
990
+ test('should respect token budget', async () => {
991
+ const result = await retriever.smartSearch('query', { maxTotalTokens: 500 });
992
+ expect(result.meta.estimatedTokens).toBeLessThanOrEqual(500);
993
+ });
994
+ ```
995
+
996
+ 4. **캐싱**
997
+ ```typescript
998
+ test('should use cache on repeat query', async () => {
999
+ await retriever.searchIndex('test');
1000
+ const start = Date.now();
1001
+ await retriever.searchIndex('test');
1002
+ expect(Date.now() - start).toBeLessThan(10);
1003
+ });
1004
+ ```
1005
+
1006
+ ## 마일스톤
1007
+
1008
+ | 단계 | 완료 기준 |
1009
+ |------|----------|
1010
+ | M1 | 타입 정의 완료 |
1011
+ | M2 | ProgressiveRetriever 기본 구현 |
1012
+ | M3 | 확장 규칙 엔진 |
1013
+ | M4 | 요약/미리보기 생성 |
1014
+ | M5 | 토큰 예산 관리 |
1015
+ | M6 | 캐싱 구현 |
1016
+ | M7 | 포맷터 및 통합 |
1017
+ | M8 | 테스트 통과 |
1018
+
1019
+ ## 2026-02-25T12:31:26.413Z | 8c526a4e-bfe7-4bdc-86c8-f16fe06f0faf
1020
+ - type: session_summary
1021
+ - session: import:organized
1022
+ # Progressive Disclosure Specification
1023
+
1024
+ > **Version**: 1.0.0
1025
+ > **Status**: Draft
1026
+ > **Created**: 2026-02-01
1027
+ > **Reference**: claude-mem (thedotmack/claude-mem)
1028
+
1029
+ ## 1. 개요
1030
+
1031
+ ### 1.1 문제 정의
1032
+
1033
+ 현재 시스템에서 메모리 검색 시 토큰 비효율 발생:
1034
+
1035
+ 1. **전체 로드 문제**: 검색 결과를 한 번에 모든 내용을 가져옴
1036
+ 2. **토큰 낭비**: 관련 없는 내용도 컨텍스트에 포함
1037
+ 3. **컨텍스트 한계**: 대용량 메모리 사용 시 토큰 초과
1038
+
1039
+ ### 1.2 해결 방향
1040
+
1041
+ **3-Layer Progressive Disclosure**:
1042
+ - Layer 1: 검색 인덱스 (ID + 요약) - 최소 토큰
1043
+ - Layer 2: 타임라인 컨텍스트 - 시간순 맥락
1044
+ - Layer 3: 상세 정보 - 선택된 항목만 전체 로드
1045
+
1046
+ ## 2. 핵심 개념
1047
+
1048
+ ### 2.1 3-Layer 아키텍처
1049
+
1050
+ ```
1051
+ ┌─────────────────────────────────────────────────────────────┐
1052
+ │ User Query │
1053
+ └─────────────────────────────────────────────────────────────┘
1054
+
1055
+
1056
+ ┌─────────────────────────────────────────────────────────────┐
1057
+ │ Layer 1: Search Index (~50-100 tokens per result) │
1058
+ │ ┌──────────────────────────────────────────────────────┐ │
1059
+ │ │ { id: "mem_1", summary: "파일 구조 설명", score: 0.95 } │ │
1060
+ │ │ { id: "mem_2", summary: "타입 정의 논의", score: 0.87 } │ │
1061
+ │ │ { id: "mem_3", summary: "버그 수정 방법", score: 0.82 } │ │
1062
+ │ └──────────────────────────────────────────────────────┘ │
1063
+ └─────────────────────────────────────────────────────────────┘
1064
+
1065
+ (선택적 확장)
1066
+
1067
+ ┌─────────────────────────────────────────────────────────────┐
1068
+ │ Layer 2: Timeline Context (~200 tokens) │
1069
+ │ ┌──────────────────────────────────────────────────────┐ │
1070
+ │ │ 2026-01-30 14:00: "파일 구조 변경 결정" │ │
1071
+ │ │ 2026-01-30 14:15: "types.ts 분리" ← mem_1 │ │
1072
+ │ │ 2026-01-30 14:30: "테스트 작성" │ │
1073
+ │ └──────────────────────────────────────────────────────┘ │
1074
+ └─────────────────────────────────────────────────────────────┘
1075
+
1076
+ (필요 시만)
1077
+
1078
+ ┌─────────────────────────────────────────────────────────────┐
1079
+ │ Layer 3: Full Details (~500-1000 tokens per result) │
1080
+ │ ┌──────────────────────────────────────────────────────┐ │
1081
+ │ │ mem_1: { │ │
1082
+ │ │ content: "전체 대화 내용...", │ │
1083
+ │ │ metadata: {...}, │ │
1084
+ │ │ evidence: [...] │ │
1085
+ │ │ } │ │
1086
+ │ └──────────────────────────────────────────────────────┘ │
1087
+ └─────────────────────────────────────────────────────────────┘
1088
+ ```
1089
+
1090
+ ### 2.2 토큰 효율성
1091
+
1092
+ | 방식 | 5개 결과 토큰 | 20개 결과 토큰 |
1093
+ |------|--------------|---------------|
1094
+ | 기존 (전체 로드) | ~5,000 | ~20,000 |
1095
+ | Progressive L1 | ~500 | ~2,000 |
1096
+ | Progressive L1+L2 | ~700 | ~2,200 |
1097
+ | Progressive L1+L2+L3 (2개) | ~1,700 | ~2,200 |
1098
+
1099
+ **예상 토큰 절약: ~10배**
1100
+
1101
+ ### 2.3 확장 트리거
1102
+
1103
+ ```typescript
1104
+ type ExpansionTrigger =
1105
+ | 'high_confidence' // score ≥ 0.92 → 자동 L3 확장
1106
+ | 'user_request' // "자세히 알려줘" → L3 확장
1107
+ | 'temporal_proximity' // 시간적 근접 → L2 확장
1108
+ | 'explicit_id' // "mem_1 보여줘" → L3 확장
1109
+ | 'ambiguity'; // 여러 유사 결과 → L2 확장
1110
+ ```
1111
+
1112
+ ## 3. 데이터 스키마
1113
+
1114
+ ### 3.1 Layer 1: SearchIndex
1115
+
1116
+ ```typescript
1117
+ const SearchIndexItemSchema = z.object({
1118
+ id: z.string(), // 이벤트/메모리 ID
1119
+ summary: z.string().max(100), // 한 줄 요약
1120
+ score: z.number(), // 유사도 점수
1121
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
1122
+ timestamp: z.date(),
1123
+ sessionId: z.string()
1124
+ });
1125
+
1126
+ type SearchIndexItem = z.infer<typeof SearchIndexItemSchema>;
1127
+
1128
+ // 반환 예시
1129
+ {
1130
+ id: "evt_abc123",
1131
+ summary: "DuckDB 스키마 설계 논의",
1132
+ score: 0.94,
1133
+ type: "response",
1134
+ timestamp: "2026-01-30T14:00:00Z",
1135
+ sessionId: "session_xyz"
1136
+ }
1137
+ ```
1138
+
1139
+ ### 3.2 Layer 2: TimelineContext
1140
+
1141
+ ```typescript
1142
+ const TimelineItemSchema = z.object({
1143
+ id: z.string(),
1144
+ timestamp: z.date(),
1145
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
1146
+ preview: z.string().max(200), // 2-3문장 미리보기
1147
+ isTarget: z.boolean() // 검색 결과에 해당하는지
1148
+ });
1149
+
1150
+ type TimelineItem = z.infer<typeof TimelineItemSchema>;
1151
+
1152
+ // 반환 예시 (target ID 주변 ±3개)
1153
+ [
1154
+ { id: "evt_1", preview: "이전 대화...", isTarget: false },
1155
+ { id: "evt_2", preview: "관련 질문...", isTarget: false },
1156
+ { id: "evt_abc123", preview: "DuckDB 스키마...", isTarget: true }, // 타겟
1157
+ { id: "evt_3", preview: "후속 논의...", isTarget: false }
1158
+ ]
1159
+ ```
1160
+
1161
+ ### 3.3 Layer 3: FullDetail
1162
+
1163
+ ```typescript
1164
+ const FullDetailSchema = z.object({
1165
+ id: z.string(),
1166
+ content: z.string(), // 전체 내용
1167
+ type: z.enum(['prompt', 'response', 'tool', 'insight']),
1168
+ timestamp: z.date(),
1169
+ sessionId: z.string(),
1170
+
1171
+ // 메타데이터
1172
+ metadata: z.object({
1173
+ tokenCount: z.number(),
1174
+ hasCode: z.boolean(),
1175
+ files: z.array(z.string()).optional(),
1176
+ tools: z.array(z.string()).optional()
1177
+ }),
1178
+
1179
+ // 관계 정보
1180
+ relations: z.object({
1181
+ parentId: z.string().optional(),
1182
+ childIds: z.array(z.string()),
1183
+ relatedIds: z.array(z.string())
1184
+ }).optional()
1185
+ });
1186
+
1187
+ type FullDetail = z.infer<typeof FullDetailSchema>;
1188
+ ```
1189
+
1190
+ ## 4. API 인터페이스
1191
+
1192
+ ### 4.1 ProgressiveRetriever
1193
+
1194
+ ```typescript
1195
+ interface ProgressiveRetriever {
1196
+ // Layer 1: 검색 인덱스 반환
1197
+ searchIndex(
1198
+ query: string,
1199
+ options?: {
1200
+ topK?: number;
1201
+ filter?: SearchFilter;
1202
+ }
1203
+ ): Promise<SearchIndexItem[]>;
1204
+
1205
+ // Layer 2: 타임라인 컨텍스트 반환
1206
+ getTimeline(
1207
+ targetIds: string[],
1208
+ options?: {
1209
+ windowSize?: number; // 앞뒤로 몇 개씩
1210
+ }
1211
+ ): Promise<TimelineItem[]>;
1212
+
1213
+ // Layer 3: 상세 정보 반환
1214
+ getDetails(ids: string[]): Promise<FullDetail[]>;
1215
+
1216
+ // 편의 메서드: 자동 확장
1217
+ smartSearch(
1218
+ query: string,
1219
+ options?: SmartSearchOptions
1220
+ ): Promise<ProgressiveSearchResult>;
1221
+ }
1222
+ ```
1223
+
1224
+ ### 4.2 SmartSearch 옵션
1225
+
1226
+ ```typescript
1227
+ interface SmartSearchOptions {
1228
+ // Layer 1 설정
1229
+ topK: number; // 기본: 10
1230
+ minScore: number; // 기본: 0.7
1231
+
1232
+ // 자동 확장 설정
1233
+ autoExpandTimeline: boolean; // 기본: true (score gap 클 때)
1234
+ autoExpandDetails: boolean; // 기본: true (score ≥ 0.92)
1235
+ maxAutoExpandCount: number; // 기본: 3
1236
+
1237
+ // 토큰 제한
1238
+ maxTotalTokens: number; // 기본: 2000
1239
+ }
1240
+ ```
1241
+
1242
+ ### 4.3 ProgressiveSearchResult
1243
+
1244
+ ```typescript
1245
+ interface ProgressiveSearchResult {
1246
+ // Layer 1 (항상 포함)
1247
+ index: SearchIndexItem[];
1248
+
1249
+ // Layer 2 (선택적)
1250
+ timeline?: TimelineItem[];
1251
+
1252
+ // Layer 3 (선택적)
1253
+ details?: FullDetail[];
1254
+
1255
+ // 메타정보
1256
+ meta: {
1257
+ totalMatches: number;
1258
+ expandedCount: number;
1259
+ estimatedTokens: number;
1260
+ expansionReason?: string;
1261
+ };
1262
+ }
1263
+ ```
1264
+
1265
+ ## 5. 확장 규칙
1266
+
1267
+ ### 5.1 자동 확장 조건
1268
+
1269
+ ```typescript
1270
+ function shouldAutoExpand(results: SearchIndexItem[]): ExpansionDecision {
1271
+ // Rule 1: 높은 신뢰도 단일 결과
1272
+ if (results[0]?.score >= 0.92 && results.length === 1) {
1273
+ return { expand: true, ids: [results[0].id], reason: 'high_confidence' };
1274
+ }
1275
+
1276
+ // Rule 2: 명확한 1등 (2등과 gap이 큼)
1277
+ if (results.length >= 2) {
1278
+ const gap = results[0].score - results[1].score;
1279
+ if (results[0].score >= 0.85 && gap >= 0.1) {
1280
+ return { expand: true, ids: [results[0].id], reason: 'clear_winner' };
1281
+ }
1282
+ }
1283
+
1284
+ // Rule 3: 모호한 결과 → 타임라인만 확장
1285
+ if (results.length >= 3 && results[2].score >= 0.8) {
1286
+ return {
1287
+ expand: true,
1288
+ expandTimeline: true,
1289
+ expandDetails: false,
1290
+ ids: results.slice(0, 3).map(r => r.id),
1291
+ reason: 'ambiguous_results'
1292
+ };
1293
+ }
1294
+
1295
+ // Rule 4: 낮은 점수 → 확장 안 함
1296
+ return { expand: false, reason: 'low_confidence' };
1297
+ }
1298
+ ```
1299
+
1300
+ ### 5.2 토큰 예산 관리
1301
+
1302
+ ```typescript
1303
+ function expandWithinBudget(
1304
+ index: SearchIndexItem[],
1305
+ budget: number
1306
+ ): ProgressiveSearchResult {
1307
+ let usedTokens = estimateTokens(index); // ~50-100 per item
1308
+ const result: ProgressiveSearchResult = { index, meta: { ... } };
1309
+
1310
+ // 예산 내에서 확장
1311
+ const sortedByScore = [...index].sort((a, b) => b.score - a.score);
1312
+
1313
+ for (const item of sortedByScore) {
1314
+ if (usedTokens >= budget) break;
1315
+
1316
+ // 타임라인 추가 (~200 tokens)
1317
+ if (usedTokens + 200 <= budget && !result.timeline) {
1318
+ result.timeline = await getTimeline([item.id]);
1319
+ usedTokens += estimateTokens(result.timeline);
1320
+ }
1321
+
1322
+ // 상세 추가 (~500-1000 tokens)
1323
+ if (item.score >= 0.85 && usedTokens + 800 <= budget) {
1324
+ const detail = await getDetails([item.id]);
1325
+ result.details = [...(result.details || []), ...detail];
1326
+ usedTokens += estimateTokens(detail);
1327
+ }
1328
+ }
1329
+
1330
+ result.meta.estimatedTokens = usedTokens;
1331
+ return result;
1332
+ }
1333
+ ```
1334
+
1335
+ ## 6. 컨텍스트 포맷
1336
+
1337
+ ### 6.1 Layer 1 포맷 (최소)
1338
+
1339
+ ```markdown
1340
+ ## Related Memories (5 matches)
1341
+
1342
+ | ID | Summary | Score |
1343
+ |----|---------|-------|
1344
+ | mem_1 | DuckDB 스키마 설계 논의 | 0.94 |
1345
+ | mem_2 | 타입 시스템 리팩토링 | 0.87 |
1346
+ | mem_3 | 벡터 저장소 설정 | 0.82 |
1347
+ | mem_4 | 테스트 코드 작성 | 0.78 |
1348
+ | mem_5 | CI/CD 파이프라인 | 0.75 |
1349
+
1350
+ *Use "show mem_1" for details*
1351
+ ```
1352
+
1353
+ ### 6.2 Layer 2 포맷 (타임라인)
1354
+
1355
+ ```markdown
1356
+ ## Related Memories with Timeline
1357
+
1358
+ ### Context around mem_1 (2026-01-30)
1359
+
1360
+ 14:00 - User: "DB 스키마를 어떻게 설계할까?"
1361
+ 14:05 - **[mem_1]** Assistant: "DuckDB를 사용하여 이벤트 소싱 패턴..."
1362
+ 14:15 - User: "인덱스는 어떻게?"
1363
+ 14:20 - Assistant: "다음 인덱스들을 추천..."
1364
+ ```
1365
+
1366
+ ### 6.3 Layer 3 포맷 (상세)
1367
+
1368
+ ```markdown
1369
+ ## Memory Detail: mem_1
1370
+
1371
+ **Session**: session_xyz | **Date**: 2026-01-30 14:05
1372
+
1373
+ ### Content
1374
+ DuckDB를 사용하여 이벤트 소싱 패턴을 구현하는 방법을 설명드립니다.
1375
+
1376
+ 1. events 테이블 생성:
1377
+ \`\`\`sql
1378
+ CREATE TABLE events (
1379
+ event_id VARCHAR PRIMARY KEY,
1380
+ ...
1381
+ );
1382
+ \`\`\`
1383
+
1384
+ 2. 인덱스 설계:
1385
+ - event_type별 인덱스
1386
+ - session_id별 인덱스
1387
+ ...
1388
+
1389
+ **Related Files**: src/core/event-store.ts, src/core/types.ts
1390
+ **Tools Used**: Read, Write
1391
+ ```
1392
+
1393
+ ## 7. 캐싱 전략
1394
+
1395
+ ### 7.1 Layer별 캐싱
1396
+
1397
+ ```typescript
1398
+ interface CacheConfig {
1399
+ layer1: {
1400
+ ttl: 60 * 1000, // 1분 (검색 결과)
1401
+ maxSize: 100 // 최근 100개 쿼리
1402
+ };
1403
+ layer2: {
1404
+ ttl: 5 * 60 * 1000, // 5분 (타임라인)
1405
+ maxSize: 500
1406
+ };
1407
+ layer3: {
1408
+ ttl: 30 * 60 * 1000, // 30분 (상세 정보)
1409
+ maxSize: 200
1410
+ };
1411
+ }
1412
+ ```
1413
+
1414
+ ### 7.2 캐시 키
1415
+
1416
+ ```typescript
1417
+ function getCacheKey(layer: number, params: unknown): string {
1418
+ switch (layer) {
1419
+ case 1:
1420
+ return `l1:${hash(params.query)}:${params.topK}`;
1421
+ case 2:
1422
+ return `l2:${params.targetIds.sort().join(',')}`;
1423
+ case 3:
1424
+ return `l3:${params.id}`;
1425
+ }
1426
+ }
1427
+ ```
1428
+
1429
+ ## 8. 성공 기준
1430
+
1431
+ - [ ] Layer 1 검색이 100ms 이내 반환
1432
+ - [ ] Layer 2 타임라인이 200ms 이내 반환
1433
+ - [ ] Layer 3 상세가 500ms 이내 반환
1434
+ - [ ] 평균 토큰 사용량이 기존 대비 50% 이상 감소
1435
+ - [ ] 자동 확장이 적절한 경우에만 동작
1436
+ - [ ] 토큰 예산 내에서 최적의 결과 제공