claude-session-continuity-mcp 1.9.6 → 1.10.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 +72 -40
- package/dist/hooks/post-tool-use.js +2 -14
- package/dist/hooks/pre-compact.d.ts +3 -2
- package/dist/hooks/pre-compact.js +113 -63
- package/dist/hooks/session-end.js +181 -17
- package/dist/hooks/session-start.js +90 -64
- package/dist/hooks/user-prompt-submit.js +9 -5
- package/dist/index.js +26 -10
- package/dist/tools/memory.js +17 -4
- package/dist/tools-v2/memory.js +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# claude-session-continuity-mcp (v1.
|
|
1
|
+
# claude-session-continuity-mcp (v1.10.0)
|
|
2
2
|
|
|
3
3
|
> **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search
|
|
4
4
|
|
|
@@ -25,27 +25,36 @@ Every new Claude Code session:
|
|
|
25
25
|
**Fully automatic.** Claude Hooks handle everything without manual calls:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
# Session start → Auto-loads relevant context
|
|
29
|
-
# When asking → Auto-injects memories/solutions
|
|
30
|
-
# During conversation →
|
|
31
|
-
# On
|
|
28
|
+
# Session start → Auto-loads relevant context + recent session history
|
|
29
|
+
# When asking → Auto-injects relevant memories/solutions
|
|
30
|
+
# During conversation → Tracks active files (no noise memories)
|
|
31
|
+
# On compact → Structured handover context for continuity
|
|
32
|
+
# On exit → Extracts commits, decisions, error-fix pairs from transcript
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
```
|
|
35
36
|
← Auto-output on session start:
|
|
36
|
-
#
|
|
37
|
+
# my-app - Session Resumed
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
**framework**: Next.js, **language**: TypeScript
|
|
39
|
+
📍 **State**: Implementing signup form
|
|
40
40
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
## Recent Sessions
|
|
42
|
+
### 2026-02-28
|
|
43
|
+
**Work**: Completed OAuth integration with Google provider
|
|
44
|
+
**Commits**: feat: add OAuth callback handler; fix: redirect URI config
|
|
45
|
+
**Decisions**: Use Server Actions instead of API routes
|
|
46
|
+
|
|
47
|
+
### 2026-02-27
|
|
48
|
+
**Work**: Set up authentication foundation
|
|
49
|
+
**Next**: Implement signup form validation
|
|
50
|
+
|
|
51
|
+
## Directives
|
|
52
|
+
- 🔴 Always use Zod for form validation
|
|
53
|
+
- 📎 Prefer Server Components by default
|
|
44
54
|
|
|
45
|
-
##
|
|
46
|
-
- 🎯
|
|
47
|
-
- ⚠️
|
|
48
|
-
- 📚 [learning] Zod form validation gives automatic type inference
|
|
55
|
+
## Key Memories
|
|
56
|
+
- 🎯 Decided on App Router, using Server Actions
|
|
57
|
+
- ⚠️ OAuth redirect_uri mismatch → check env file
|
|
49
58
|
```
|
|
50
59
|
|
|
51
60
|
**Zero manual work. Context follows you.**
|
|
@@ -64,8 +73,6 @@ npm install claude-session-continuity-mcp
|
|
|
64
73
|
1. Registers MCP server in `~/.claude.json`
|
|
65
74
|
2. Installs Claude Hooks in `~/.claude/settings.json`
|
|
66
75
|
|
|
67
|
-
> **v1.6.1:** Fixed critical bug where hooks were installed to wrong settings file. Now correctly installs to `~/.claude/settings.json`. Auto-migrates existing users from `settings.local.json`.
|
|
68
|
-
|
|
69
76
|
### What Gets Installed
|
|
70
77
|
|
|
71
78
|
**MCP Server** (in `~/.claude.json`):
|
|
@@ -101,9 +108,9 @@ npm install claude-session-continuity-mcp
|
|
|
101
108
|
|------|---------|----------|
|
|
102
109
|
| `SessionStart` | `claude-hook-session-start` | Auto-loads project context on session start |
|
|
103
110
|
| `UserPromptSubmit` | `claude-hook-user-prompt` | Auto-injects relevant memories + past reference search |
|
|
104
|
-
| `PostToolUse` | `claude-hook-post-tool` | Tracks
|
|
105
|
-
| `PreCompact` | `claude-hook-pre-compact` |
|
|
106
|
-
| `Stop` | `claude-hook-session-end` |
|
|
111
|
+
| `PostToolUse` | `claude-hook-post-tool` | Tracks active files (Edit, Write) — no noise memories |
|
|
112
|
+
| `PreCompact` | `claude-hook-pre-compact` | Structured handover context before compression |
|
|
113
|
+
| `Stop` | `claude-hook-session-end` | Extracts commits, decisions, error-fix pairs from transcript |
|
|
107
114
|
|
|
108
115
|
### Manual Hook Management
|
|
109
116
|
|
|
@@ -129,19 +136,18 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
129
136
|
| Feature | Description |
|
|
130
137
|
|---------|-------------|
|
|
131
138
|
| 🤖 **Zero Manual Work** | Claude Hooks automate all context capture/load |
|
|
132
|
-
| 🎯 **
|
|
139
|
+
| 🎯 **Quality Memory Only** | **(v1.10.0)** Only decisions, learnings, errors — no file-change noise |
|
|
133
140
|
| 🧠 **Semantic Search** | multilingual-e5-small embedding (94+ languages, 384d) |
|
|
134
141
|
| 🌍 **Multilingual** | Korean/English/Japanese + cross-language search (EN→KR, KR→EN) |
|
|
135
|
-
| 🔗 **Git Integration** | Commit messages auto-
|
|
142
|
+
| 🔗 **Git Integration** | Commit messages auto-extracted from transcripts |
|
|
136
143
|
| 🕸️ **Knowledge Graph** | Memory relations (solves, causes, extends...) |
|
|
137
|
-
| 📊 **Memory Classification** |
|
|
144
|
+
| 📊 **Memory Classification** | 5 types: observation, decision, learning, error, pattern |
|
|
138
145
|
| ✅ **Integrated Verification** | One-click build/test/lint execution |
|
|
139
146
|
| 📋 **Task Management** | Priority-based task management |
|
|
140
147
|
| 🔧 **Solution Archive** | Auto-search error solutions |
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
| 🚪 **Auto Session End** | **(v1.5.0)** No manual session_end needed |
|
|
148
|
+
| 📝 **Structured Handover** | **(v1.10.0)** PreCompact saves work summary, active files, pending actions |
|
|
149
|
+
| 🚪 **Smart Session End** | **(v1.10.0)** Extracts commits, decisions, error-fix pairs from transcript |
|
|
150
|
+
| 🗑️ **Auto Noise Cleanup** | **(v1.10.0)** Auto-deletes stale observation memories (3d+) |
|
|
145
151
|
| 🔍 **Past Reference Detection** | **(v1.8.0)** "저번에 X 어떻게 했어?" auto-searches DB |
|
|
146
152
|
| 📝 **User Directive Extraction** | **(v1.8.0)** Auto-extracts "always/never" rules from prompts |
|
|
147
153
|
|
|
@@ -153,32 +159,53 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
153
159
|
|
|
154
160
|
**SessionStart Hook** (`npx claude-hook-session-start`):
|
|
155
161
|
- Auto-detects project: monorepo (`apps/project-name/`) or single project (`package.json` root folder name)
|
|
156
|
-
- Loads context from
|
|
157
|
-
- Injects:
|
|
162
|
+
- Loads context from `.claude/sessions.db`
|
|
163
|
+
- Injects: Current state, **3 recent sessions** with commits/decisions, directives, pending tasks, filtered key memories
|
|
164
|
+
- Auto-cleans stale noise memories (3d+ auto-tracked, 14d+ auto-compact)
|
|
158
165
|
|
|
159
166
|
**UserPromptSubmit Hook** (`npx claude-hook-user-prompt`):
|
|
160
167
|
- Runs on every prompt submission
|
|
161
|
-
- Injects relevant context
|
|
168
|
+
- Injects relevant context (filtered: decisions, learnings, errors only)
|
|
169
|
+
|
|
170
|
+
**PostToolUse Hook** (`npx claude-hook-post-tool`):
|
|
171
|
+
- Tracks hot file paths and updates `active_context.recent_files`
|
|
172
|
+
- **No longer creates observation memories** (v1.10.0 — eliminates `[File Change]` noise)
|
|
173
|
+
|
|
174
|
+
**PreCompact Hook** (`npx claude-hook-pre-compact`):
|
|
175
|
+
- Builds structured handover context: work summary, active file, pending action, key facts, recent errors
|
|
176
|
+
- **No longer stores auto-compact memories** (v1.10.0)
|
|
177
|
+
|
|
178
|
+
**Stop Hook** (`npx claude-hook-session-end`):
|
|
179
|
+
- Extracts commit messages from JSONL transcript (`git commit -m` patterns)
|
|
180
|
+
- Extracts error-fix pairs (error → resolution within 3 messages)
|
|
181
|
+
- Extracts decisions ("because", "instead of", "chose" patterns)
|
|
182
|
+
- Stores structured metadata in `sessions.issues` column as JSON
|
|
162
183
|
|
|
163
184
|
### Example Output (Session Start)
|
|
164
185
|
|
|
165
186
|
```markdown
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
## Tech Stack
|
|
169
|
-
**framework**: Next.js, **language**: TypeScript
|
|
187
|
+
# my-app - Session Resumed
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
📍 Implementing signup form
|
|
189
|
+
📍 **State**: Implementing signup form
|
|
173
190
|
🚧 **Blocker**: OAuth callback URL issue
|
|
174
191
|
|
|
175
|
-
##
|
|
192
|
+
## Recent Sessions
|
|
193
|
+
### 2026-02-28
|
|
194
|
+
**Work**: Completed OAuth integration
|
|
195
|
+
**Commits**: feat: add OAuth handler; fix: redirect config
|
|
196
|
+
**Decisions**: Use Server Actions over API routes
|
|
197
|
+
**Next**: Implement form validation
|
|
198
|
+
|
|
199
|
+
## Directives
|
|
200
|
+
- 🔴 Always use Zod for validation
|
|
201
|
+
|
|
202
|
+
## Pending Tasks
|
|
176
203
|
- 🔄 [P8] Implement form validation
|
|
177
204
|
- ⏳ [P5] Add error handling
|
|
178
205
|
|
|
179
|
-
##
|
|
180
|
-
- 🎯
|
|
181
|
-
- ⚠️
|
|
206
|
+
## Key Memories
|
|
207
|
+
- 🎯 Decided on App Router, using Server Actions
|
|
208
|
+
- ⚠️ OAuth redirect_uri mismatch → check env file
|
|
182
209
|
```
|
|
183
210
|
|
|
184
211
|
### Hook Management
|
|
@@ -496,6 +523,11 @@ npm run test:coverage
|
|
|
496
523
|
- [x] Empty session skip and techStack save improvements (v1.7.1)
|
|
497
524
|
- [x] Past reference auto-detection in UserPromptSubmit hook (v1.8.0)
|
|
498
525
|
- [x] User directive extraction ("always/never" rules) (v1.8.0)
|
|
526
|
+
- [x] Memory quality overhaul — no more `[File Change]` noise (v1.10.0)
|
|
527
|
+
- [x] Structured handover context in PreCompact (v1.10.0)
|
|
528
|
+
- [x] Smart session-end: commit/decision/error-fix extraction from transcript (v1.10.0)
|
|
529
|
+
- [x] Auto noise cleanup (3d+ observations, 14d+ auto-compact) (v1.10.0)
|
|
530
|
+
- [x] 3 recent sessions display with structured metadata (v1.10.0)
|
|
499
531
|
- [ ] sqlite-vec native vector search (v2 - when data > 1000 records)
|
|
500
532
|
- [ ] Web dashboard
|
|
501
533
|
- [ ] Cloud sync option
|
|
@@ -183,20 +183,8 @@ async function main() {
|
|
|
183
183
|
catch {
|
|
184
184
|
// 오류 시 무시
|
|
185
185
|
}
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
const existingMemory = db.prepare(`
|
|
189
|
-
SELECT id FROM memories
|
|
190
|
-
WHERE project = ?
|
|
191
|
-
AND content LIKE ?
|
|
192
|
-
AND date(created_at) = ?
|
|
193
|
-
`).get(project, `%${path.basename(filePath)}%`, today);
|
|
194
|
-
if (!existingMemory) {
|
|
195
|
-
db.prepare(`
|
|
196
|
-
INSERT INTO memories (content, memory_type, project, importance, tags)
|
|
197
|
-
VALUES (?, 'observation', ?, 3, ?)
|
|
198
|
-
`).run(`[File Change] ${summary}`, project, `auto-tracked,${changeType},${getFileExtension(filePath)}`);
|
|
199
|
-
}
|
|
186
|
+
// auto-tracked 메모리 기록 제거 (v1.10.0)
|
|
187
|
+
// git이 파일 변경을 더 잘 추적함. hot_paths + recent_files만 유지.
|
|
200
188
|
}
|
|
201
189
|
db.close();
|
|
202
190
|
process.exit(0);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PreCompact Hook - 컨텍스트 압축 전
|
|
3
|
+
* PreCompact Hook v2 - 컨텍스트 압축 전 구조화된 HANDOVER 생성
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 컴팩션 전에 대화 내용을 분석해 구조화된 컨텍스트를 systemMessage로 반환합니다.
|
|
6
|
+
* v1과 달리 memories 테이블에 저장하지 않습니다 (노이즈 방지).
|
|
6
7
|
*/
|
|
7
8
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PreCompact Hook - 컨텍스트 압축 전
|
|
3
|
+
* PreCompact Hook v2 - 컨텍스트 압축 전 구조화된 HANDOVER 생성
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 컴팩션 전에 대화 내용을 분석해 구조화된 컨텍스트를 systemMessage로 반환합니다.
|
|
6
|
+
* v1과 달리 memories 테이블에 저장하지 않습니다 (노이즈 방지).
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
@@ -30,12 +31,10 @@ function getDbPath(cwd) {
|
|
|
30
31
|
function detectProject(cwd) {
|
|
31
32
|
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
32
33
|
const appsDir = path.join(workspaceRoot, 'apps');
|
|
33
|
-
// apps/ 하위인지 확인
|
|
34
34
|
if (cwd.startsWith(appsDir + path.sep)) {
|
|
35
35
|
const relative = path.relative(appsDir, cwd);
|
|
36
36
|
return relative.split(path.sep)[0];
|
|
37
37
|
}
|
|
38
|
-
// apps/ 외부 하위 프로젝트 (hackathons/ 등)
|
|
39
38
|
if (cwd !== workspaceRoot) {
|
|
40
39
|
let current = cwd;
|
|
41
40
|
while (current !== workspaceRoot && current !== path.parse(current).root) {
|
|
@@ -52,44 +51,97 @@ function detectProject(cwd) {
|
|
|
52
51
|
current = path.dirname(current);
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
// 워크스페이스 루트 → 폴더명 반환
|
|
56
54
|
return path.basename(workspaceRoot);
|
|
57
55
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
function stripMarkdown(text) {
|
|
57
|
+
return text
|
|
58
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
59
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
60
|
+
.replace(/#{1,6}\s*/g, '')
|
|
61
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 대화 transcript에서 구조화된 핸드오버 컨텍스트를 빌드합니다.
|
|
66
|
+
*/
|
|
67
|
+
function buildHandoverContext(transcript) {
|
|
68
|
+
const context = {
|
|
69
|
+
workSummary: '',
|
|
70
|
+
activeFile: null,
|
|
71
|
+
pendingAction: null,
|
|
72
|
+
keyFacts: [],
|
|
73
|
+
recentErrors: []
|
|
74
|
+
};
|
|
75
|
+
const userMessages = transcript.filter(m => m.role === 'user');
|
|
76
|
+
const assistantMessages = transcript.filter(m => m.role === 'assistant');
|
|
77
|
+
// 1. workSummary: 첫 user 메시지 = 작업 요청
|
|
78
|
+
if (userMessages.length > 0) {
|
|
79
|
+
const first = userMessages[0].content;
|
|
80
|
+
// 코드블록, 테이블 제거 후 첫 의미있는 라인
|
|
81
|
+
const cleaned = first
|
|
82
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map(l => l.trim())
|
|
85
|
+
.filter(l => l.length > 10 && !l.startsWith('|') && !l.startsWith('---'));
|
|
86
|
+
if (cleaned.length > 0) {
|
|
87
|
+
context.workSummary = stripMarkdown(cleaned[0]).slice(0, 200);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// 2. activeFile: 최근 메시지에서 파일 경로 추출
|
|
91
|
+
const recentAll = transcript.slice(-10);
|
|
92
|
+
for (const msg of recentAll.reverse()) {
|
|
93
|
+
const filePatterns = [
|
|
94
|
+
/(?:file_path|파일)[:\s]*["']?([^\s"',]+\.\w{1,6})/,
|
|
95
|
+
/(?:Edit|Write|Read|수정|생성|읽기)\s+.*?(\S+\.\w{1,6})/,
|
|
96
|
+
/`([^`]+\.\w{1,6})`/,
|
|
71
97
|
];
|
|
72
|
-
for (const pattern of
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
|
|
98
|
+
for (const pattern of filePatterns) {
|
|
99
|
+
const match = msg.content.match(pattern);
|
|
100
|
+
if (match?.[1] && !match[1].includes('http')) {
|
|
101
|
+
context.activeFile = match[1];
|
|
102
|
+
break;
|
|
76
103
|
}
|
|
77
104
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
if (context.activeFile)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
// 3. pendingAction: 마지막 메시지가 user면 미완료 요청
|
|
109
|
+
if (transcript.length > 0 && transcript[transcript.length - 1].role === 'user') {
|
|
110
|
+
const lastUser = transcript[transcript.length - 1].content;
|
|
111
|
+
const cleaned = stripMarkdown(lastUser.split('\n')[0] || lastUser);
|
|
112
|
+
if (cleaned.length > 5) {
|
|
113
|
+
context.pendingAction = cleaned.slice(0, 150);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 4. keyFacts: assistant 메시지에서 설정값, 포트, 버전 등 추출
|
|
117
|
+
const factPatterns = [
|
|
118
|
+
/(?:port|포트)\s*(?:is|=|:|→)\s*(\d{2,5})/gi,
|
|
119
|
+
/(?:version|버전)\s*(?:is|=|:|→)\s*([\d.]+)/gi,
|
|
120
|
+
/(?:IP|ip)\s*(?:is|=|:|→)\s*([\d.]+)/gi,
|
|
121
|
+
/(?:using|사용)\s+([\w\s.-]+?\s+v[\d.]+)/gi,
|
|
122
|
+
];
|
|
123
|
+
for (const msg of assistantMessages.slice(-10)) {
|
|
124
|
+
for (const pattern of factPatterns) {
|
|
125
|
+
pattern.lastIndex = 0;
|
|
126
|
+
const match = msg.content.match(pattern);
|
|
127
|
+
if (match) {
|
|
128
|
+
context.keyFacts.push(stripMarkdown(match[0]).slice(0, 100));
|
|
87
129
|
}
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
context.keyFacts = [...new Set(context.keyFacts)].slice(0, 5);
|
|
133
|
+
// 5. recentErrors: 에러 패턴 추출
|
|
134
|
+
for (const msg of transcript.slice(-15)) {
|
|
135
|
+
const errorMatch = msg.content.match(/(?:Error|error|ERROR|오류|실패|FAILED|Exception)[:\s](.{10,100})/);
|
|
136
|
+
if (errorMatch) {
|
|
137
|
+
const err = stripMarkdown(errorMatch[0]).slice(0, 100);
|
|
138
|
+
if (!context.recentErrors.includes(err)) {
|
|
139
|
+
context.recentErrors.push(err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
context.recentErrors = context.recentErrors.slice(0, 3);
|
|
144
|
+
return context;
|
|
93
145
|
}
|
|
94
146
|
async function main() {
|
|
95
147
|
try {
|
|
@@ -106,23 +158,23 @@ async function main() {
|
|
|
106
158
|
process.exit(0);
|
|
107
159
|
}
|
|
108
160
|
const db = new Database(dbPath);
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
161
|
+
// 핸드오버 컨텍스트 빌드
|
|
162
|
+
const handover = input.transcript ? buildHandoverContext(input.transcript) : null;
|
|
163
|
+
// active_context 업데이트 (memories에는 저장하지 않음)
|
|
164
|
+
if (handover?.workSummary) {
|
|
165
|
+
const stateStr = [
|
|
166
|
+
handover.workSummary,
|
|
167
|
+
handover.activeFile ? `file: ${handover.activeFile}` : '',
|
|
168
|
+
handover.pendingAction ? `pending: ${handover.pendingAction.slice(0, 50)}` : ''
|
|
169
|
+
].filter(Boolean).join(' | ');
|
|
118
170
|
db.prepare(`
|
|
119
171
|
INSERT OR REPLACE INTO active_context (project, current_state, updated_at)
|
|
120
172
|
VALUES (?, ?, datetime('now'))
|
|
121
|
-
`).run(project,
|
|
173
|
+
`).run(project, stateStr.slice(0, 300));
|
|
122
174
|
}
|
|
123
175
|
// === 컨텍스트 재주입: systemMessage로 반환 ===
|
|
124
|
-
const recoveryLines = [`# ${project} -
|
|
125
|
-
// 사용자 지시사항
|
|
176
|
+
const recoveryLines = [`# ${project} - Compact Recovery\n`];
|
|
177
|
+
// 사용자 지시사항
|
|
126
178
|
try {
|
|
127
179
|
const directives = db.prepare(`
|
|
128
180
|
SELECT directive, priority FROM user_directives
|
|
@@ -154,27 +206,25 @@ async function main() {
|
|
|
154
206
|
if (active.blockers)
|
|
155
207
|
recoveryLines.push(`**Blocker**: ${active.blockers}`);
|
|
156
208
|
}
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
recoveryLines.push(`**
|
|
209
|
+
// 핸드오버 컨텍스트
|
|
210
|
+
if (handover) {
|
|
211
|
+
recoveryLines.push(`\n## Handover`);
|
|
212
|
+
if (handover.workSummary)
|
|
213
|
+
recoveryLines.push(`**Working on**: ${handover.workSummary}`);
|
|
214
|
+
if (handover.activeFile)
|
|
215
|
+
recoveryLines.push(`**Active file**: ${handover.activeFile}`);
|
|
216
|
+
if (handover.pendingAction)
|
|
217
|
+
recoveryLines.push(`**Pending**: ${handover.pendingAction}`);
|
|
218
|
+
if (handover.keyFacts.length > 0) {
|
|
219
|
+
recoveryLines.push('**Key facts**:');
|
|
220
|
+
handover.keyFacts.forEach(f => recoveryLines.push(`- ${f}`));
|
|
166
221
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (keyPoints.length > 0) {
|
|
171
|
-
recoveryLines.push(`\n## Session Key Points`);
|
|
172
|
-
for (const kp of keyPoints) {
|
|
173
|
-
recoveryLines.push(`- ${kp}`);
|
|
222
|
+
if (handover.recentErrors.length > 0) {
|
|
223
|
+
recoveryLines.push('**Recent errors**:');
|
|
224
|
+
handover.recentErrors.forEach(e => recoveryLines.push(`- ${e}`));
|
|
174
225
|
}
|
|
175
226
|
}
|
|
176
227
|
db.close();
|
|
177
|
-
// systemMessage로 반환 → 컴팩션 후에도 유지
|
|
178
228
|
const output = {
|
|
179
229
|
continue: true,
|
|
180
230
|
systemMessage: recoveryLines.join('\n')
|
|
@@ -192,15 +192,13 @@ async function readRecentAssistantMessages(transcriptPath, maxMessages = 5) {
|
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
194
|
* 다음 할 일 추출 (텍스트에서)
|
|
195
|
-
* 테이블 행, 코드블록 내부는 제외
|
|
196
195
|
*/
|
|
197
196
|
function extractNextTasks(content) {
|
|
198
197
|
const nextTasks = [];
|
|
199
|
-
// 코드블록과 테이블 행 제거
|
|
200
198
|
const cleaned = content
|
|
201
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
199
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
202
200
|
.split('\n')
|
|
203
|
-
.filter(line => !line.trim().startsWith('|'))
|
|
201
|
+
.filter(line => !line.trim().startsWith('|'))
|
|
204
202
|
.join('\n');
|
|
205
203
|
const nextPatterns = [
|
|
206
204
|
/(?:next steps?|todo|remaining|다음 (?:단계|작업|할 일)|남은 작업|해야 할)[:\s]*([^.!?\n]{10,})/gi,
|
|
@@ -217,6 +215,144 @@ function extractNextTasks(content) {
|
|
|
217
215
|
}
|
|
218
216
|
return nextTasks;
|
|
219
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* JSONL transcript에서 git commit 메시지 추출
|
|
220
|
+
*/
|
|
221
|
+
async function extractCommitMessages(transcriptPath) {
|
|
222
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
223
|
+
return [];
|
|
224
|
+
const commits = [];
|
|
225
|
+
try {
|
|
226
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
227
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
228
|
+
for await (const line of rl) {
|
|
229
|
+
if (!line.trim())
|
|
230
|
+
continue;
|
|
231
|
+
try {
|
|
232
|
+
const content = line;
|
|
233
|
+
// git commit -m "message" 또는 heredoc 패턴
|
|
234
|
+
const commitPatterns = [
|
|
235
|
+
/git commit.*?-m\s*["']([^"']{10,150})["']/,
|
|
236
|
+
/git commit.*?-m\s*"\$\(cat <<'?EOF'?\n([\s\S]{10,150}?)(?:\n\s*Co-Authored|\nEOF)/,
|
|
237
|
+
];
|
|
238
|
+
for (const pattern of commitPatterns) {
|
|
239
|
+
const match = content.match(pattern);
|
|
240
|
+
if (match?.[1]) {
|
|
241
|
+
const msg = match[1].trim().split('\n')[0]; // 첫 줄만
|
|
242
|
+
if (msg.length > 10 && !msg.startsWith('Co-Authored')) {
|
|
243
|
+
commits.push(msg.slice(0, 150));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* skip */ }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch { /* file read error */ }
|
|
252
|
+
return [...new Set(commits)].slice(0, 5);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* JSONL transcript에서 에러→해결 쌍 추출
|
|
256
|
+
*/
|
|
257
|
+
async function extractErrorFixPairs(transcriptPath) {
|
|
258
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
259
|
+
return [];
|
|
260
|
+
const pairs = [];
|
|
261
|
+
const entries = [];
|
|
262
|
+
try {
|
|
263
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
264
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
265
|
+
for await (const line of rl) {
|
|
266
|
+
if (!line.trim())
|
|
267
|
+
continue;
|
|
268
|
+
try {
|
|
269
|
+
const entry = JSON.parse(line);
|
|
270
|
+
const role = entry.type || entry.role || '';
|
|
271
|
+
let text = '';
|
|
272
|
+
if (typeof entry.message?.content === 'string') {
|
|
273
|
+
text = entry.message.content;
|
|
274
|
+
}
|
|
275
|
+
else if (Array.isArray(entry.message?.content)) {
|
|
276
|
+
text = entry.message.content
|
|
277
|
+
.filter((b) => b.type === 'text')
|
|
278
|
+
.map((b) => b.text)
|
|
279
|
+
.join('\n');
|
|
280
|
+
}
|
|
281
|
+
if (text.length > 5)
|
|
282
|
+
entries.push({ role, text: text.slice(0, 500) });
|
|
283
|
+
}
|
|
284
|
+
catch { /* skip */ }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { /* file error */ }
|
|
288
|
+
const recent = entries.slice(-30);
|
|
289
|
+
const errorRe = /(?:error|Error|ERROR|오류|실패|FAILED|Exception)[:\s](.{5,80})/;
|
|
290
|
+
const fixRe = /(?:fixed|resolved|수정|해결|Added|수정 완료)/i;
|
|
291
|
+
for (let i = 0; i < recent.length - 1; i++) {
|
|
292
|
+
const errorMatch = recent[i].text.match(errorRe);
|
|
293
|
+
if (errorMatch) {
|
|
294
|
+
for (let j = i + 1; j < Math.min(i + 4, recent.length); j++) {
|
|
295
|
+
if (recent[j].role === 'assistant' && fixRe.test(recent[j].text)) {
|
|
296
|
+
const errorStr = stripMarkdown(errorMatch[0]).slice(0, 80);
|
|
297
|
+
const fixLine = recent[j].text.split('\n')
|
|
298
|
+
.find(l => fixRe.test(l));
|
|
299
|
+
const fixStr = fixLine ? stripMarkdown(fixLine).slice(0, 80) : 'resolved';
|
|
300
|
+
pairs.push(`${errorStr} → ${fixStr}`);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return [...new Set(pairs)].slice(0, 3);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* JSONL transcript에서 결정 사항 추출
|
|
310
|
+
*/
|
|
311
|
+
async function extractDecisions(transcriptPath) {
|
|
312
|
+
const messages = await readRecentAssistantMessages(transcriptPath, 10);
|
|
313
|
+
const decisions = [];
|
|
314
|
+
const decisionPatterns = [
|
|
315
|
+
/(?:chose|using|switched to|went with)\s+(.{10,80})\s+(?:because|since|instead of|over)/gi,
|
|
316
|
+
/(?:instead of|rather than)\s+(.{10,60})/gi,
|
|
317
|
+
/(.{10,60})(?:으로|로)\s+(?:결정|변경|전환)(?:했|함|합니다)/g,
|
|
318
|
+
/(.{10,60})(?:대신|말고)\s+(.{10,60})(?:사용|적용)/g,
|
|
319
|
+
];
|
|
320
|
+
for (const msg of messages) {
|
|
321
|
+
for (const pattern of decisionPatterns) {
|
|
322
|
+
pattern.lastIndex = 0;
|
|
323
|
+
const matches = msg.match(pattern);
|
|
324
|
+
if (matches) {
|
|
325
|
+
decisions.push(...matches.slice(0, 1).map(m => stripMarkdown(m).slice(0, 150)));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return [...new Set(decisions)].slice(0, 3);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 최근 assistant 메시지에서 액션 동사 포함 라인 추출 (lastWork 폴백)
|
|
333
|
+
*/
|
|
334
|
+
async function extractWorkFromRecentMessages(transcriptPath) {
|
|
335
|
+
const messages = await readRecentAssistantMessages(transcriptPath, 5);
|
|
336
|
+
const actionVerbs = /(?:created|modified|added|removed|fixed|updated|implemented|deployed|configured|refactored|만들|수정|추가|삭제|구현|배포|설정|완료)/i;
|
|
337
|
+
const filePath = /(?:\/[\w.-]+){2,}|[\w.-]+\.\w{1,4}/;
|
|
338
|
+
for (const msg of messages.reverse()) {
|
|
339
|
+
const lines = msg.split('\n').filter(l => !isNoiseLine(l));
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
if (actionVerbs.test(line) && line.length > 20) {
|
|
342
|
+
const cleaned = stripMarkdown(line).trim();
|
|
343
|
+
if (cleaned.length > 15)
|
|
344
|
+
return cleaned.slice(0, 200);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// 파일 경로 포함 라인도 시도
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
if (filePath.test(line) && actionVerbs.test(line)) {
|
|
350
|
+
return stripMarkdown(line).trim().slice(0, 200);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return '';
|
|
355
|
+
}
|
|
220
356
|
async function main() {
|
|
221
357
|
try {
|
|
222
358
|
let inputData = '';
|
|
@@ -238,15 +374,32 @@ async function main() {
|
|
|
238
374
|
process.exit(0);
|
|
239
375
|
}
|
|
240
376
|
const db = new Database(dbPath);
|
|
241
|
-
// ===
|
|
377
|
+
// === 추출 시작 ===
|
|
242
378
|
let lastWork = '';
|
|
243
379
|
let nextTasks = [];
|
|
244
|
-
|
|
245
|
-
|
|
380
|
+
let commitMessages = [];
|
|
381
|
+
let errorsSolved = [];
|
|
382
|
+
let decisions = [];
|
|
383
|
+
// Phase 1: transcript_path에서 고품질 데이터 추출
|
|
384
|
+
if (input.transcript_path) {
|
|
385
|
+
// git commit 메시지 추출 (가장 고품질 요약)
|
|
386
|
+
commitMessages = await extractCommitMessages(input.transcript_path);
|
|
387
|
+
// 에러→해결 쌍 추출
|
|
388
|
+
errorsSolved = await extractErrorFixPairs(input.transcript_path);
|
|
389
|
+
// 결정 사항 추출
|
|
390
|
+
decisions = await extractDecisions(input.transcript_path);
|
|
391
|
+
}
|
|
392
|
+
// Phase 2: lastWork 결정 (우선순위 폴백)
|
|
393
|
+
// 2a: 커밋 메시지 기반 (가장 신뢰도 높음)
|
|
394
|
+
if (commitMessages.length > 0) {
|
|
395
|
+
lastWork = commitMessages.slice(0, 3).join('; ');
|
|
396
|
+
}
|
|
397
|
+
// 2b: last_assistant_message에서 추출
|
|
398
|
+
if (!lastWork && input.last_assistant_message) {
|
|
246
399
|
lastWork = extractSummaryFromText(input.last_assistant_message);
|
|
247
400
|
nextTasks = extractNextTasks(input.last_assistant_message);
|
|
248
401
|
}
|
|
249
|
-
//
|
|
402
|
+
// 2c: transcript에서 최근 assistant 메시지 스캔
|
|
250
403
|
if (!lastWork && input.transcript_path) {
|
|
251
404
|
const recentMessages = await readRecentAssistantMessages(input.transcript_path);
|
|
252
405
|
for (let i = recentMessages.length - 1; i >= 0; i--) {
|
|
@@ -254,12 +407,15 @@ async function main() {
|
|
|
254
407
|
if (lastWork)
|
|
255
408
|
break;
|
|
256
409
|
}
|
|
257
|
-
|
|
258
|
-
if (recentMessages.length > 0) {
|
|
410
|
+
if (recentMessages.length > 0 && nextTasks.length === 0) {
|
|
259
411
|
nextTasks = extractNextTasks(recentMessages[recentMessages.length - 1]);
|
|
260
412
|
}
|
|
261
413
|
}
|
|
262
|
-
//
|
|
414
|
+
// 2d: 액션 동사 기반 폴백
|
|
415
|
+
if (!lastWork && input.transcript_path) {
|
|
416
|
+
lastWork = await extractWorkFromRecentMessages(input.transcript_path);
|
|
417
|
+
}
|
|
418
|
+
// 2e: 레거시 transcript 배열
|
|
263
419
|
if (!lastWork && input.transcript) {
|
|
264
420
|
const assistantMsgs = input.transcript.filter(m => m.role === 'assistant');
|
|
265
421
|
if (assistantMsgs.length < 2) {
|
|
@@ -273,7 +429,7 @@ async function main() {
|
|
|
273
429
|
break;
|
|
274
430
|
}
|
|
275
431
|
}
|
|
276
|
-
// === modified_files
|
|
432
|
+
// === modified_files ===
|
|
277
433
|
let modifiedFiles = [];
|
|
278
434
|
try {
|
|
279
435
|
const activeCtx = db.prepare('SELECT recent_files FROM active_context WHERE project = ?').get(project);
|
|
@@ -282,7 +438,7 @@ async function main() {
|
|
|
282
438
|
}
|
|
283
439
|
}
|
|
284
440
|
catch { /* active_context may not exist */ }
|
|
285
|
-
// last_work 폴백: 파일 목록 기반
|
|
441
|
+
// last_work 최종 폴백: 파일 목록 기반
|
|
286
442
|
if (!lastWork && modifiedFiles.length > 0) {
|
|
287
443
|
const fileNames = modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
|
|
288
444
|
lastWork = `Modified files: ${fileNames}`;
|
|
@@ -293,7 +449,7 @@ async function main() {
|
|
|
293
449
|
db.close();
|
|
294
450
|
process.exit(0);
|
|
295
451
|
}
|
|
296
|
-
// 중복 저장
|
|
452
|
+
// 중복 저장 방지
|
|
297
453
|
const recentDup = db.prepare(`
|
|
298
454
|
SELECT id FROM sessions
|
|
299
455
|
WHERE project = ? AND last_work = ? AND timestamp > datetime('now', '-60 seconds')
|
|
@@ -304,11 +460,18 @@ async function main() {
|
|
|
304
460
|
db.close();
|
|
305
461
|
process.exit(0);
|
|
306
462
|
}
|
|
463
|
+
// 구조화 메타데이터 (issues 컬럼 활용)
|
|
464
|
+
const metadata = {
|
|
465
|
+
commits: commitMessages,
|
|
466
|
+
decisions,
|
|
467
|
+
errorsSolved
|
|
468
|
+
};
|
|
469
|
+
const hasMetadata = commitMessages.length > 0 || decisions.length > 0 || errorsSolved.length > 0;
|
|
307
470
|
// 세션 기록 저장
|
|
308
471
|
db.prepare(`
|
|
309
|
-
INSERT INTO sessions (project, last_work, next_tasks, modified_files)
|
|
310
|
-
VALUES (?, ?, ?, ?)
|
|
311
|
-
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)));
|
|
472
|
+
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?)
|
|
474
|
+
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null);
|
|
312
475
|
// 활성 컨텍스트 업데이트
|
|
313
476
|
db.prepare(`
|
|
314
477
|
INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
|
|
@@ -317,6 +480,7 @@ async function main() {
|
|
|
317
480
|
db.close();
|
|
318
481
|
console.log(`[SessionEnd] Saved session for ${project}`);
|
|
319
482
|
console.log(` Last work: ${lastWork.slice(0, 80)}`);
|
|
483
|
+
console.log(` Commits: ${commitMessages.length}, Decisions: ${decisions.length}, Errors: ${errorsSolved.length}`);
|
|
320
484
|
console.log(` Modified files: ${modifiedFiles.length}`);
|
|
321
485
|
console.log(` Next tasks: ${nextTasks.length}`);
|
|
322
486
|
process.exit(0);
|
|
@@ -44,44 +44,77 @@ function getProject(cwd, workspaceRoot) {
|
|
|
44
44
|
// 워크스페이스 루트 (모노레포 포함) → 폴더명 반환
|
|
45
45
|
return path.basename(workspaceRoot);
|
|
46
46
|
}
|
|
47
|
+
function cleanupNoiseMemories(db) {
|
|
48
|
+
try {
|
|
49
|
+
// 3일+ auto-tracked 관찰 메모리 삭제
|
|
50
|
+
db.prepare(`
|
|
51
|
+
DELETE FROM memories
|
|
52
|
+
WHERE memory_type = 'observation'
|
|
53
|
+
AND tags LIKE '%auto-tracked%'
|
|
54
|
+
AND created_at < datetime('now', '-3 days')
|
|
55
|
+
`).run();
|
|
56
|
+
// 14일+ auto-compact 패턴 메모리 삭제
|
|
57
|
+
db.prepare(`
|
|
58
|
+
DELETE FROM memories
|
|
59
|
+
WHERE tags LIKE '%auto-compact%'
|
|
60
|
+
AND created_at < datetime('now', '-14 days')
|
|
61
|
+
`).run();
|
|
62
|
+
}
|
|
63
|
+
catch { /* ignore */ }
|
|
64
|
+
}
|
|
47
65
|
function loadContext(dbPath, project) {
|
|
48
66
|
if (!fs.existsSync(dbPath))
|
|
49
67
|
return null;
|
|
50
68
|
try {
|
|
51
|
-
const db = new Database(dbPath
|
|
69
|
+
const db = new Database(dbPath);
|
|
70
|
+
// 노이즈 메모리 자동 정리
|
|
71
|
+
cleanupNoiseMemories(db);
|
|
52
72
|
const lines = [`# ${project} - Session Resumed\n`];
|
|
53
|
-
// 기술 스택
|
|
54
|
-
const fixed = db.prepare('SELECT tech_stack FROM project_context WHERE project = ?').get(project);
|
|
55
|
-
if (fixed?.tech_stack) {
|
|
56
|
-
const stack = JSON.parse(fixed.tech_stack);
|
|
57
|
-
const stackStr = Object.entries(stack).map(([k, v]) => `**${k}**: ${v}`).join(', ');
|
|
58
|
-
lines.push(`## Tech Stack\n${stackStr}\n`);
|
|
59
|
-
}
|
|
60
73
|
// 현재 상태
|
|
61
74
|
const active = db.prepare('SELECT current_state, blockers FROM active_context WHERE project = ?').get(project);
|
|
62
75
|
if (active?.current_state) {
|
|
63
|
-
lines.push(
|
|
76
|
+
lines.push(`📍 **State**: ${active.current_state}`);
|
|
64
77
|
if (active.blockers)
|
|
65
78
|
lines.push(`🚧 **Blocker**: ${active.blockers}`);
|
|
66
79
|
lines.push('');
|
|
67
80
|
}
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
SELECT last_work, next_tasks, timestamp FROM sessions
|
|
81
|
+
// 최근 3개 세션 (빈 세션 skip)
|
|
82
|
+
const recentSessions = db.prepare(`
|
|
83
|
+
SELECT last_work, next_tasks, issues, timestamp FROM sessions
|
|
71
84
|
WHERE project = ?
|
|
72
85
|
AND last_work != 'Session ended'
|
|
73
86
|
AND last_work != 'Session work completed'
|
|
74
87
|
AND last_work != 'Session started'
|
|
75
88
|
AND last_work != ''
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
lines.push(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
AND length(last_work) > 15
|
|
90
|
+
ORDER BY timestamp DESC LIMIT 3
|
|
91
|
+
`).all(project);
|
|
92
|
+
if (recentSessions.length > 0) {
|
|
93
|
+
lines.push('## Recent Sessions');
|
|
94
|
+
for (const session of recentSessions) {
|
|
95
|
+
lines.push(`### ${session.timestamp?.slice(0, 10) || 'unknown'}`);
|
|
96
|
+
lines.push(`**Work**: ${session.last_work}`);
|
|
97
|
+
// 구조화 메타데이터 파싱 (session-end v2에서 저장)
|
|
98
|
+
if (session.issues) {
|
|
99
|
+
try {
|
|
100
|
+
const meta = JSON.parse(session.issues);
|
|
101
|
+
if (meta.commits?.length > 0) {
|
|
102
|
+
lines.push(`**Commits**: ${meta.commits.slice(0, 3).join('; ')}`);
|
|
103
|
+
}
|
|
104
|
+
if (meta.decisions?.length > 0) {
|
|
105
|
+
lines.push(`**Decisions**: ${meta.decisions.join('; ')}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* plain text issues or empty, skip */ }
|
|
109
|
+
}
|
|
110
|
+
if (session.next_tasks) {
|
|
111
|
+
try {
|
|
112
|
+
const next = JSON.parse(session.next_tasks);
|
|
113
|
+
if (next.length > 0)
|
|
114
|
+
lines.push(`**Next**: ${next.slice(0, 2).join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
catch { /* skip */ }
|
|
117
|
+
}
|
|
85
118
|
}
|
|
86
119
|
lines.push('');
|
|
87
120
|
}
|
|
@@ -92,7 +125,7 @@ function loadContext(dbPath, project) {
|
|
|
92
125
|
WHERE project = ? ORDER BY priority DESC, created_at DESC LIMIT 10
|
|
93
126
|
`).all(project);
|
|
94
127
|
if (directives.length > 0) {
|
|
95
|
-
lines.push('##
|
|
128
|
+
lines.push('## Directives');
|
|
96
129
|
for (const d of directives) {
|
|
97
130
|
const icon = d.priority === 'high' ? '🔴' : '📎';
|
|
98
131
|
lines.push(`- ${icon} ${d.directive}`);
|
|
@@ -101,58 +134,51 @@ function loadContext(dbPath, project) {
|
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
136
|
catch { /* table may not exist yet */ }
|
|
104
|
-
//
|
|
137
|
+
// 미완료 태스크
|
|
105
138
|
try {
|
|
106
|
-
const
|
|
107
|
-
SELECT
|
|
108
|
-
WHERE project = ? AND
|
|
109
|
-
ORDER BY
|
|
139
|
+
const tasks = db.prepare(`
|
|
140
|
+
SELECT title, priority, status FROM tasks
|
|
141
|
+
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
142
|
+
ORDER BY priority DESC LIMIT 5
|
|
110
143
|
`).all(project);
|
|
111
|
-
if (
|
|
112
|
-
lines.push('##
|
|
113
|
-
for (const
|
|
114
|
-
const
|
|
115
|
-
lines.push(`- ${
|
|
144
|
+
if (tasks.length > 0) {
|
|
145
|
+
lines.push('## Pending Tasks');
|
|
146
|
+
for (const t of tasks) {
|
|
147
|
+
const icon = t.status === 'in_progress' ? '🔄' : '⏳';
|
|
148
|
+
lines.push(`- ${icon} [P${t.priority}] ${t.title}`);
|
|
116
149
|
}
|
|
117
150
|
lines.push('');
|
|
118
151
|
}
|
|
119
152
|
}
|
|
120
|
-
catch { /* table may not exist
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
|
|
144
|
-
};
|
|
145
|
-
lines.push('## 🧠 Key Memories');
|
|
146
|
-
for (const m of memories) {
|
|
147
|
-
const icon = typeIcons[m.memory_type] || '💭';
|
|
148
|
-
const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
|
|
149
|
-
lines.push(`- ${icon} [${m.memory_type}] ${content}`);
|
|
153
|
+
catch { /* table may not exist */ }
|
|
154
|
+
// 중요 메모리 (노이즈 필터링)
|
|
155
|
+
try {
|
|
156
|
+
const memories = db.prepare(`
|
|
157
|
+
SELECT content, memory_type FROM memories
|
|
158
|
+
WHERE project = ?
|
|
159
|
+
AND memory_type IN ('decision', 'learning', 'error', 'preference')
|
|
160
|
+
AND importance >= 5
|
|
161
|
+
AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
|
|
162
|
+
AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
|
|
163
|
+
ORDER BY importance DESC, accessed_at DESC LIMIT 5
|
|
164
|
+
`).all(project);
|
|
165
|
+
if (memories.length > 0) {
|
|
166
|
+
const typeIcons = {
|
|
167
|
+
decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
|
|
168
|
+
};
|
|
169
|
+
lines.push('## Key Memories');
|
|
170
|
+
for (const m of memories) {
|
|
171
|
+
const icon = typeIcons[m.memory_type] || '💭';
|
|
172
|
+
const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
|
|
173
|
+
lines.push(`- ${icon} ${content}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push('');
|
|
150
176
|
}
|
|
151
|
-
lines.push('');
|
|
152
177
|
}
|
|
178
|
+
catch { /* ignore */ }
|
|
153
179
|
db.close();
|
|
154
180
|
lines.push('---');
|
|
155
|
-
lines.push('_Auto-injected by session-continuity. Use `session_end` when done._');
|
|
181
|
+
lines.push('_Auto-injected by session-continuity v2. Use `session_end` when done._');
|
|
156
182
|
return lines.join('\n');
|
|
157
183
|
}
|
|
158
184
|
catch (e) {
|
|
@@ -286,21 +286,25 @@ function loadContext(dbPath, project, prompt) {
|
|
|
286
286
|
}
|
|
287
287
|
lines.push('');
|
|
288
288
|
}
|
|
289
|
-
// 중요 메모리
|
|
289
|
+
// 중요 메모리 (노이즈 필터링 - v1.10.0)
|
|
290
290
|
const memories = db.prepare(`
|
|
291
291
|
SELECT content, memory_type, importance FROM memories
|
|
292
292
|
WHERE project = ?
|
|
293
|
-
|
|
293
|
+
AND memory_type IN ('decision', 'learning', 'error', 'preference')
|
|
294
|
+
AND importance >= 5
|
|
295
|
+
AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
|
|
296
|
+
AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
|
|
297
|
+
ORDER BY importance DESC, accessed_at DESC LIMIT 5
|
|
294
298
|
`).all(project);
|
|
295
299
|
if (memories.length > 0) {
|
|
296
300
|
const typeIcons = {
|
|
297
|
-
|
|
301
|
+
decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
|
|
298
302
|
};
|
|
299
|
-
lines.push('##
|
|
303
|
+
lines.push('## Key Memories');
|
|
300
304
|
for (const m of memories) {
|
|
301
305
|
const icon = typeIcons[m.memory_type] || '💭';
|
|
302
306
|
const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
|
|
303
|
-
lines.push(`- ${icon}
|
|
307
|
+
lines.push(`- ${icon} ${content}`);
|
|
304
308
|
}
|
|
305
309
|
lines.push('');
|
|
306
310
|
}
|
package/dist/index.js
CHANGED
|
@@ -256,6 +256,17 @@ async function generateEmbedding(text, type = 'query') {
|
|
|
256
256
|
return null;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
function parseTags(tags) {
|
|
260
|
+
if (!tags)
|
|
261
|
+
return [];
|
|
262
|
+
try {
|
|
263
|
+
const parsed = JSON.parse(tags);
|
|
264
|
+
return Array.isArray(parsed) ? parsed : [String(parsed)];
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
259
270
|
function cosineSimilarity(a, b) {
|
|
260
271
|
if (a.length !== b.length)
|
|
261
272
|
return 0;
|
|
@@ -1482,7 +1493,7 @@ async function handleTool(name, args) {
|
|
|
1482
1493
|
content: m.content.substring(0, 300) + (m.content.length > 300 ? '...' : ''),
|
|
1483
1494
|
type: m.memory_type,
|
|
1484
1495
|
project: m.project || 'global',
|
|
1485
|
-
tags:
|
|
1496
|
+
tags: parseTags(m.tags),
|
|
1486
1497
|
importance: m.importance,
|
|
1487
1498
|
similarity: m.similarity ? Math.round(m.similarity * 100) + '%' : undefined,
|
|
1488
1499
|
created: m.created_at
|
|
@@ -1866,23 +1877,27 @@ async function generateProjectContext(project) {
|
|
|
1866
1877
|
}
|
|
1867
1878
|
lines.push('');
|
|
1868
1879
|
}
|
|
1869
|
-
// 5.
|
|
1880
|
+
// 5. 중요 메모리 (노이즈 필터링 - v1.10.0)
|
|
1870
1881
|
const recentMemories = db.prepare(`
|
|
1871
1882
|
SELECT id, content, memory_type, importance FROM memories
|
|
1872
1883
|
WHERE project = ?
|
|
1873
|
-
|
|
1884
|
+
AND memory_type IN ('decision', 'learning', 'error', 'preference')
|
|
1885
|
+
AND importance >= 5
|
|
1886
|
+
AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
|
|
1887
|
+
AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
|
|
1888
|
+
ORDER BY importance DESC, accessed_at DESC LIMIT 5
|
|
1874
1889
|
`).all(project);
|
|
1875
1890
|
if (recentMemories.length > 0) {
|
|
1876
|
-
lines.push(`## 🧠
|
|
1891
|
+
lines.push(`## 🧠 Key Memories`);
|
|
1877
1892
|
for (const mem of recentMemories) {
|
|
1878
1893
|
const typeIcon = {
|
|
1879
|
-
observation: '👀',
|
|
1880
1894
|
decision: '🎯',
|
|
1881
1895
|
learning: '📚',
|
|
1882
1896
|
error: '⚠️',
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1897
|
+
preference: '💡'
|
|
1898
|
+
};
|
|
1899
|
+
const icon = typeIcon[mem.memory_type] || '💭';
|
|
1900
|
+
lines.push(`- ${icon} ${mem.content.substring(0, 100)}${mem.content.length > 100 ? '...' : ''}`);
|
|
1886
1901
|
}
|
|
1887
1902
|
lines.push('');
|
|
1888
1903
|
}
|
|
@@ -1904,9 +1919,10 @@ async function generateProjectContext(project) {
|
|
|
1904
1919
|
}
|
|
1905
1920
|
async function generateRecentMemories(project, limit = 10) {
|
|
1906
1921
|
const lines = ['# 🧠 최근 메모리\n'];
|
|
1922
|
+
const noiseFilter = `AND memory_type IN ('decision','learning','error','preference') AND importance >= 5 AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL) AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)`;
|
|
1907
1923
|
const sql = project
|
|
1908
|
-
? `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE project = ? ORDER BY importance DESC, created_at DESC LIMIT ?`
|
|
1909
|
-
: `SELECT id, content, memory_type, project, importance, created_at FROM memories ORDER BY importance DESC, created_at DESC LIMIT ?`;
|
|
1924
|
+
? `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE project = ? ${noiseFilter} ORDER BY importance DESC, created_at DESC LIMIT ?`
|
|
1925
|
+
: `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE 1=1 ${noiseFilter} ORDER BY importance DESC, created_at DESC LIMIT ?`;
|
|
1910
1926
|
const memories = project
|
|
1911
1927
|
? db.prepare(sql).all(project, limit)
|
|
1912
1928
|
: db.prepare(sql).all(limit);
|
package/dist/tools/memory.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
// 메모리 시스템 도구 (7개)
|
|
2
2
|
import { db } from '../db/database.js';
|
|
3
3
|
import { generateEmbedding, embeddingToBuffer } from '../utils/embedding.js';
|
|
4
|
+
// 태그 파싱 헬퍼 (JSON 배열 또는 콤마구분 문자열 모두 처리)
|
|
5
|
+
function parseTags(tags) {
|
|
6
|
+
if (!tags)
|
|
7
|
+
return [];
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(tags);
|
|
10
|
+
return Array.isArray(parsed) ? parsed : [String(parsed)];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// 콤마구분 문자열 fallback (e.g. "auto-tracked,code,ts")
|
|
14
|
+
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
// ===== 도구 정의 =====
|
|
5
18
|
export const memoryTools = [
|
|
6
19
|
{
|
|
@@ -168,7 +181,7 @@ export function recallMemory(query, memoryType, project, limit = 10, minImportan
|
|
|
168
181
|
? row.content.slice(0, maxContentLength) + '...'
|
|
169
182
|
: row.content,
|
|
170
183
|
type: row.memory_type,
|
|
171
|
-
tags:
|
|
184
|
+
tags: parseTags(row.tags),
|
|
172
185
|
project: row.project,
|
|
173
186
|
importance: row.importance,
|
|
174
187
|
createdAt: row.created_at,
|
|
@@ -220,7 +233,7 @@ export function recallByTimeframe(timeframe, memoryType, project, limit = 20) {
|
|
|
220
233
|
id: row.id,
|
|
221
234
|
content: row.content,
|
|
222
235
|
type: row.memory_type,
|
|
223
|
-
tags:
|
|
236
|
+
tags: parseTags(row.tags),
|
|
224
237
|
project: row.project,
|
|
225
238
|
importance: row.importance,
|
|
226
239
|
createdAt: row.created_at
|
|
@@ -249,14 +262,14 @@ export function searchByTag(tags, matchAll = false, limit = 20) {
|
|
|
249
262
|
const conditions = tags.map(() => `tags LIKE ?`);
|
|
250
263
|
sql += matchAll ? conditions.join(' AND ') : conditions.join(' OR ');
|
|
251
264
|
sql += ` ORDER BY importance DESC, created_at DESC LIMIT ?`;
|
|
252
|
-
const params = [...tags.map(t =>
|
|
265
|
+
const params = [...tags.map(t => `%${t}%`), limit];
|
|
253
266
|
const stmt = db.prepare(sql);
|
|
254
267
|
const rows = stmt.all(...params);
|
|
255
268
|
const memories = rows.map(row => ({
|
|
256
269
|
id: row.id,
|
|
257
270
|
content: row.content,
|
|
258
271
|
type: row.memory_type,
|
|
259
|
-
tags:
|
|
272
|
+
tags: parseTags(row.tags),
|
|
260
273
|
project: row.project,
|
|
261
274
|
importance: row.importance,
|
|
262
275
|
createdAt: row.created_at
|
package/dist/tools-v2/memory.js
CHANGED
|
@@ -4,6 +4,18 @@ import { db } from '../db/database.js';
|
|
|
4
4
|
import { generateEmbedding, embeddingToBuffer, bufferToEmbedding, cosineSimilarity } from '../utils/embedding.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { MemoryStoreSchema, MemorySearchSchema, MemoryDeleteSchema } from '../schemas.js';
|
|
7
|
+
// 태그 파싱 헬퍼 (JSON 배열 또는 콤마구분 문자열 모두 처리)
|
|
8
|
+
function parseTags(tags) {
|
|
9
|
+
if (!tags)
|
|
10
|
+
return [];
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(tags);
|
|
13
|
+
return Array.isArray(parsed) ? parsed : [String(parsed)];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
// ===== 도구 정의 =====
|
|
8
20
|
export const memoryTools = [
|
|
9
21
|
{
|
|
@@ -178,7 +190,7 @@ async function performFTSSearch(query, type, project, limit = 10, minImportance
|
|
|
178
190
|
id: row.id,
|
|
179
191
|
content: row.content.length > 300 ? row.content.slice(0, 300) + '...' : row.content,
|
|
180
192
|
type: row.memory_type,
|
|
181
|
-
tags:
|
|
193
|
+
tags: parseTags(row.tags),
|
|
182
194
|
project: row.project,
|
|
183
195
|
importance: row.importance,
|
|
184
196
|
createdAt: row.created_at
|
|
@@ -241,7 +253,7 @@ async function performSemanticSearch(query, type, project, limit = 10, minImport
|
|
|
241
253
|
id: row.id,
|
|
242
254
|
content: row.content.length > 300 ? row.content.slice(0, 300) + '...' : row.content,
|
|
243
255
|
type: row.memory_type,
|
|
244
|
-
tags:
|
|
256
|
+
tags: parseTags(row.tags),
|
|
245
257
|
project: row.project,
|
|
246
258
|
importance: row.importance,
|
|
247
259
|
similarity: Math.round(row.similarity * 100) / 100,
|