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 CHANGED
@@ -1,4 +1,4 @@
1
- # claude-session-continuity-mcp (v1.9.0)
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 (Git-based semantic search)
29
- # When asking → Auto-injects memories/solutions related to your query
30
- # During conversation → Auto-captures important decisions/errors/learnings
31
- # On commitCommit messages automatically become memories
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 compactStructured 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
- # 🚀 my-app - Session Resumed
37
+ # my-app - Session Resumed
37
38
 
38
- ## Tech Stack
39
- **framework**: Next.js, **language**: TypeScript
39
+ 📍 **State**: Implementing signup form
40
40
 
41
- ## Current State
42
- 📍 Implementing signup form
43
- 🚧 **Blocker**: OAuth callback URL issue
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
- ## 🧠 Relevant Memories (semantic: 0.89)
46
- - 🎯 [decision] Decided on App Router, using Server Actions
47
- - ⚠️ [error] OAuth redirect_uri mismatch → check env file
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 file changes (Edit, Write) automatically |
105
- | `PreCompact` | `claude-hook-pre-compact` | Saves important context before compression |
106
- | `Stop` | `claude-hook-session-end` | Auto-saves session on exit (no manual call needed) |
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
- | 🎯 **Query-Based Injection** | Selectively inject only relevant memories/solutions |
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-memorized |
142
+ | 🔗 **Git Integration** | Commit messages auto-extracted from transcripts |
136
143
  | 🕸️ **Knowledge Graph** | Memory relations (solves, causes, extends...) |
137
- | 📊 **Memory Classification** | 6 types: observation, decision, learning, error, pattern, code |
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
- | 📁 **File Change Tracking** | **(v1.5.0)** Auto-track Edit/Write tool usage |
142
- | 💾 **Auto Backup** | **(v1.5.0)** Daily SQLite backup (max 5) |
143
- | 🛡️ **PreCompact Save** | **(v1.5.0)** Save context before compression |
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 `~/.claude/sessions.db`
157
- - Injects: Tech stack, current state, pending tasks, recent memories
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 based on current project
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
- # 🚀 my-app - Session Resumed
167
-
168
- ## Tech Stack
169
- **framework**: Next.js, **language**: TypeScript
187
+ # my-app - Session Resumed
170
188
 
171
- ## Current State
172
- 📍 Implementing signup form
189
+ 📍 **State**: Implementing signup form
173
190
  🚧 **Blocker**: OAuth callback URL issue
174
191
 
