claude-memory-layer 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +372 -74
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/hooks/post-tool-use.js +6 -0
  5. package/dist/hooks/post-tool-use.js.map +2 -2
  6. package/dist/hooks/session-end.js +6 -0
  7. package/dist/hooks/session-end.js.map +2 -2
  8. package/dist/hooks/session-start.js +29 -13
  9. package/dist/hooks/session-start.js.map +2 -2
  10. package/dist/hooks/stop.js +6 -0
  11. package/dist/hooks/stop.js.map +2 -2
  12. package/dist/hooks/user-prompt-submit.js +245 -31
  13. package/dist/hooks/user-prompt-submit.js.map +3 -3
  14. package/dist/server/api/index.js +329 -31
  15. package/dist/server/api/index.js.map +3 -3
  16. package/dist/server/index.js +336 -38
  17. package/dist/server/index.js.map +3 -3
  18. package/dist/services/memory-service.js +6 -0
  19. package/dist/services/memory-service.js.map +2 -2
  20. package/dist/ui/app.js +236 -4
  21. package/dist/ui/index.html +51 -0
  22. package/dist/ui/style.css +34 -0
  23. package/memory/_index.md +4 -0
  24. package/memory/agent_response/uncategorized/2026-02-26.md +151 -1
  25. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  26. package/memory/session_summary/uncategorized/2026-02-26.md +13 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-02-26.md +9 -1
  29. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  30. package/memory/user_prompt/uncategorized/2026-02-26.md +9 -0
  31. package/package.json +3 -2
  32. package/scripts/delete-unknown-projects.js +154 -0
  33. package/src/hooks/session-start.ts +9 -3
  34. package/src/hooks/user-prompt-submit.ts +225 -29
  35. package/src/server/api/events.ts +1 -0
  36. package/src/server/api/stats.ts +346 -0
  37. package/src/services/memory-service.ts +7 -0
  38. package/src/ui/app.js +236 -4
  39. package/src/ui/index.html +51 -0
  40. package/src/ui/style.css +34 -0
@@ -0,0 +1,13 @@
1
+
2
+ ## 2026-02-26T13:53:54.921Z | 3fa78495-3f90-45ff-a85c-43dc29ddedb7
3
+ - type: session_summary
4
+ - session: bench-session-2
5
+ Session with 1 user prompts and 6 responses.
6
+ Topics discussed:
7
+ - 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
8
+ ## 2026-02-26T13:54:46.584Z | cb94a8e4-4a9d-4bfc-8ee5-1458f5e304e7
9
+ - type: session_summary
10
+ - session: bench-session-3
11
+ Session with 1 user prompts and 6 responses.
12
+ Topics discussed:
13
+ - 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
@@ -0,0 +1,5 @@
1
+
2
+ ## 2026-03-03T14:04:47.970Z | 7dfbcdd9-f49f-482d-a0c6-9779331cc5cf
3
+ - type: session_summary
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ Session with 0 user prompts and 1 responses.
@@ -198,4 +198,12 @@
198
198
  ## 2026-02-26T12:51:57.850Z | 3b09d82b-7fa3-4673-978e-87ec65d901dd
199
199
  - type: tool_observation
200
200
  - session: 2f324dae-7d8f-4f67-842e-acf507b79c59
