claude-session-continuity-mcp 1.9.7 → 1.10.1
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 +263 -19
- package/dist/hooks/session-start.js +90 -64
- package/dist/hooks/user-prompt-submit.js +9 -5
- package/dist/index.js +26 -10
- 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, JSON.stringify(['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')
|
|
@@ -134,11 +134,13 @@ function extractSummaryFromText(content) {
|
|
|
134
134
|
if (cleaned.length > 10)
|
|
135
135
|
return cleaned.slice(0, 200);
|
|
136
136
|
}
|
|
137
|
-
// 전략 4: 첫 헤딩 제목
|
|
137
|
+
// 전략 4: 첫 헤딩 제목 — 단, 일반적인 섹션 헤딩은 제외
|
|
138
138
|
const headingMatch = cleanedContent.match(/^#{1,3}\s+(.+)$/m);
|
|
139
139
|
if (headingMatch?.[1]) {
|
|
140
140
|
const title = stripMarkdown(headingMatch[1]).trim();
|
|
141
|
-
|
|
141
|
+
// "결과 요약", "평가", "분석" 같은 일반 헤딩은 의미없는 요약이므로 건너뜀
|
|
142
|
+
const genericHeadings = /^(결과|요약|분석|평가|결론|테스트|현재|문제|핵심|다음|참고|MCP|Overview|Summary|Result|Analysis|Test)/i;
|
|
143
|
+
if (title.length > 5 && !genericHeadings.test(title))
|
|
142
144
|
return title.slice(0, 200);
|
|
143
145
|
}
|
|
144
146
|
// 전략 5: 첫 의미있는 단락 (노이즈 라인 건너뜀)
|
|
@@ -192,15 +194,13 @@ async function readRecentAssistantMessages(transcriptPath, maxMessages = 5) {
|
|
|
192
194
|
}
|
|
193
195
|
/**
|
|
194
196
|
* 다음 할 일 추출 (텍스트에서)
|
|
195
|
-
* 테이블 행, 코드블록 내부는 제외
|
|
196
197
|
*/
|
|
197
198
|
function extractNextTasks(content) {
|
|
198
199
|
const nextTasks = [];
|
|
199
|
-
// 코드블록과 테이블 행 제거
|
|
200
200
|
const cleaned = content
|
|
201
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
201
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
202
202
|
.split('\n')
|
|
203
|
-
.filter(line => !line.trim().startsWith('|'))
|
|
203
|
+
.filter(line => !line.trim().startsWith('|'))
|
|
204
204
|
.join('\n');
|
|
205
205
|
const nextPatterns = [
|
|
206
206
|
/(?:next steps?|todo|remaining|다음 (?:단계|작업|할 일)|남은 작업|해야 할)[:\s]*([^.!?\n]{10,})/gi,
|
|
@@ -217,6 +217,211 @@ function extractNextTasks(content) {
|
|
|
217
217
|
}
|
|
218
218
|
return nextTasks;
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* JSONL transcript에서 git commit 메시지 추출
|
|
222
|
+
*/
|
|
223
|
+
async function extractCommitMessages(transcriptPath) {
|
|
224
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
225
|
+
return [];
|
|
226
|
+
const commits = [];
|
|
227
|
+
// git commit -m "message" 또는 heredoc 패턴 (JSON 파싱 후 적용)
|
|
228
|
+
const commitPatterns = [
|
|
229
|
+
/git commit.*?-m\s*"\$\(cat <<'?EOF'?\n(.+?)(?:\n\n|\nCo-Authored|\nEOF)/s,
|
|
230
|
+
/git commit.*?-m\s*["']([^"'\n]{10,150})["']/,
|
|
231
|
+
];
|
|
232
|
+
try {
|
|
233
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
234
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
235
|
+
for await (const line of rl) {
|
|
236
|
+
if (!line.includes('git commit'))
|
|
237
|
+
continue;
|
|
238
|
+
try {
|
|
239
|
+
const entry = JSON.parse(line);
|
|
240
|
+
// tool_use 블록에서 command 추출
|
|
241
|
+
const content = entry.message?.content;
|
|
242
|
+
if (!Array.isArray(content))
|
|
243
|
+
continue;
|
|
244
|
+
for (const block of content) {
|
|
245
|
+
if (block.type !== 'tool_use')
|
|
246
|
+
continue;
|
|
247
|
+
const cmd = block.input?.command;
|
|
248
|
+
if (!cmd || !cmd.includes('-m'))
|
|
249
|
+
continue;
|
|
250
|
+
// git commit이 명령어의 시작이거나 && 이후에 나와야 함
|
|
251
|
+
if (!/(?:^|&&\s*)git\s+commit/.test(cmd))
|
|
252
|
+
continue;
|
|
253
|
+
for (const pattern of commitPatterns) {
|
|
254
|
+
const match = cmd.match(pattern);
|
|
255
|
+
if (match?.[1]) {
|
|
256
|
+
const msg = match[1].trim().split('\n')[0]; // 첫 줄만
|
|
257
|
+
if (msg.length > 10 && !msg.startsWith('Co-Authored')) {
|
|
258
|
+
commits.push(msg.slice(0, 150));
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch { /* skip malformed lines */ }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch { /* file read error */ }
|
|
269
|
+
return [...new Set(commits)].slice(0, 5);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* JSONL transcript에서 에러→해결 쌍 추출
|
|
273
|
+
*/
|
|
274
|
+
async function extractErrorFixPairs(transcriptPath) {
|
|
275
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
276
|
+
return [];
|
|
277
|
+
const pairs = [];
|
|
278
|
+
const entries = [];
|
|
279
|
+
try {
|
|
280
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
281
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
282
|
+
for await (const line of rl) {
|
|
283
|
+
if (!line.trim())
|
|
284
|
+
continue;
|
|
285
|
+
try {
|
|
286
|
+
const entry = JSON.parse(line);
|
|
287
|
+
const role = entry.type || entry.role || '';
|
|
288
|
+
let text = '';
|
|
289
|
+
if (typeof entry.message?.content === 'string') {
|
|
290
|
+
text = entry.message.content;
|
|
291
|
+
}
|
|
292
|
+
else if (Array.isArray(entry.message?.content)) {
|
|
293
|
+
text = entry.message.content
|
|
294
|
+
.filter((b) => b.type === 'text')
|
|
295
|
+
.map((b) => b.text)
|
|
296
|
+
.join('\n');
|
|
297
|
+
}
|
|
298
|
+
if (text.length > 5)
|
|
299
|
+
entries.push({ role, text: text.slice(0, 500) });
|
|
300
|
+
}
|
|
301
|
+
catch { /* skip */ }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch { /* file error */ }
|
|
305
|
+
const recent = entries.slice(-30);
|
|
306
|
+
const errorRe = /(?:error|Error|ERROR|오류|실패|FAILED|Exception)[:\s](.{5,80})/;
|
|
307
|
+
const fixRe = /(?:fixed|resolved|수정|해결|Added|수정 완료)/i;
|
|
308
|
+
for (let i = 0; i < recent.length - 1; i++) {
|
|
309
|
+
const errorMatch = recent[i].text.match(errorRe);
|
|
310
|
+
if (errorMatch) {
|
|
311
|
+
for (let j = i + 1; j < Math.min(i + 4, recent.length); j++) {
|
|
312
|
+
if (recent[j].role === 'assistant' && fixRe.test(recent[j].text)) {
|
|
313
|
+
const errorStr = stripMarkdown(errorMatch[0]).slice(0, 80);
|
|
314
|
+
const fixLine = recent[j].text.split('\n')
|
|
315
|
+
.find(l => fixRe.test(l));
|
|
316
|
+
const fixStr = fixLine ? stripMarkdown(fixLine).slice(0, 80) : 'resolved';
|
|
317
|
+
pairs.push(`${errorStr} → ${fixStr}`);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return [...new Set(pairs)].slice(0, 3);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* JSONL transcript에서 결정 사항 추출
|
|
327
|
+
*/
|
|
328
|
+
async function extractDecisions(transcriptPath) {
|
|
329
|
+
const messages = await readRecentAssistantMessages(transcriptPath, 10);
|
|
330
|
+
const decisions = [];
|
|
331
|
+
const decisionPatterns = [
|
|
332
|
+
/(?:chose|using|switched to|went with)\s+(.{10,80})\s+(?:because|since|instead of|over)/gi,
|
|
333
|
+
/(?:instead of|rather than)\s+(.{10,60})/gi,
|
|
334
|
+
/(.{10,60})(?:으로|로)\s+(?:결정|변경|전환)(?:했|함|합니다)/g,
|
|
335
|
+
/(.{10,60})(?:대신|말고)\s+(.{10,60})(?:사용|적용)/g,
|
|
336
|
+
];
|
|
337
|
+
for (const msg of messages) {
|
|
338
|
+
for (const pattern of decisionPatterns) {
|
|
339
|
+
pattern.lastIndex = 0;
|
|
340
|
+
const matches = msg.match(pattern);
|
|
341
|
+
if (matches) {
|
|
342
|
+
decisions.push(...matches.slice(0, 1).map(m => stripMarkdown(m).slice(0, 150)));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return [...new Set(decisions)].slice(0, 3);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* transcript에서 첫 사용자 요청 추출 (세션의 핵심 목적)
|
|
350
|
+
*/
|
|
351
|
+
async function extractUserRequest(transcriptPath) {
|
|
352
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
353
|
+
return '';
|
|
354
|
+
try {
|
|
355
|
+
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
356
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
357
|
+
for await (const line of rl) {
|
|
358
|
+
if (!line.trim())
|
|
359
|
+
continue;
|
|
360
|
+
try {
|
|
361
|
+
const entry = JSON.parse(line);
|
|
362
|
+
if (entry.type !== 'human' && entry.type !== 'user')
|
|
363
|
+
continue;
|
|
364
|
+
const content = entry.message?.content;
|
|
365
|
+
let text = '';
|
|
366
|
+
if (typeof content === 'string') {
|
|
367
|
+
text = content;
|
|
368
|
+
}
|
|
369
|
+
else if (Array.isArray(content)) {
|
|
370
|
+
text = content
|
|
371
|
+
.filter((b) => b.type === 'text')
|
|
372
|
+
.map((b) => b.text)
|
|
373
|
+
.join('\n');
|
|
374
|
+
}
|
|
375
|
+
// system-reminder 태그 제거
|
|
376
|
+
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
377
|
+
if (text.length < 5)
|
|
378
|
+
continue;
|
|
379
|
+
// 시스템/메타 메시지 스킵
|
|
380
|
+
if (text.startsWith('[Request interrupted'))
|
|
381
|
+
continue;
|
|
382
|
+
if (text.startsWith('This session is being continued'))
|
|
383
|
+
continue;
|
|
384
|
+
// "Implement the following plan:" → 실제 플랜 제목만 추출
|
|
385
|
+
const planMatch = text.match(/^Implement the following plan:\s*\n+#\s*(.+)/);
|
|
386
|
+
if (planMatch)
|
|
387
|
+
return planMatch[1].trim().slice(0, 200);
|
|
388
|
+
// 긴 텍스트는 첫 줄만 (플랜, 컨텍스트 덤프 등)
|
|
389
|
+
const firstLine = text.split('\n')[0].trim();
|
|
390
|
+
if (firstLine.length > 200)
|
|
391
|
+
return firstLine.slice(0, 200);
|
|
392
|
+
return stripMarkdown(firstLine).slice(0, 200);
|
|
393
|
+
}
|
|
394
|
+
catch { /* skip */ }
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch { /* file read error */ }
|
|
398
|
+
return '';
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 최근 assistant 메시지에서 액션 동사 포함 라인 추출 (lastWork 폴백)
|
|
402
|
+
*/
|
|
403
|
+
async function extractWorkFromRecentMessages(transcriptPath) {
|
|
404
|
+
const messages = await readRecentAssistantMessages(transcriptPath, 5);
|
|
405
|
+
const actionVerbs = /(?:created|modified|added|removed|fixed|updated|implemented|deployed|configured|refactored|만들|수정|추가|삭제|구현|배포|설정|완료)/i;
|
|
406
|
+
const filePath = /(?:\/[\w.-]+){2,}|[\w.-]+\.\w{1,4}/;
|
|
407
|
+
for (const msg of messages.reverse()) {
|
|
408
|
+
const lines = msg.split('\n').filter(l => !isNoiseLine(l));
|
|
409
|
+
for (const line of lines) {
|
|
410
|
+
if (actionVerbs.test(line) && line.length > 20) {
|
|
411
|
+
const cleaned = stripMarkdown(line).trim();
|
|
412
|
+
if (cleaned.length > 15)
|
|
413
|
+
return cleaned.slice(0, 200);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// 파일 경로 포함 라인도 시도
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (filePath.test(line) && actionVerbs.test(line)) {
|
|
419
|
+
return stripMarkdown(line).trim().slice(0, 200);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return '';
|
|
424
|
+
}
|
|
220
425
|
async function main() {
|
|
221
426
|
try {
|
|
222
427
|
let inputData = '';
|
|
@@ -238,15 +443,43 @@ async function main() {
|
|
|
238
443
|
process.exit(0);
|
|
239
444
|
}
|
|
240
445
|
const db = new Database(dbPath);
|
|
241
|
-
// ===
|
|
446
|
+
// === 추출 시작 ===
|
|
242
447
|
let lastWork = '';
|
|
243
448
|
let nextTasks = [];
|
|
244
|
-
|
|
245
|
-
|
|
449
|
+
let commitMessages = [];
|
|
450
|
+
let errorsSolved = [];
|
|
451
|
+
let decisions = [];
|
|
452
|
+
// Phase 1: transcript_path에서 고품질 데이터 추출
|
|
453
|
+
let userRequest = '';
|
|
454
|
+
if (input.transcript_path) {
|
|
455
|
+
[commitMessages, errorsSolved, decisions, userRequest] = await Promise.all([
|
|
456
|
+
extractCommitMessages(input.transcript_path),
|
|
457
|
+
extractErrorFixPairs(input.transcript_path),
|
|
458
|
+
extractDecisions(input.transcript_path),
|
|
459
|
+
extractUserRequest(input.transcript_path),
|
|
460
|
+
]);
|
|
461
|
+
}
|
|
462
|
+
// Phase 2: lastWork 결정 (우선순위 폴백)
|
|
463
|
+
// 2a: 사용자 요청 + 커밋 메시지 조합 (가장 이상적)
|
|
464
|
+
if (userRequest && commitMessages.length > 0) {
|
|
465
|
+
lastWork = `${userRequest} → ${commitMessages.slice(0, 2).join('; ')}`;
|
|
466
|
+
if (lastWork.length > 250)
|
|
467
|
+
lastWork = lastWork.slice(0, 250);
|
|
468
|
+
}
|
|
469
|
+
// 2b: 커밋 메시지만 (사용자 요청 없을 때)
|
|
470
|
+
else if (commitMessages.length > 0) {
|
|
471
|
+
lastWork = commitMessages.slice(0, 3).join('; ');
|
|
472
|
+
}
|
|
473
|
+
// 2c: 사용자 요청만 (커밋 없을 때)
|
|
474
|
+
else if (userRequest) {
|
|
475
|
+
lastWork = userRequest;
|
|
476
|
+
}
|
|
477
|
+
// 2d: last_assistant_message에서 추출
|
|
478
|
+
if (!lastWork && input.last_assistant_message) {
|
|
246
479
|
lastWork = extractSummaryFromText(input.last_assistant_message);
|
|
247
480
|
nextTasks = extractNextTasks(input.last_assistant_message);
|
|
248
481
|
}
|
|
249
|
-
//
|
|
482
|
+
// 2c: transcript에서 최근 assistant 메시지 스캔
|
|
250
483
|
if (!lastWork && input.transcript_path) {
|
|
251
484
|
const recentMessages = await readRecentAssistantMessages(input.transcript_path);
|
|
252
485
|
for (let i = recentMessages.length - 1; i >= 0; i--) {
|
|
@@ -254,12 +487,15 @@ async function main() {
|
|
|
254
487
|
if (lastWork)
|
|
255
488
|
break;
|
|
256
489
|
}
|
|
257
|
-
|
|
258
|
-
if (recentMessages.length > 0) {
|
|
490
|
+
if (recentMessages.length > 0 && nextTasks.length === 0) {
|
|
259
491
|
nextTasks = extractNextTasks(recentMessages[recentMessages.length - 1]);
|
|
260
492
|
}
|
|
261
493
|
}
|
|
262
|
-
//
|
|
494
|
+
// 2d: 액션 동사 기반 폴백
|
|
495
|
+
if (!lastWork && input.transcript_path) {
|
|
496
|
+
lastWork = await extractWorkFromRecentMessages(input.transcript_path);
|
|
497
|
+
}
|
|
498
|
+
// 2e: 레거시 transcript 배열
|
|
263
499
|
if (!lastWork && input.transcript) {
|
|
264
500
|
const assistantMsgs = input.transcript.filter(m => m.role === 'assistant');
|
|
265
501
|
if (assistantMsgs.length < 2) {
|
|
@@ -273,7 +509,7 @@ async function main() {
|
|
|
273
509
|
break;
|
|
274
510
|
}
|
|
275
511
|
}
|
|
276
|
-
// === modified_files
|
|
512
|
+
// === modified_files ===
|
|
277
513
|
let modifiedFiles = [];
|
|
278
514
|
try {
|
|
279
515
|
const activeCtx = db.prepare('SELECT recent_files FROM active_context WHERE project = ?').get(project);
|
|
@@ -282,7 +518,7 @@ async function main() {
|
|
|
282
518
|
}
|
|
283
519
|
}
|
|
284
520
|
catch { /* active_context may not exist */ }
|
|
285
|
-
// last_work 폴백: 파일 목록 기반
|
|
521
|
+
// last_work 최종 폴백: 파일 목록 기반
|
|
286
522
|
if (!lastWork && modifiedFiles.length > 0) {
|
|
287
523
|
const fileNames = modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
|
|
288
524
|
lastWork = `Modified files: ${fileNames}`;
|
|
@@ -293,7 +529,7 @@ async function main() {
|
|
|
293
529
|
db.close();
|
|
294
530
|
process.exit(0);
|
|
295
531
|
}
|
|
296
|
-
// 중복 저장
|
|
532
|
+
// 중복 저장 방지
|
|
297
533
|
const recentDup = db.prepare(`
|
|
298
534
|
SELECT id FROM sessions
|
|
299
535
|
WHERE project = ? AND last_work = ? AND timestamp > datetime('now', '-60 seconds')
|
|
@@ -304,11 +540,18 @@ async function main() {
|
|
|
304
540
|
db.close();
|
|
305
541
|
process.exit(0);
|
|
306
542
|
}
|
|
543
|
+
// 구조화 메타데이터 (issues 컬럼 활용)
|
|
544
|
+
const metadata = {
|
|
545
|
+
commits: commitMessages,
|
|
546
|
+
decisions,
|
|
547
|
+
errorsSolved
|
|
548
|
+
};
|
|
549
|
+
const hasMetadata = commitMessages.length > 0 || decisions.length > 0 || errorsSolved.length > 0;
|
|
307
550
|
// 세션 기록 저장
|
|
308
551
|
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)));
|
|
552
|
+
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues)
|
|
553
|
+
VALUES (?, ?, ?, ?, ?)
|
|
554
|
+
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null);
|
|
312
555
|
// 활성 컨텍스트 업데이트
|
|
313
556
|
db.prepare(`
|
|
314
557
|
INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
|
|
@@ -317,6 +560,7 @@ async function main() {
|
|
|
317
560
|
db.close();
|
|
318
561
|
console.log(`[SessionEnd] Saved session for ${project}`);
|
|
319
562
|
console.log(` Last work: ${lastWork.slice(0, 80)}`);
|
|
563
|
+
console.log(` Commits: ${commitMessages.length}, Decisions: ${decisions.length}, Errors: ${errorsSolved.length}`);
|
|
320
564
|
console.log(` Modified files: ${modifiedFiles.length}`);
|
|
321
565
|
console.log(` Next tasks: ${nextTasks.length}`);
|
|
322
566
|
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);
|