claude-session-continuity-mcp 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/dist/hooks/session-end.js +31 -0
- package/dist/hooks/session-start.js +97 -59
- package/dist/index.js +46 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# claude-session-continuity-mcp (v1.
|
|
1
|
+
# claude-session-continuity-mcp (v1.12.1)
|
|
2
2
|
|
|
3
|
-
> **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search
|
|
3
|
+
> **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search + auto error→solution pipeline
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-session-continuity-mcp)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -27,7 +27,7 @@ Every new Claude Code session:
|
|
|
27
27
|
```bash
|
|
28
28
|
# Session start → Auto-loads relevant context + recent session history
|
|
29
29
|
# When asking → Auto-injects relevant memories/solutions
|
|
30
|
-
# During conversation → Tracks active files
|
|
30
|
+
# During conversation → Tracks active files + auto-injects error solutions
|
|
31
31
|
# On compact → Structured handover context for continuity
|
|
32
32
|
# On exit → Extracts commits, decisions, error-fix pairs from transcript
|
|
33
33
|
```
|
|
@@ -108,7 +108,7 @@ npm install claude-session-continuity-mcp
|
|
|
108
108
|
|------|---------|----------|
|
|
109
109
|
| `SessionStart` | `claude-hook-session-start` | Auto-loads project context on session start |
|
|
110
110
|
| `UserPromptSubmit` | `claude-hook-user-prompt` | Auto-injects relevant memories + past reference search |
|
|
111
|
-
| `PostToolUse` | `claude-hook-post-tool` | Tracks active files (Edit, Write)
|
|
111
|
+
| `PostToolUse` | `claude-hook-post-tool` | Tracks active files (Edit, Write) + auto-injects error solutions (Bash) |
|
|
112
112
|
| `PreCompact` | `claude-hook-pre-compact` | Structured handover context before compression |
|
|
113
113
|
| `Stop` | `claude-hook-session-end` | Extracts commits, decisions, error-fix pairs from transcript |
|
|
114
114
|
|
|
@@ -144,7 +144,10 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
144
144
|
| 📊 **Memory Classification** | 5 types: observation, decision, learning, error, pattern |
|
|
145
145
|
| ✅ **Integrated Verification** | One-click build/test/lint execution |
|
|
146
146
|
| 📋 **Task Management** | Priority-based task management |
|
|
147
|
-
| 🔧 **Solution
|
|
147
|
+
| 🔧 **Auto Error→Solution** | **(v1.12.0)** Bash errors auto-detect → inject past solutions; session-end auto-records error-fix pairs |
|
|
148
|
+
| 💰 **Token Efficiency** | **(v1.11.0)** Removed loadContext from UserPromptSubmit (saves 24-60K tokens/session) |
|
|
149
|
+
| 📑 **Progressive Disclosure** | **(v1.11.0)** memory_search returns index first, memory_get for full content |
|
|
150
|
+
| ⏳ **Temporal Decay** | **(v1.11.0)** Memory scoring with type-specific half-lives for relevance |
|
|
148
151
|
| 📝 **Structured Handover** | **(v1.10.0)** PreCompact saves work summary, active files, pending actions |
|
|
149
152
|
| 🚪 **Smart Session End** | **(v1.10.0)** Extracts commits, decisions, error-fix pairs from transcript |
|
|
150
153
|
| 🗑️ **Auto Noise Cleanup** | **(v1.10.0)** Auto-deletes stale observation memories (3d+) |
|
|
@@ -165,10 +168,12 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
165
168
|
|
|
166
169
|
**UserPromptSubmit Hook** (`npx claude-hook-user-prompt`):
|
|
167
170
|
- Runs on every prompt submission
|
|
171
|
+
- **(v1.11.0)** No longer calls loadContext() — saves 24-60K tokens/session
|
|
168
172
|
- Injects relevant context (filtered: decisions, learnings, errors only)
|
|
169
173
|
|
|
170
174
|
**PostToolUse Hook** (`npx claude-hook-post-tool`):
|
|
171
175
|
- Tracks hot file paths and updates `active_context.recent_files`
|
|
176
|
+
- **(v1.12.0)** Auto-detects Bash errors → searches solutions DB → injects past solutions into context
|
|
172
177
|
- **No longer creates observation memories** (v1.10.0 — eliminates `[File Change]` noise)
|
|
173
178
|
|
|
174
179
|
**PreCompact Hook** (`npx claude-hook-pre-compact`):
|
|
@@ -178,7 +183,9 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
178
183
|
**Stop Hook** (`npx claude-hook-session-end`):
|
|
179
184
|
- Extracts commit messages from JSONL transcript (`git commit -m` patterns)
|
|
180
185
|
- Extracts error-fix pairs (error → resolution within 3 messages)
|
|
186
|
+
- **(v1.12.0)** Auto-records error→fix pairs to solutions table for future reuse
|
|
181
187
|
- Extracts decisions ("because", "instead of", "chose" patterns)
|
|
188
|
+
- **(v1.11.0)** Single-pass transcript parsing (4 JSONL reads → 1)
|
|
182
189
|
- Stores structured metadata in `sessions.issues` column as JSON
|
|
183
190
|
|
|
184
191
|
### Example Output (Session Start)
|
|
@@ -280,7 +287,7 @@ Now we use `npm exec --`:
|
|
|
280
287
|
|
|
281
288
|
---
|
|
282
289
|
|
|
283
|
-
## Tools (v5 API) -
|
|
290
|
+
## Tools (v5 API) - 25 Focused Tools
|
|
284
291
|
|
|
285
292
|
### 1. Session Lifecycle (4) ⭐
|
|
286
293
|
|
|
@@ -363,7 +370,7 @@ verify_test({ project: "my-app" })
|
|
|
363
370
|
verify_all({ project: "my-app" })
|
|
364
371
|
```
|
|
365
372
|
|
|
366
|
-
### 6. Memory System (
|
|
373
|
+
### 6. Memory System (5)
|
|
367
374
|
|
|
368
375
|
```javascript
|
|
369
376
|
// Store a classified memory
|
|
@@ -376,7 +383,7 @@ memory_store({
|
|
|
376
383
|
relatedTo: 23 // Connect to existing memory
|
|
377
384
|
})
|
|
378
385
|
|
|
379
|
-
// Search memories (
|
|
386
|
+
// Search memories — returns index (id, type, tags, score) for token efficiency
|
|
380
387
|
memory_search({
|
|
381
388
|
query: "state management test",
|
|
382
389
|
type: "learning",
|
|
@@ -384,6 +391,9 @@ memory_search({
|
|
|
384
391
|
limit: 10
|
|
385
392
|
})
|
|
386
393
|
|
|
394
|
+
// Get full memory content by ID (v1.11.0)
|
|
395
|
+
memory_get({ memoryId: 23 })
|
|
396
|
+
|
|
387
397
|
// Find related memories (graph + semantic)
|
|
388
398
|
memory_related({
|
|
389
399
|
memoryId: 23,
|
|
@@ -528,6 +538,14 @@ npm run test:coverage
|
|
|
528
538
|
- [x] Smart session-end: commit/decision/error-fix extraction from transcript (v1.10.0)
|
|
529
539
|
- [x] Auto noise cleanup (3d+ observations, 14d+ auto-compact) (v1.10.0)
|
|
530
540
|
- [x] 3 recent sessions display with structured metadata (v1.10.0)
|
|
541
|
+
- [x] Token efficiency — remove loadContext from UserPromptSubmit, saves 24-60K tokens/session (v1.11.0)
|
|
542
|
+
- [x] Single-pass transcript parsing, 4 JSONL reads → 1 (v1.11.0)
|
|
543
|
+
- [x] Temporal decay for memory scoring with type-specific half-lives (v1.11.0)
|
|
544
|
+
- [x] Progressive disclosure — memory_search returns index, memory_get for full content (v1.11.0)
|
|
545
|
+
- [x] Memory consolidation via Jaccard similarity (v1.11.0)
|
|
546
|
+
- [x] Auto error→solution pipeline — PostToolUse detects Bash errors, injects past solutions (v1.12.0)
|
|
547
|
+
- [x] SessionEnd auto-records error-fix pairs to solutions table (v1.12.0)
|
|
548
|
+
- [x] Cross-project solution search with current project prioritization (v1.12.0)
|
|
531
549
|
- [ ] sqlite-vec native vector search (v2 - when data > 1000 records)
|
|
532
550
|
- [ ] Web dashboard
|
|
533
551
|
- [ ] Cloud sync option
|
|
@@ -567,6 +567,37 @@ async function main() {
|
|
|
567
567
|
}
|
|
568
568
|
catch { /* solutions table may not exist */ }
|
|
569
569
|
}
|
|
570
|
+
// architecture_decisions 자동 누적 (세션에서 추출된 결정사항 병합)
|
|
571
|
+
if (decisions.length > 0) {
|
|
572
|
+
try {
|
|
573
|
+
const existing = db.prepare('SELECT architecture_decisions FROM project_context WHERE project = ?').get(project);
|
|
574
|
+
let existingDecisions = [];
|
|
575
|
+
if (existing?.architecture_decisions) {
|
|
576
|
+
try {
|
|
577
|
+
existingDecisions = JSON.parse(existing.architecture_decisions);
|
|
578
|
+
}
|
|
579
|
+
catch { /* ignore */ }
|
|
580
|
+
}
|
|
581
|
+
// 중복 제거 후 병합 (최대 20개 유지)
|
|
582
|
+
const merged = [...new Set([...existingDecisions, ...decisions])].slice(-20);
|
|
583
|
+
db.prepare(`
|
|
584
|
+
INSERT INTO project_context (project, architecture_decisions)
|
|
585
|
+
VALUES (?, ?)
|
|
586
|
+
ON CONFLICT(project) DO UPDATE SET architecture_decisions = ?
|
|
587
|
+
`).run(project, JSON.stringify(merged), JSON.stringify(merged));
|
|
588
|
+
}
|
|
589
|
+
catch { /* project_context table may not exist */ }
|
|
590
|
+
}
|
|
591
|
+
// 세션 임베딩 사전 생성 (search_sessions 성능 최적화)
|
|
592
|
+
try {
|
|
593
|
+
const lastSession = db.prepare('SELECT id FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
594
|
+
if (lastSession && lastWork) {
|
|
595
|
+
// 간단한 임베딩은 동기적으로 시도하지 않고 DB에 표시만 남김
|
|
596
|
+
// MCP 서버의 generateEmbedding이 search 시 캐시 miss에서 lazy 생성
|
|
597
|
+
// session-end 훅은 transformers 모델 로드 오버헤드가 크므로 skip
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch { /* ignore */ }
|
|
570
601
|
db.close();
|
|
571
602
|
console.log(`[SessionEnd] Saved session for ${project}`);
|
|
572
603
|
console.log(` Last work: ${lastWork.slice(0, 80)}`);
|
|
@@ -62,6 +62,11 @@ function cleanupNoiseMemories(db) {
|
|
|
62
62
|
}
|
|
63
63
|
catch { /* ignore */ }
|
|
64
64
|
}
|
|
65
|
+
// 토큰 예산 시스템 (컨텍스트 무한 증가 방지)
|
|
66
|
+
const MAX_CONTEXT_TOKENS = parseInt(process.env.MCP_CONTEXT_BUDGET || '2000', 10);
|
|
67
|
+
function estimateTokens(text) {
|
|
68
|
+
return Math.ceil(text.length / 4); // 대략적 추정 (한글은 1.5~2배)
|
|
69
|
+
}
|
|
65
70
|
function loadContext(dbPath, project) {
|
|
66
71
|
if (!fs.existsSync(dbPath))
|
|
67
72
|
return null;
|
|
@@ -70,15 +75,19 @@ function loadContext(dbPath, project) {
|
|
|
70
75
|
// 노이즈 메모리 자동 정리
|
|
71
76
|
cleanupNoiseMemories(db);
|
|
72
77
|
const lines = [`# ${project} - Session Resumed\n`];
|
|
73
|
-
|
|
78
|
+
let tokenBudget = MAX_CONTEXT_TOKENS;
|
|
79
|
+
// [Priority 1] 현재 상태
|
|
74
80
|
const active = db.prepare('SELECT current_state, blockers FROM active_context WHERE project = ?').get(project);
|
|
75
81
|
if (active?.current_state) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const stateBlock = `📍 **State**: ${active.current_state}` + (active.blockers ? `\n🚧 **Blocker**: ${active.blockers}` : '');
|
|
83
|
+
const cost = estimateTokens(stateBlock);
|
|
84
|
+
if (tokenBudget > cost) {
|
|
85
|
+
lines.push(stateBlock);
|
|
86
|
+
lines.push('');
|
|
87
|
+
tokenBudget -= cost;
|
|
88
|
+
}
|
|
80
89
|
}
|
|
81
|
-
// 최근 3개 세션 (빈 세션 skip)
|
|
90
|
+
// [Priority 2] 최근 3개 세션 (빈 세션 skip)
|
|
82
91
|
const recentSessions = db.prepare(`
|
|
83
92
|
SELECT last_work, next_tasks, issues, timestamp FROM sessions
|
|
84
93
|
WHERE project = ?
|
|
@@ -89,61 +98,83 @@ function loadContext(dbPath, project) {
|
|
|
89
98
|
AND length(last_work) > 15
|
|
90
99
|
ORDER BY timestamp DESC LIMIT 3
|
|
91
100
|
`).all(project);
|
|
92
|
-
if (recentSessions.length > 0) {
|
|
93
|
-
|
|
101
|
+
if (recentSessions.length > 0 && tokenBudget > 100) {
|
|
102
|
+
const sessionLines = ['## Recent Sessions'];
|
|
94
103
|
for (const session of recentSessions) {
|
|
95
|
-
// last_work 60자 제한 (토큰 예산)
|
|
96
104
|
const work = session.last_work.length > 60 ? session.last_work.slice(0, 60) + '...' : session.last_work;
|
|
97
|
-
|
|
98
|
-
// 커밋 정보 (간결하게)
|
|
105
|
+
sessionLines.push(`- [${session.timestamp?.slice(0, 10) || '?'}] ${work}`);
|
|
99
106
|
if (session.issues) {
|
|
100
107
|
try {
|
|
101
108
|
const meta = JSON.parse(session.issues);
|
|
102
109
|
if (meta.commits?.length > 0) {
|
|
103
|
-
|
|
110
|
+
sessionLines.push(` commits: ${meta.commits.slice(0, 2).join('; ').slice(0, 80)}`);
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
catch { /* skip */ }
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
const cost = estimateTokens(sessionLines.join('\n'));
|
|
117
|
+
if (tokenBudget > cost) {
|
|
118
|
+
lines.push(...sessionLines, '');
|
|
119
|
+
tokenBudget -= cost;
|
|
120
|
+
}
|
|
110
121
|
}
|
|
111
|
-
// 사용자 지시사항
|
|
122
|
+
// [Priority 3] 사용자 지시사항 (가장 중요 - 예산 부족해도 high priority는 포함)
|
|
112
123
|
try {
|
|
113
124
|
const directives = db.prepare(`
|
|
114
125
|
SELECT directive, priority FROM user_directives
|
|
115
126
|
WHERE project = ? ORDER BY priority DESC, created_at DESC LIMIT 5
|
|
116
127
|
`).all(project);
|
|
117
128
|
if (directives.length > 0) {
|
|
118
|
-
|
|
129
|
+
const directiveLines = ['## Directives'];
|
|
119
130
|
for (const d of directives) {
|
|
120
131
|
const icon = d.priority === 'high' ? '🔴' : '📎';
|
|
121
|
-
|
|
132
|
+
directiveLines.push(`- ${icon} ${d.directive}`);
|
|
133
|
+
}
|
|
134
|
+
const cost = estimateTokens(directiveLines.join('\n'));
|
|
135
|
+
// 지시사항은 예산 초과해도 high priority는 포함
|
|
136
|
+
const highOnly = directives.filter(d => d.priority === 'high');
|
|
137
|
+
if (tokenBudget > cost) {
|
|
138
|
+
lines.push(...directiveLines, '');
|
|
139
|
+
tokenBudget -= cost;
|
|
140
|
+
}
|
|
141
|
+
else if (highOnly.length > 0) {
|
|
142
|
+
const criticalLines = ['## Directives'];
|
|
143
|
+
for (const d of highOnly)
|
|
144
|
+
criticalLines.push(`- 🔴 ${d.directive}`);
|
|
145
|
+
lines.push(...criticalLines, '');
|
|
146
|
+
tokenBudget -= estimateTokens(criticalLines.join('\n'));
|
|
122
147
|
}
|
|
123
|
-
lines.push('');
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
catch { /* table may not exist yet */ }
|
|
127
|
-
// 미완료 태스크
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
151
|
+
// [Priority 4] 미완료 태스크
|
|
152
|
+
if (tokenBudget > 50) {
|
|
153
|
+
try {
|
|
154
|
+
const tasks = db.prepare(`
|
|
155
|
+
SELECT title, priority, status FROM tasks
|
|
156
|
+
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
157
|
+
ORDER BY priority DESC LIMIT 5
|
|
158
|
+
`).all(project);
|
|
159
|
+
if (tasks.length > 0) {
|
|
160
|
+
const taskLines = ['## Pending Tasks'];
|
|
161
|
+
for (const t of tasks) {
|
|
162
|
+
const icon = t.status === 'in_progress' ? '🔄' : '⏳';
|
|
163
|
+
taskLines.push(`- ${icon} [P${t.priority}] ${t.title}`);
|
|
164
|
+
}
|
|
165
|
+
const cost = estimateTokens(taskLines.join('\n'));
|
|
166
|
+
if (tokenBudget > cost) {
|
|
167
|
+
lines.push(...taskLines, '');
|
|
168
|
+
tokenBudget -= cost;
|
|
169
|
+
}
|
|
139
170
|
}
|
|
140
|
-
lines.push('');
|
|
141
171
|
}
|
|
172
|
+
catch { /* table may not exist */ }
|
|
142
173
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
// [Priority 5] 중요 메모리 (temporal decay 적용, 예산 내에서)
|
|
175
|
+
if (tokenBudget > 80)
|
|
176
|
+
try {
|
|
177
|
+
const memories = db.prepare(`
|
|
147
178
|
SELECT content, memory_type, importance, created_at, access_count FROM memories
|
|
148
179
|
WHERE project = ?
|
|
149
180
|
AND memory_type IN ('decision', 'learning', 'error', 'preference')
|
|
@@ -152,36 +183,43 @@ function loadContext(dbPath, project) {
|
|
|
152
183
|
AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
|
|
153
184
|
ORDER BY importance DESC, accessed_at DESC LIMIT 20
|
|
154
185
|
`).all(project);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
if (memories.length > 0) {
|
|
187
|
+
// Decay 적용 후 top 5 선택
|
|
188
|
+
const DECAY_RATES = {
|
|
189
|
+
decision: 0.001, learning: 0.003, error: 0.01, preference: 0.002
|
|
190
|
+
};
|
|
191
|
+
const scored = memories.map(m => {
|
|
192
|
+
const ageDays = (Date.now() - new Date(m.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
193
|
+
const decayRate = DECAY_RATES[m.memory_type] ?? 0.005;
|
|
194
|
+
const score = m.importance * Math.exp(-decayRate * ageDays) * Math.log2(m.access_count + 2);
|
|
195
|
+
return { ...m, score };
|
|
196
|
+
}).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
197
|
+
const typeIcons = {
|
|
198
|
+
decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
|
|
199
|
+
};
|
|
200
|
+
const memoryLines = ['## Key Memories'];
|
|
201
|
+
for (const m of scored) {
|
|
202
|
+
const icon = typeIcons[m.memory_type] || '💭';
|
|
203
|
+
const content = m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content;
|
|
204
|
+
memoryLines.push(`- ${icon} ${content}`);
|
|
205
|
+
}
|
|
206
|
+
const cost = estimateTokens(memoryLines.join('\n'));
|
|
207
|
+
if (tokenBudget > cost) {
|
|
208
|
+
lines.push(...memoryLines, '');
|
|
209
|
+
tokenBudget -= cost;
|
|
210
|
+
}
|
|
174
211
|
}
|
|
175
|
-
lines.push('');
|
|
176
212
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// 솔루션 통계 (1줄)
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
// [Priority 6] 솔루션 통계 (1줄, 저비용)
|
|
180
215
|
try {
|
|
181
216
|
const solCount = db.prepare('SELECT COUNT(*) as cnt FROM solutions WHERE project = ?').get(project)?.cnt || 0;
|
|
182
217
|
if (solCount > 0) {
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
const solLine = `\nSolutions: ${solCount} recorded (auto-injected on error)\n`;
|
|
219
|
+
if (tokenBudget > 10) {
|
|
220
|
+
lines.push(solLine);
|
|
221
|
+
tokenBudget -= estimateTokens(solLine);
|
|
222
|
+
}
|
|
185
223
|
}
|
|
186
224
|
}
|
|
187
225
|
catch { /* solutions table may not exist */ }
|
package/dist/index.js
CHANGED
|
@@ -278,6 +278,23 @@ function cosineSimilarity(a, b) {
|
|
|
278
278
|
}
|
|
279
279
|
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
280
280
|
}
|
|
281
|
+
// ===== 임베딩 저장 (재시도 포함) =====
|
|
282
|
+
async function storeEmbeddingWithRetry(db, entityType, entityId, text, retries = 2) {
|
|
283
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
284
|
+
try {
|
|
285
|
+
const embedding = await generateEmbedding(text, 'passage');
|
|
286
|
+
if (embedding) {
|
|
287
|
+
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
288
|
+
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)')
|
|
289
|
+
.run(entityType, entityId, buffer);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch { /* retry */ }
|
|
294
|
+
if (attempt < retries)
|
|
295
|
+
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
281
298
|
// ===== 유틸리티 함수 =====
|
|
282
299
|
async function fileExists(filePath) {
|
|
283
300
|
try {
|
|
@@ -892,9 +909,9 @@ async function handleTool(name, args) {
|
|
|
892
909
|
const days = args.days || 7;
|
|
893
910
|
const sessions = db.prepare(`
|
|
894
911
|
SELECT * FROM sessions
|
|
895
|
-
WHERE project = ? AND timestamp > datetime('now', '
|
|
912
|
+
WHERE project = ? AND timestamp > datetime('now', '-' || ? || ' days')
|
|
896
913
|
ORDER BY timestamp DESC LIMIT ?
|
|
897
|
-
`).all(project, limit);
|
|
914
|
+
`).all(project, days, limit);
|
|
898
915
|
return {
|
|
899
916
|
content: [{
|
|
900
917
|
type: 'text',
|
|
@@ -928,10 +945,26 @@ async function handleTool(name, args) {
|
|
|
928
945
|
// 모든 세션 가져와서 유사도 계산
|
|
929
946
|
const allSessions = db.prepare(project
|
|
930
947
|
? 'SELECT * FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 100'
|
|
931
|
-
: 'SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 100').all(project ? [project] : []);
|
|
948
|
+
: 'SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 100').all(...(project ? [project] : []));
|
|
949
|
+
// 사전 캐시된 임베딩 활용 (없으면 on-the-fly 생성 후 캐시)
|
|
932
950
|
const scored = await Promise.all(allSessions.map(async (s) => {
|
|
933
|
-
const
|
|
934
|
-
|
|
951
|
+
const sessionId = s.id;
|
|
952
|
+
// 캐시된 임베딩 확인
|
|
953
|
+
const cached = db.prepare('SELECT embedding FROM embeddings_v4 WHERE entity_type = ? AND entity_id = ?').get('session', sessionId);
|
|
954
|
+
let emb = null;
|
|
955
|
+
if (cached?.embedding) {
|
|
956
|
+
emb = Array.from(new Float32Array(cached.embedding.buffer, cached.embedding.byteOffset, cached.embedding.byteLength / 4));
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
const text = `${s.last_work} ${s.current_status || ''}`;
|
|
960
|
+
emb = await generateEmbedding(text, 'passage');
|
|
961
|
+
// 생성 성공 시 캐시 저장
|
|
962
|
+
if (emb) {
|
|
963
|
+
const buffer = Buffer.from(new Float32Array(emb).buffer);
|
|
964
|
+
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)')
|
|
965
|
+
.run('session', sessionId, buffer);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
935
968
|
const similarity = emb ? cosineSimilarity(queryEmbedding, emb) : 0;
|
|
936
969
|
return { ...s, similarity };
|
|
937
970
|
}));
|
|
@@ -1180,13 +1213,8 @@ async function handleTool(name, args) {
|
|
|
1180
1213
|
INSERT INTO solutions (project, error_signature, error_message, solution, related_files, keywords)
|
|
1181
1214
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
1182
1215
|
`).run(project || null, errorSignature, errorMessage || null, solution, relatedFiles ? JSON.stringify(relatedFiles) : null, keywords);
|
|
1183
|
-
// 임베딩 저장 (시맨틱
|
|
1184
|
-
|
|
1185
|
-
if (embedding) {
|
|
1186
|
-
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
1187
|
-
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)').run('solution', result.lastInsertRowid, buffer);
|
|
1188
|
-
}
|
|
1189
|
-
}).catch(() => { });
|
|
1216
|
+
// 임베딩 저장 (시맨틱 검색용, 재시도 포함)
|
|
1217
|
+
storeEmbeddingWithRetry(db, 'solution', result.lastInsertRowid, `${errorSignature} ${errorMessage || ''} ${solution}`);
|
|
1190
1218
|
return {
|
|
1191
1219
|
content: [{
|
|
1192
1220
|
type: 'text',
|
|
@@ -1408,13 +1436,8 @@ async function handleTool(name, args) {
|
|
|
1408
1436
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
1409
1437
|
`).run(content, memoryType, tags ? JSON.stringify(tags) : null, project || null, importance, null);
|
|
1410
1438
|
const memoryId = result.lastInsertRowid;
|
|
1411
|
-
// 임베딩 생성 (
|
|
1412
|
-
|
|
1413
|
-
if (embedding) {
|
|
1414
|
-
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
1415
|
-
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)').run('memory', memoryId, buffer);
|
|
1416
|
-
}
|
|
1417
|
-
}).catch(() => { });
|
|
1439
|
+
// 임베딩 생성 (재시도 포함)
|
|
1440
|
+
storeEmbeddingWithRetry(db, 'memory', memoryId, content);
|
|
1418
1441
|
// 관계 자동 생성
|
|
1419
1442
|
if (relatedTo) {
|
|
1420
1443
|
db.prepare(`
|
|
@@ -1645,10 +1668,10 @@ async function handleTool(name, args) {
|
|
|
1645
1668
|
const project = args.project;
|
|
1646
1669
|
const totalMemories = db.prepare(project
|
|
1647
1670
|
? 'SELECT COUNT(*) as count FROM memories WHERE project = ?'
|
|
1648
|
-
: 'SELECT COUNT(*) as count FROM memories').get(project ? [project] : []).count;
|
|
1671
|
+
: 'SELECT COUNT(*) as count FROM memories').get(...(project ? [project] : [])).count;
|
|
1649
1672
|
const byType = db.prepare(project
|
|
1650
1673
|
? 'SELECT memory_type as type, COUNT(*) as count FROM memories WHERE project = ? GROUP BY memory_type'
|
|
1651
|
-
: 'SELECT memory_type as type, COUNT(*) as count FROM memories GROUP BY memory_type').all(project ? [project] : []);
|
|
1674
|
+
: 'SELECT memory_type as type, COUNT(*) as count FROM memories GROUP BY memory_type').all(...(project ? [project] : []));
|
|
1652
1675
|
const byProject = db.prepare(`
|
|
1653
1676
|
SELECT COALESCE(project, 'global') as project, COUNT(*) as count
|
|
1654
1677
|
FROM memories GROUP BY project ORDER BY count DESC LIMIT 10
|
|
@@ -1663,13 +1686,13 @@ async function handleTool(name, args) {
|
|
|
1663
1686
|
FROM memories
|
|
1664
1687
|
${project ? 'WHERE project = ?' : ''}
|
|
1665
1688
|
ORDER BY created_at DESC LIMIT 5
|
|
1666
|
-
`).all(project ? [project] : []);
|
|
1689
|
+
`).all(...(project ? [project] : []));
|
|
1667
1690
|
const topAccessedMemories = db.prepare(`
|
|
1668
1691
|
SELECT id, memory_type, content, access_count
|
|
1669
1692
|
FROM memories
|
|
1670
1693
|
${project ? 'WHERE project = ?' : ''}
|
|
1671
1694
|
ORDER BY access_count DESC LIMIT 5
|
|
1672
|
-
`).all(project ? [project] : []);
|
|
1695
|
+
`).all(...(project ? [project] : []));
|
|
1673
1696
|
return {
|
|
1674
1697
|
content: [{
|
|
1675
1698
|
type: 'text',
|