201
- {"toolName":"Bash","toolInput":{"command":"git add src/core/sqlite-event-store.ts && git commit -m \"$(cat <<'EOF'\nfix(api): handle missing retrieval_traces table in read-only mode\n\nDashboard API creates read-only MemoryService which skips schema\ncreation. If the DB was created before retrieval_traces was added,\nqueries fail with \"no such table\". Now returns empty results gracefully.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit retrieval_traces fix"},"toolOutput":"[main 60d59c6] fix(api): handle missing retrieval_traces table in read-only mode\n 1 file changed, 52 insertions(+), 40 deletions(-)","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"6ee6b497-8756-43c8-a208-8830a70efc1a"}}
201
+ {"toolName":"Bash","toolInput":{"command":"git add src/core/sqlite-event-store.ts && git commit -m \"$(cat <<'EOF'\nfix(api): handle missing retrieval_traces table in read-only mode\n\nDashboard API creates read-only MemoryService which skips schema\ncreation. If the DB was created before retrieval_traces was added,\nqueries fail with \"no such table\". Now returns empty results gracefully.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit retrieval_traces fix"},"toolOutput":"[main 60d59c6] fix(api): handle missing retrieval_traces table in read-only mode\n 1 file changed, 52 insertions(+), 40 deletions(-)","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"6ee6b497-8756-43c8-a208-8830a70efc1a"}}
202
+ ## 2026-02-26T13:53:54.436Z | 4ced656c-1426-44fd-9f2d-148c5432c1f8
203
+ - type: tool_observation
204
+ - session: bench-session-2
205
+ {"toolName":"Bash","toolInput":{"cmd":"echo hello"},"toolOutput":"hello\n","durationMs":0,"success":true,"metadata":{"turnId":"ccd580af-30da-4227-8a48-6ce91c3bfeba"}}
206
+ ## 2026-02-26T13:54:46.108Z | 0fa770e5-f71d-4eda-be4d-7a6529171718
207
+ - type: tool_observation
208
+ - session: bench-session-3
209
+ {"toolName":"Bash","toolInput":{"cmd":"echo hello"},"toolOutput":"hello\n","durationMs":0,"success":true,"metadata":{"turnId":"03e597c8-295a-43d8-9959-bc77607d30f8"}}
@@ -0,0 +1,21 @@
1
+
2
+ ## 2026-03-03T14:03:38.204Z | 46d8dc3c-a7ce-4602-ae18-daf4ed0266f1
3
+ - type: tool_observation
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ {"toolName":"Skill","toolInput":{"skill":"commit"},"toolOutput":"{\"success\":true,\"commandName\":\"commit\"}","durationMs":0,"success":true,"metadata":{"turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
6
+ ## 2026-03-03T14:03:43.897Z | ae933ad8-5f37-4a0b-9028-85bb29bdb6fc
7
+ - type: tool_observation
8
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
9
+ {"toolName":"Bash","toolInput":{"command":"git status && git diff && git log --oneline -5","description":"Check git status, diff, and recent commits"},"toolOutput":"On branch main\nYour branch is up to date with 'origin/main'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified: package-lock.json\n\tmodified: package.json\n\tmodified: src/cli/index.ts\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\t.npm-cache/\n\tbootstrap-kb/\n\tclaude-memory-layer-1.0.14.tgz\n\td.sh\n\tdeploy.sh\n\tmemory/\n\tscripts/delete-unknown-projects.js\n\tyarn.lock\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\ndiff --git package-lock.json package-lock.json\nindex c01b2e9..1181958 100644\n--- package-lock.json\n+++ package-lock.json\n@@ -1,12 +1,12 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"lockfileVersion\": 3,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\ndiff --git package.json package.json\nindex bd41ac1..453d235 100644\n--- package.json\n+++ package.json\n@@ -1,6 +1,6 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n\n... [121 lines truncated] ...\n\n delete settings.hooks.PostToolUse;\n+ delete settings.hooks.Stop;\n+ delete settings.hooks.SessionEnd;\n \n // Clean up empty hooks object\n if (Object.keys(settings.hooks).length === 0) {\n@@ -219,19 +238,22 @@ program\n console.log('\\n🧠 Claude Memory Layer Status\\n');\n \n // Check hooks\n- const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))\n- );\n- const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('post-tool-use'))\n- );\n+ const hasSessionStartHook = hasHook(settings, 'SessionStart', 'session-start');\n+ const hasUserPromptHook = hasHook(settings, 'UserPromptSubmit', 'user-prompt-submit');\n+ const hasPostToolHook = hasHook(settings, 'PostToolUse', 'post-tool-use');\n+ const hasStopHook = hasHook(settings, 'Stop', 'stop');\n+ const hasSessionEndHook = hasHook(settings, 'SessionEnd', 'session-end');\n \n console.log('Hooks:');\n+ console.log(` SessionStart: ${hasSessionStartHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` UserPromptSubmit: ${hasUserPromptHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` PostToolUse: ${hasPostToolHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` Stop: ${hasStopHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` SessionEnd: ${hasSessionEndHook ? '✅ Installed' : '❌ Not installed'}`);\n \n // Check plugin files\n- const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));\n+ const hooksExist = REQUIRED_HOOK_FILES\n+ .every((file) => fs.existsSync(path.join(pluginPath, 'hooks', file)));\n console.log(`\\nPlugin files: ${hooksExist ? '✅ Found' : '❌ Not found'}`);\n console.log(` Path: ${pluginPath}`);\n \n@@ -239,7 +261,7 @@ program\n const dashboardRunning = await isServerRunning(37777);\n console.log(`\\nDashboard: ${dashboardRunning ? '✅ Running at http://localhost:37777' : '⏹️ Not running'}`);\n \n- if (!hasUserPromptHook || !hasPostToolHook) {\n+ if (!hasSessionStartHook || !hasUserPromptHook || !hasPostToolHook || !hasStopHook || !hasSessionEndHook) {\n console.log('\\n💡 Run \"claude-memory-layer install\" to set up hooks.\\n');\n } else {\n console.log('\\n✅ Plugin is fully installed and configured.\\n');\n257241b fix(hook): use lightweight service in session-start\n60d59c6 fix(api): handle missing retrieval_traces table in read-only mode\n1ab58f2 feat(build): inject version from package.json at build time\n4d48779 tune(hook): raise default injection threshold with fallback retry for recall\n3c618db fix(import): normalize underscore/hyphen path variants for project folder matching","durationMs":0,"success":true,"metadata":{"command":"git status","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
10
+ ## 2026-03-03T14:03:55.698Z | 101afa33-2fd1-4008-88b8-c824465ccd69
11
+ - type: tool_observation
12
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
13
+ {"toolName":"Bash","toolInput":{"command":"cat scripts/delete-unknown-projects.js","description":"Check new script content"},"toolOutput":"#!/usr/bin/env node\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nfunction parseArgs(argv) {\n const result = {\n apply: false,\n hashes: []\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === '--apply') {\n result.apply = true;\n continue;\n }\n if (arg === '--hash' && i + 1 < argv.length) {\n result.hashes.push(String(argv[i + 1]).trim().toLowerCase());\n i += 1;\n continue;\n }\n if (arg.startsWith('--hash=')) {\n result.hashes.push(arg.slice('--hash='.length).trim().toLowerCase());\n }\n }\n\n return result;\n}\n\nfunction loadRegistryHashes(registryPath) {\n if (!fs.existsSync(registryPath)) return new Set();\n try {\n const raw = JSON.parse(fs.readFileSync(registryPath, 'utf8'));\n const sessions = raw && typeof raw === 'object' ? raw.sessions : null;\n if (!sessions || typeof sessions !== 'object') return new Set();\n\n const hashes = new Set();\n for (const entry of Object.values(sessions)) {\n if (!entry || typeof entry !== 'object') continue;\n const hash = entry.projectHash;\n if (typeof hash === 'string' && /^[a-f0-9]{8}$/.test(hash)) {\n hashes.add(hash);\n }\n }\n return hashes;\n } catch {\n return new Set();\n }\n}\n\n... [54 lines truncated] ...\n\n .map((entry) => entry.name)\n .filter((name) => /^[a-f0-9]{8}$/.test(name))\n .filter((hash) => !knownHashes.has(hash))\n .filter((hash) => filterHashes.size === 0 || filterHashes.has(hash));\n\n const projectInfo = candidates.map((hash) => {\n const fullPath = path.join(projectsDir, hash);\n const sizeBytes = getDirSizeBytes(fullPath);\n return {\n hash,\n path: fullPath,\n sizeBytes,\n sizeHuman: formatBytes(sizeBytes)\n };\n });\n\n const totalBytes = projectInfo.reduce((sum, item) => sum + item.sizeBytes, 0);\n\n if (!args.apply) {\n console.log(JSON.stringify({\n status: 'dry-run',\n message: 'Run with --apply to delete the directories below.',\n memoryRoot,\n projectsDir,\n registryPath,\n unknownCount: projectInfo.length,\n totalSizeBytes: totalBytes,\n totalSizeHuman: formatBytes(totalBytes),\n projects: projectInfo\n }, null, 2));\n process.exit(0);\n }\n\n let deleted = 0;\n for (const item of projectInfo) {\n fs.rmSync(item.path, { recursive: true, force: true });\n deleted += 1;\n }\n\n console.log(JSON.stringify({\n status: 'ok',\n mode: 'apply',\n deletedCount: deleted,\n deletedSizeBytes: totalBytes,\n deletedSizeHuman: formatBytes(totalBytes),\n deletedHashes: projectInfo.map((item) => item.hash)\n }, null, 2));\n}\n\nmain();","durationMs":0,"success":true,"metadata":{"command":"cat","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
14
+ ## 2026-03-03T14:04:06.818Z | e92edb69-bd60-41e4-a490-08a7c50deadd
15
+ - type: tool_observation
16
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
17
+ {"toolName":"Bash","toolInput":{"command":"git add package.json package-lock.json src/cli/index.ts scripts/delete-unknown-projects.js","description":"Stage relevant changed and new files"},"toolOutput":"","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
18
+ ## 2026-03-03T14:04:29.483Z | bff186d2-1eba-4fb3-bbb4-98a5dccbc508
19
+ - type: tool_observation
20
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
21
+ {"toolName":"Bash","toolInput":{"command":"git commit -m \"$(cat <<'EOF'\nfeat(cli): expand hook coverage to all lifecycle events\n\n- Add SessionStart, Stop, SessionEnd hooks alongside existing UserPromptSubmit and PostToolUse\n- Refactor getHooksConfig with makeHook helper to reduce repetition\n- Add REQUIRED_HOOK_FILES constant and hasHook helper for validation\n- Update install/uninstall/status commands to handle all 5 hooks\n- Add scripts/delete-unknown-projects.js for cleaning orphaned project dirs\n- Add ops:projects:clean-unknown npm script\n- Bump version to 1.0.18\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit all staged changes"},"toolOutput":"[main b37aa13] feat(cli): expand hook coverage to all lifecycle events\n 4 files changed, 214 insertions(+), 37 deletions(-)\n create mode 100755 scripts/delete-unknown-projects.js","durationMs":0,"success":true,"metadata":{"command":"git commit","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
@@ -14,3 +14,12 @@ api/stats/retrieval-traces?limit=20
14
14
  suggested_actions 를 생성할때
15
15
  "각 색상·사이즈별 재고 가능 여부 확인해줘"
16
16
  같이 없는 기능은 생성하지 않도록 프롬프트 개선해줘
17
+
18
+ ## 2026-02-26T13:53:54.228Z | efefccef-1a9a-4e89-b1fa-b855fd8fdc85
19
+ - type: user_prompt
20
+ - session: bench-session-2
21
+ 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
22
+ ## 2026-02-26T13:54:45.879Z | f7fca9e1-5169-4732-8845-34fdb5e699c5
23
+ - type: user_prompt
24
+ - session: bench-session-3
25
+ 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-layer",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Claude Code plugin that learns from conversations to provide personalized assistance",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "ops:sync-gap:heal": "bash scripts/sync-gap-auto-heal.sh",
20
20
  "ops:review:resolve": "node scripts/review-queue-auto-resolve.js",
21
21
  "ops:heartbeat": "bash scripts/heartbeat-memory-orchestrator.sh",
22
- "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve"
22
+ "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve",
23
+ "ops:projects:clean-unknown": "node scripts/delete-unknown-projects.js"
23
24
  },
24
25
  "keywords": [
25
26
  "claude-code",
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ function parseArgs(argv) {
7
+ const result = {
8
+ apply: false,
9
+ hashes: []
10
+ };
11
+
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = argv[i];
14
+ if (arg === '--apply') {
15
+ result.apply = true;
16
+ continue;
17
+ }
18
+ if (arg === '--hash' && i + 1 < argv.length) {
19
+ result.hashes.push(String(argv[i + 1]).trim().toLowerCase());
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (arg.startsWith('--hash=')) {
24
+ result.hashes.push(arg.slice('--hash='.length).trim().toLowerCase());
25
+ }
26
+ }
27
+
28
+ return result;
29
+ }
30
+
31
+ function loadRegistryHashes(registryPath) {
32
+ if (!fs.existsSync(registryPath)) return new Set();
33
+ try {
34
+ const raw = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
35
+ const sessions = raw && typeof raw === 'object' ? raw.sessions : null;
36
+ if (!sessions || typeof sessions !== 'object') return new Set();
37
+
38
+ const hashes = new Set();
39
+ for (const entry of Object.values(sessions)) {
40
+ if (!entry || typeof entry !== 'object') continue;
41
+ const hash = entry.projectHash;
42
+ if (typeof hash === 'string' && /^[a-f0-9]{8}$/.test(hash)) {
43
+ hashes.add(hash);
44
+ }
45
+ }
46
+ return hashes;
47
+ } catch {
48
+ return new Set();
49
+ }
50
+ }
51
+
52
+ function getDirSizeBytes(targetPath) {
53
+ if (!fs.existsSync(targetPath)) return 0;
54
+ const stat = fs.statSync(targetPath);
55
+ if (stat.isFile()) return stat.size;
56
+ if (!stat.isDirectory()) return 0;
57
+
58
+ let total = 0;
59
+ const entries = fs.readdirSync(targetPath, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const child = path.join(targetPath, entry.name);
62
+ if (entry.isDirectory()) {
63
+ total += getDirSizeBytes(child);
64
+ } else if (entry.isFile()) {
65
+ total += fs.statSync(child).size;
66
+ }
67
+ }
68
+ return total;
69
+ }
70
+
71
+ function formatBytes(bytes) {
72
+ if (bytes <= 0) return '0 B';
73
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
74
+ let value = bytes;
75
+ let unitIdx = 0;
76
+ while (value >= 1024 && unitIdx < units.length - 1) {
77
+ value /= 1024;
78
+ unitIdx += 1;
79
+ }
80
+ return `${value.toFixed(1)} ${units[unitIdx]}`;
81
+ }
82
+
83
+ function main() {
84
+ const args = parseArgs(process.argv.slice(2));
85
+ const memoryRoot = process.env.CML_MEMORY_ROOT || path.join(os.homedir(), '.claude-code', 'memory');
86
+ const projectsDir = path.join(memoryRoot, 'projects');
87
+ const registryPath = path.join(memoryRoot, 'session-registry.json');
88
+
89
+ if (!fs.existsSync(projectsDir)) {
90
+ console.log(JSON.stringify({
91
+ status: 'skip',
92
+ reason: 'projects_dir_not_found',
93
+ projectsDir
94
+ }, null, 2));
95
+ process.exit(0);
96
+ }
97
+
98
+ const knownHashes = loadRegistryHashes(registryPath);
99
+ const filterHashes = new Set(
100
+ args.hashes.filter((h) => /^[a-f0-9]{8}$/.test(h))
101
+ );
102
+
103
+ const candidates = fs.readdirSync(projectsDir, { withFileTypes: true })
104
+ .filter((entry) => entry.isDirectory())
105
+ .map((entry) => entry.name)
106
+ .filter((name) => /^[a-f0-9]{8}$/.test(name))
107
+ .filter((hash) => !knownHashes.has(hash))
108
+ .filter((hash) => filterHashes.size === 0 || filterHashes.has(hash));
109
+
110
+ const projectInfo = candidates.map((hash) => {
111
+ const fullPath = path.join(projectsDir, hash);
112
+ const sizeBytes = getDirSizeBytes(fullPath);
113
+ return {
114
+ hash,
115
+ path: fullPath,
116
+ sizeBytes,
117
+ sizeHuman: formatBytes(sizeBytes)
118
+ };
119
+ });
120
+
121
+ const totalBytes = projectInfo.reduce((sum, item) => sum + item.sizeBytes, 0);
122
+
123
+ if (!args.apply) {
124
+ console.log(JSON.stringify({
125
+ status: 'dry-run',
126
+ message: 'Run with --apply to delete the directories below.',
127
+ memoryRoot,
128
+ projectsDir,
129
+ registryPath,
130
+ unknownCount: projectInfo.length,
131
+ totalSizeBytes: totalBytes,
132
+ totalSizeHuman: formatBytes(totalBytes),
133
+ projects: projectInfo
134
+ }, null, 2));
135
+ process.exit(0);
136
+ }
137
+
138
+ let deleted = 0;
139
+ for (const item of projectInfo) {
140
+ fs.rmSync(item.path, { recursive: true, force: true });
141
+ deleted += 1;
142
+ }
143
+
144
+ console.log(JSON.stringify({
145
+ status: 'ok',
146
+ mode: 'apply',
147
+ deletedCount: deleted,
148
+ deletedSizeBytes: totalBytes,
149
+ deletedSizeHuman: formatBytes(totalBytes),
150
+ deletedHashes: projectInfo.map((item) => item.hash)
151
+ }, null, 2));
152
+ }
153
+
154
+ main();
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import {
8
- getMemoryServiceForProject,
8
+ getLightweightMemoryService,
9
9
  registerSession
10
10
  } from '../services/memory-service.js';
11
11
  import type { SessionStartInput, SessionStartOutput } from '../core/types.js';
@@ -18,8 +18,8 @@ async function main(): Promise<void> {
18
18
  // Register session with project path for other hooks to find
19
19
  registerSession(input.session_id, input.cwd);
20
20
 
21
- // Get project-specific memory service
22
- const memoryService = getMemoryServiceForProject(input.cwd);
21
+ // Use lightweight service to avoid starting background workers in hook process
22
+ const memoryService = getLightweightMemoryService(input.session_id);
23
23
 
24
24
  try {
25
25
  // Start session in memory service
@@ -42,6 +42,12 @@ async function main(): Promise<void> {
42
42
  } catch (error) {
43
43
  console.error('Memory hook error:', error);
44
44
  console.log(JSON.stringify({ context: '' }));
45
+ } finally {
46
+ try {
47
+ await memoryService.close();
48
+ } catch {
49
+ // Best-effort cleanup
50
+ }
45
51
  }
46
52
  }
47
53
 
@@ -11,7 +11,10 @@
11
11
  */
12
12
 
13
13
  import { randomUUID } from 'crypto';
14
- import { getLightweightMemoryService } from '../services/memory-service.js';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ import { getLightweightMemoryService, getMemoryServiceForSession } from '../services/memory-service.js';
15
18
  import { writeTurnState } from '../core/turn-state.js';
16
19
  import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
17
20
 
@@ -21,6 +24,20 @@ const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
21
24
  const BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.4');
22
25
  const FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || '0.3');
23
26
  const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
27
+ const RETRIEVAL_MODE = (process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || 'hybrid') as 'keyword' | 'semantic' | 'hybrid';
28
+ const SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || '1200');
29
+ const ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || '3');
30
+
31
+ const ADHERENCE_STATE_DIR = path.join(os.homedir(), '.claude-code', 'memory');
32
+
33
+ interface AdherenceState {
34
+ sessionId: string;
35
+ turnCount: number;
36
+ lastCheckedTurn: number;
37
+ lastPrompt: string;
38
+ lastReason?: string;
39
+ updatedAt: string;
40
+ }
24
41
 
25
42
  /**
26
43
  * Determine if a prompt is worth storing as a memory.
@@ -42,6 +59,120 @@ function getDynamicMinScore(prompt: string): number {
42
59
  return BASE_MIN_SCORE;
43
60
  }
44
61
 
62
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
63
+ return new Promise((resolve, reject) => {
64
+ const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
65
+ promise
66
+ .then((result) => {
67
+ clearTimeout(timer);
68
+ resolve(result);
69
+ })
70
+ .catch((error) => {
71
+ clearTimeout(timer);
72
+ reject(error);
73
+ });
74
+ });
75
+ }
76
+
77
+ function formatMemoryContext(items: Array<{ type: string; content: string }>): string {
78
+ if (items.length === 0) return '';
79
+ const lines = items.map((m) => {
80
+ const preview = m.content.length > 300 ? m.content.substring(0, 300) + '...' : m.content;
81
+ return `- [${m.type}] ${preview}`;
82
+ });
83
+ return `💡 **Related memories found:**\n\n${lines.join('\n\n')}`;
84
+ }
85
+
86
+ function getAdherenceStatePath(sessionId: string): string {
87
+ return path.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
88
+ }
89
+
90
+ function readAdherenceState(sessionId: string): AdherenceState {
91
+ try {
92
+ const filePath = getAdherenceStatePath(sessionId);
93
+ if (!fs.existsSync(filePath)) {
94
+ return {
95
+ sessionId,
96
+ turnCount: 0,
97
+ lastCheckedTurn: 0,
98
+ lastPrompt: '',
99
+ lastReason: 'init',
100
+ updatedAt: new Date().toISOString()
101
+ };
102
+ }
103
+
104
+ const data = fs.readFileSync(filePath, 'utf8');
105
+ const parsed = JSON.parse(data) as AdherenceState;
106
+ if (parsed.sessionId !== sessionId) throw new Error('session mismatch');
107
+ return parsed;
108
+ } catch {
109
+ return {
110
+ sessionId,
111
+ turnCount: 0,
112
+ lastCheckedTurn: 0,
113
+ lastPrompt: '',
114
+ lastReason: 'init',
115
+ updatedAt: new Date().toISOString()
116
+ };
117
+ }
118
+ }
119
+
120
+ function writeAdherenceState(state: AdherenceState): void {
121
+ try {
122
+ if (!fs.existsSync(ADHERENCE_STATE_DIR)) {
123
+ fs.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
124
+ }
125
+ const filePath = getAdherenceStatePath(state.sessionId);
126
+ const tempPath = filePath + '.tmp';
127
+ fs.writeFileSync(tempPath, JSON.stringify(state));
128
+ fs.renameSync(tempPath, filePath);
129
+ } catch {
130
+ // non-critical
131
+ }
132
+ }
133
+
134
+ function hasWriteIntent(prompt: string): boolean {
135
+ return /(fix|refactor|implement|change|modify|edit|update|rewrite|patch|create|add|remove|delete|버그|수정|리팩터|구현|추가|삭제|개선)/i.test(prompt);
136
+ }
137
+
138
+ function tokenize(text: string): string[] {
139
+ const stopwords = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'have', 'what', 'when', 'where', 'how', 'why', '그리고', '그리고요', '이거', '그거', '해주세요', '해줘', '좀', '에서', '으로', '하는', '해']);
140
+ return text
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9가-힣\s]/g, ' ')
143
+ .split(/\s+/)
144
+ .filter((w) => w.length >= 2 && !stopwords.has(w));
145
+ }
146
+
147
+ function isTopicShift(currentPrompt: string, lastPrompt: string): boolean {
148
+ if (!lastPrompt || lastPrompt.length < 10) return false;
149
+ const a = new Set(tokenize(currentPrompt));
150
+ const b = new Set(tokenize(lastPrompt));
151
+ if (a.size === 0 || b.size === 0) return false;
152
+
153
+ let intersection = 0;
154
+ for (const token of a) {
155
+ if (b.has(token)) intersection++;
156
+ }
157
+ const union = a.size + b.size - intersection;
158
+ const similarity = union > 0 ? intersection / union : 0;
159
+ return similarity < 0.2;
160
+ }
161
+
162
+ function shouldRunAdherenceCheck(turnCount: number, prompt: string, state: AdherenceState): { run: boolean; reason: string } {
163
+ if (turnCount === 1) return { run: true, reason: 'first-turn' };
164
+ if (hasWriteIntent(prompt)) return { run: true, reason: 'write-intent' };
165
+ if (isTopicShift(prompt, state.lastPrompt)) return { run: true, reason: 'topic-shift' };
166
+ if (turnCount - state.lastCheckedTurn >= ADHERENCE_INTERVAL_TURNS) return { run: true, reason: 'interval' };
167
+ return { run: false, reason: 'skip' };
168
+ }
169
+
170
+ function logAdherenceDecision(sessionId: string, turn: number, run: boolean, reason: string): void {
171
+ if (!process.env.CLAUDE_MEMORY_DEBUG) return;
172
+ const mode = run ? 'enforced' : 'skipped';
173
+ console.error(`[adherence] session=${sessionId} turn=${turn} mode=${mode} reason=${reason}`);
174
+ }
175
+
45
176
  async function main(): Promise<void> {
46
177
  // Read input from stdin
47
178
  const inputData = await readStdin();
@@ -58,62 +189,127 @@ async function main(): Promise<void> {
58
189
  const memoryService = getLightweightMemoryService(input.session_id);
59
190
 
60
191
  try {
192
+ let context = '';
193
+
194
+ const adherenceState = readAdherenceState(input.session_id);
195
+ const currentTurn = adherenceState.turnCount + 1;
196
+ const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
197
+ logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
198
+
61
199
  // Store only non-trivial prompts (skip /commands, short inputs)
62
200
  if (shouldStorePrompt(input.prompt)) {
63
201
  await memoryService.storeUserPrompt(
64
202
  input.session_id,
65
203
  input.prompt,
66
- { turnId }
204
+ {
205
+ turnId,
206
+ adherence: {
207
+ checked: adherenceDecision.run,
208
+ reason: adherenceDecision.reason,
209
+ turn: currentTurn
210
+ }
211
+ }
67
212
  );
68
213
  }
69
214
 
70
- let context = '';
71
-
72
- // Fast keyword search if enabled
73
- if (ENABLE_SEARCH && input.prompt.length > 10) {
215
+ // Search strategy: turn-1 always enforce adherence check,
216
+ // then adaptively enforce on write-intent/topic-shift/interval
217
+ if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
74
218
  const minScore = getDynamicMinScore(input.prompt);
75
- let results = await memoryService.keywordSearch(input.prompt, {
76
- topK: MAX_MEMORIES,
77
- minScore
78
- });
219
+ let mergedMemories: Array<{ type: string; content: string; id?: string; score?: number }> = [];
220
+
221
+ const canUseSemantic = RETRIEVAL_MODE === 'semantic' || RETRIEVAL_MODE === 'hybrid';
222
+ if (canUseSemantic) {
223
+ try {
224
+ const semanticService = getMemoryServiceForSession(input.session_id);
225
+ const semantic = await withTimeout(
226
+ semanticService.retrieveMemories(input.prompt, {
227
+ topK: MAX_MEMORIES,
228
+ minScore,
229
+ sessionId: input.session_id,
230
+ intentRewrite: true,
231
+ adaptiveRerank: true,
232
+ projectScopeMode: 'strict'
233
+ }),
234
+ SEMANTIC_TIMEOUT_MS
235
+ );
79
236
 
80
- // recall rescue: if nothing found at tuned threshold, retry with fallback floor
81
- if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
82
- results = await memoryService.keywordSearch(input.prompt, {
237
+ mergedMemories = semantic.memories.map((m) => ({
238
+ type: m.event.eventType,
239
+ content: m.event.content,
240
+ id: m.event.id,
241
+ score: m.score
242
+ }));
243
+ } catch {
244
+ // Semantic retrieval is best-effort; fallback below handles the rest
245
+ }
246
+ }
247
+
248
+ const shouldUseKeywordFallback =
249
+ RETRIEVAL_MODE === 'keyword' ||
250
+ RETRIEVAL_MODE === 'hybrid' ||
251
+ mergedMemories.length === 0;
252
+
253
+ if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
254
+ let results = await memoryService.keywordSearch(input.prompt, {
83
255
  topK: MAX_MEMORIES,
84
- minScore: FALLBACK_MIN_SCORE
256
+ minScore
85
257
  });
258
+
259
+ // recall rescue: if nothing found at tuned threshold, retry with fallback floor
260
+ if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
261
+ results = await memoryService.keywordSearch(input.prompt, {
262
+ topK: MAX_MEMORIES,
263
+ minScore: FALLBACK_MIN_SCORE
264
+ });
265
+ }
266
+
267
+ const existingIds = new Set(mergedMemories.map((m) => m.id).filter(Boolean));
268
+ for (const r of results) {
269
+ if (existingIds.has(r.event.id)) continue;
270
+ mergedMemories.push({
271
+ type: r.event.eventType,
272
+ content: r.event.content,
273
+ id: r.event.id,
274
+ score: r.score
275
+ });
276
+ if (mergedMemories.length >= MAX_MEMORIES) break;
277
+ }
86
278
  }
87
279
 
88
- if (results.length > 0) {
280
+ if (mergedMemories.length > 0) {
89
281
  // Increment access count for found memories
90
- const eventIds = results.map(r => r.event.id);
91
- await memoryService.incrementMemoryAccess(eventIds);
282
+ const eventIds = mergedMemories.map((m) => m.id).filter((v): v is string => Boolean(v));
283
+ if (eventIds.length > 0) {
284
+ await memoryService.incrementMemoryAccess(eventIds);
285
+ }
92
286
 
93
287
  // Record each retrieval for helpfulness tracking
94
- for (const r of results) {
288
+ for (const m of mergedMemories) {
289
+ if (!m.id) continue;
95
290
  try {
96
291
  await memoryService.recordRetrieval(
97
- r.event.id,
292
+ m.id,
98
293
  input.session_id,
99
- r.score,
294
+ m.score ?? minScore,
100
295
  input.prompt
101
296
  );
102
297
  } catch { /* non-critical */ }
103
298
  }
104
299
 
105
- // Format context
106
- const memories = results.map(r => {
107
- const preview = r.event.content.length > 300
108
- ? r.event.content.substring(0, 300) + '...'
109
- : r.event.content;
110
- return `- [${r.event.eventType}] ${preview}`;
111
- });
112
-
113
- context = `💡 **Related memories found:**\n\n${memories.join('\n\n')}`;
300
+ context = formatMemoryContext(mergedMemories);
114
301
  }
115
302
  }
116
303
 
304
+ writeAdherenceState({
305
+ sessionId: input.session_id,
306
+ turnCount: currentTurn,
307
+ lastCheckedTurn: adherenceDecision.run ? currentTurn : adherenceState.lastCheckedTurn,
308
+ lastPrompt: input.prompt,
309
+ lastReason: adherenceDecision.reason,
310
+ updatedAt: new Date().toISOString()
311
+ });
312
+
117
313
  const output: UserPromptSubmitOutput = { context };
118
314
  console.log(JSON.stringify(output));
119
315
  } catch (error) {
@@ -66,6 +66,7 @@ eventsRouter.get('/', async (c) => {
66
66
  sessionId: e.sessionId,
67
67
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : ''),
68
68
  contentLength: e.content.length,
69
+ metadata: e.metadata,
69
70
  accessCount: (e as any).access_count || 0,
70
71
  lastAccessedAt: (e as any).last_accessed_at || null
71
72
  })),