175
- ## 📋 Pending Tasks
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
- ## 🧠 Key Memories
180
- - 🎯 [decision] Decided on App Router, using Server Actions
181
- - ⚠️ [error] OAuth redirect_uri mismatch → check env file
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
- const today = new Date().toISOString().slice(0, 10);
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 extractKeyPoints(transcript) {
59
- const keyPoints = [];
60
- // 최근 메시지에서 중요 패턴 추출
61
- const recentMessages = transcript.slice(-20);
62
- for (const msg of recentMessages) {
63
- if (msg.role !== 'assistant')
64
- continue;
65
- const content = msg.content;
66
- // 결정 사항 패턴
67
- const decisionPatterns = [
68
- /(?:decided|결정|선택)[^.]*\./gi,
69
- /(?:will use|사용할)[^.]*\./gi,
70
- /(?:approach|방식)[^.]*\./gi,
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 decisionPatterns) {
73
- const matches = content.match(pattern);
74
- if (matches) {
75
- keyPoints.push(...matches.slice(0, 2));
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
- const errorPatterns = [
80
- /(?:fixed|수정|해결)[^.]*(?:error|bug|issue|오류|버그)[^.]*\./gi,
81
- /(?:error|bug|issue|오류|버그)[^.]*(?:fixed|수정|해결)[^.]*\./gi,
82
- ];
83
- for (const pattern of errorPatterns) {
84
- const matches = content.match(pattern);
85
- if (matches) {
86
- keyPoints.push(...matches.slice(0, 2));
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
- const unique = [...new Set(keyPoints)].slice(0, 5);
92
- return unique.map(p => p.slice(0, 200));
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
- // transcript에서 핵심 포인트 추출
110
- const keyPoints = input.transcript ? extractKeyPoints(input.transcript) : [];
111
- if (keyPoints.length > 0) {
112
- // 중요 메모리로 저장
113
- db.prepare(`
114
- INSERT INTO memories (content, memory_type, project, importance, tags)
115
- VALUES (?, 'pattern', ?, 8, '["auto-compact","session-summary"]')
116
- `).run(`[Pre-Compact Summary] ${keyPoints.join(' | ')}`, project);
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, `Compacted: ${keyPoints[0]?.slice(0, 50) || 'Session context saved'}`);
173
+ `).run(project, stateStr.slice(0, 300));
122
174
  }
123
175
  // === 컨텍스트 재주입: systemMessage로 반환 ===
124
- const recoveryLines = [`# ${project} - Recovered Context\n`];
125
- // 사용자 지시사항 (HIGH 우선)
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
- // Hot paths (상위 5개)
158
- try {
159
- const hotPaths = db.prepare(`
160
- SELECT file_path, access_count FROM hot_paths
161
- WHERE project = ? AND last_accessed > datetime('now', '-7 days')
162
- ORDER BY access_count DESC LIMIT 5
163
- `).all(project);
164
- if (hotPaths.length > 0) {
165
- recoveryLines.push(`**Hot Files**: ${hotPaths.map(h => h.file_path.split('/').pop()).join(', ')}`);
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
- catch { /* table may not exist */ }
169
- // Key points from this session
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
- if (title.length > 5)
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
- // === last_work 추출 ===
446
+ // === 추출 시작 ===
242
447
  let lastWork = '';
243
448
  let nextTasks = [];
244
- // 소스 1: last_assistant_message (Stop 이벤트에서 직접 제공)
245
- if (input.last_assistant_message) {
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
- // 소스 2: transcript_path에서 마지막 assistant 메시지들 읽기 (소스 1 실패 시)
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
- // next tasks도 마지막 메시지에서 추출
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
- // 소스 3: 레거시 transcript 배열 (이전 버전 호환)
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: active_context에서 PostToolUse가 실시간 저장한 파일 목록 ===
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
- // 중복 저장 방지: 최근 60초 이내 동일 last_work
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, { readonly: true });
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(`## Current State\n📍 ${active.current_state}`);
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
- // 마지막 세션 (빈 세션 skip)
69
- const last = db.prepare(`
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
- ORDER BY timestamp DESC LIMIT 1
77
- `).get(project);
78
- if (last?.last_work) {
79
- lines.push(`## Last Session (${last.timestamp?.slice(0, 10) || 'unknown'})`);
80
- lines.push(`**Work**: ${last.last_work}`);
81
- if (last.next_tasks) {
82
- const next = JSON.parse(last.next_tasks);
83
- if (next.length > 0)
84
- lines.push(`**Next**: ${next.slice(0, 3).join(' ')}`);
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('## 📌 Directives');
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
- // Hot Files (최근 7일, 상위 10개)
137
+ // 미완료 태스크
105
138
  try {
106
- const hotPaths = db.prepare(`
107
- SELECT file_path, access_count FROM hot_paths
108
- WHERE project = ? AND last_accessed > datetime('now', '-7 days')
109
- ORDER BY access_count DESC LIMIT 10
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 (hotPaths.length > 0) {
112
- lines.push('## 🔥 Hot Files');
113
- for (const h of hotPaths) {
114
- const fileName = h.file_path.split('/').pop() || h.file_path;
115
- lines.push(`- ${fileName} (${h.access_count}x)`);
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 yet */ }
121
- // 미완료 태스크
122
- const tasks = db.prepare(`
123
- SELECT title, priority, status FROM tasks
124
- WHERE project = ? AND status IN ('pending', 'in_progress')
125
- ORDER BY priority DESC LIMIT 5
126
- `).all(project);
127
- if (tasks.length > 0) {
128
- lines.push('## 📋 Pending Tasks');
129
- for (const t of tasks) {
130
- const icon = t.status === 'in_progress' ? '🔄' : '⏳';
131
- lines.push(`- ${icon} [P${t.priority}] ${t.title}`);
132
- }
133
- lines.push('');
134
- }
135
- // 중요 메모리
136
- const memories = db.prepare(`
137
- SELECT content, memory_type FROM memories
138
- WHERE project = ?
139
- ORDER BY importance DESC, created_at DESC LIMIT 5
140
- `).all(project);
141
- if (memories.length > 0) {
142
- const typeIcons = {
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
- ORDER BY importance DESC, created_at DESC LIMIT 5
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
- observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
301
+ decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
298
302
  };
299
- lines.push('## 🧠 Key Memories');
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} [${m.memory_type}] ${content}`);
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: m.tags ? JSON.parse(m.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. 최근 관련 메모리 (중요도 높은 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
- ORDER BY importance DESC, created_at DESC LIMIT 5
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
- pattern: '🔄'
1884
- }[mem.memory_type] || '💭';
1885
- lines.push(`- ${typeIcon} [${mem.memory_type}] ${mem.content.substring(0, 100)}${mem.content.length > 100 ? '...' : ''}`);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.9.7",
3
+ "version": "1.10.1",
4
4
  "description": "Session Continuity for Claude Code - Never re-explain your project again",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",