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
@@ -8,6 +8,9 @@ const __dirname = dirname(__filename);
8
8
 
9
9
  // src/hooks/user-prompt-submit.ts
10
10
  import { randomUUID as randomUUID9 } from "crypto";
11
+ import * as fs6 from "fs";
12
+ import * as path5 from "path";
13
+ import * as os3 from "os";
11
14
 
12
15
  // src/services/memory-service.ts
13
16
  import * as path3 from "path";
@@ -70,11 +73,11 @@ function toDate(value) {
70
73
  return new Date(value);
71
74
  return new Date(String(value));
72
75
  }
73
- function createDatabase(path5, options) {
76
+ function createDatabase(path6, options) {
74
77
  if (options?.readOnly) {
75
- return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
78
+ return new duckdb.Database(path6, { access_mode: "READ_ONLY" });
76
79
  }
77
- return new duckdb.Database(path5);
80
+ return new duckdb.Database(path6);
78
81
  }
79
82
  function dbRun(db, sql, params = []) {
80
83
  return new Promise((resolve2, reject) => {
@@ -758,12 +761,12 @@ import { randomUUID as randomUUID2 } from "crypto";
758
761
  import Database from "better-sqlite3";
759
762
  import * as fs from "fs";
760
763
  import * as nodePath from "path";
761
- function createSQLiteDatabase(path5, options) {
762
- const dir = nodePath.dirname(path5);
764
+ function createSQLiteDatabase(path6, options) {
765
+ const dir = nodePath.dirname(path6);
763
766
  if (!fs.existsSync(dir)) {
764
767
  fs.mkdirSync(dir, { recursive: true });
765
768
  }
766
- const db = new Database(path5, {
769
+ const db = new Database(path6, {
767
770
  readonly: options?.readonly ?? false
768
771
  });
769
772
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -3439,8 +3442,8 @@ _Context:_ ${sessionContext}`;
3439
3442
  matchesMetadataScope(metadata, expected) {
3440
3443
  if (!metadata)
3441
3444
  return false;
3442
- return Object.entries(expected).every(([path5, value]) => {
3443
- const actual = path5.split(".").reduce((acc, key) => {
3445
+ return Object.entries(expected).every(([path6, value]) => {
3446
+ const actual = path6.split(".").reduce((acc, key) => {
3444
3447
  if (typeof acc !== "object" || acc === null)
3445
3448
  return void 0;
3446
3449
  return acc[key];
@@ -6881,6 +6884,12 @@ var MemoryService = class {
6881
6884
  recordMemoryAccess(eventId, sessionId, confidence = 1) {
6882
6885
  this.graduation.recordAccess(eventId, sessionId, confidence);
6883
6886
  }
6887
+ /**
6888
+ * Backward-compatible alias used by some hooks
6889
+ */
6890
+ async close() {
6891
+ await this.shutdown();
6892
+ }
6884
6893
  /**
6885
6894
  * Shutdown service
6886
6895
  */
@@ -6916,6 +6925,42 @@ var MemoryService = class {
6916
6925
  }
6917
6926
  };
6918
6927
  var serviceCache = /* @__PURE__ */ new Map();
6928
+ var GLOBAL_KEY = "__global__";
6929
+ function getDefaultMemoryService() {
6930
+ if (!serviceCache.has(GLOBAL_KEY)) {
6931
+ serviceCache.set(GLOBAL_KEY, new MemoryService({
6932
+ storagePath: "~/.claude-code/memory",
6933
+ analyticsEnabled: false,
6934
+ // Hooks don't need DuckDB
6935
+ sharedStoreConfig: { enabled: false }
6936
+ // Shared store uses DuckDB too
6937
+ }));
6938
+ }
6939
+ return serviceCache.get(GLOBAL_KEY);
6940
+ }
6941
+ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
6942
+ const hash = hashProjectPath(projectPath);
6943
+ if (!serviceCache.has(hash)) {
6944
+ const storagePath = getProjectStoragePath(projectPath);
6945
+ serviceCache.set(hash, new MemoryService({
6946
+ storagePath,
6947
+ projectHash: hash,
6948
+ projectPath,
6949
+ // Override shared store config - hooks don't need DuckDB
6950
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
6951
+ analyticsEnabled: false
6952
+ // Hooks don't need DuckDB
6953
+ }));
6954
+ }
6955
+ return serviceCache.get(hash);
6956
+ }
6957
+ function getMemoryServiceForSession(sessionId) {
6958
+ const projectInfo = getSessionProject(sessionId);
6959
+ if (projectInfo) {
6960
+ return getMemoryServiceForProject(projectInfo.projectPath);
6961
+ }
6962
+ return getDefaultMemoryService();
6963
+ }
6919
6964
  function getLightweightMemoryService(sessionId) {
6920
6965
  const projectInfo = getSessionProject(sessionId);
6921
6966
  const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
@@ -6968,6 +7013,10 @@ var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
6968
7013
  var BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.4");
6969
7014
  var FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || "0.3");
6970
7015
  var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
7016
+ var RETRIEVAL_MODE = process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || "hybrid";
7017
+ var SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || "1200");
7018
+ var ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || "3");
7019
+ var ADHERENCE_STATE_DIR = path5.join(os3.homedir(), ".claude-code", "memory");
6971
7020
  function shouldStorePrompt(prompt) {
6972
7021
  const trimmed = prompt.trim();
6973
7022
  if (trimmed.startsWith("/"))
@@ -6986,6 +7035,113 @@ function getDynamicMinScore(prompt) {
6986
7035
  return Math.max(0.3, BASE_MIN_SCORE - 0.05);
6987
7036
  return BASE_MIN_SCORE;
6988
7037
  }
7038
+ function withTimeout(promise, timeoutMs) {
7039
+ return new Promise((resolve2, reject) => {
7040
+ const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
7041
+ promise.then((result) => {
7042
+ clearTimeout(timer);
7043
+ resolve2(result);
7044
+ }).catch((error) => {
7045
+ clearTimeout(timer);
7046
+ reject(error);
7047
+ });
7048
+ });
7049
+ }
7050
+ function formatMemoryContext(items) {
7051
+ if (items.length === 0)
7052
+ return "";
7053
+ const lines = items.map((m) => {
7054
+ const preview = m.content.length > 300 ? m.content.substring(0, 300) + "..." : m.content;
7055
+ return `- [${m.type}] ${preview}`;
7056
+ });
7057
+ return `\u{1F4A1} **Related memories found:**
7058
+
7059
+ ${lines.join("\n\n")}`;
7060
+ }
7061
+ function getAdherenceStatePath(sessionId) {
7062
+ return path5.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
7063
+ }
7064
+ function readAdherenceState(sessionId) {
7065
+ try {
7066
+ const filePath = getAdherenceStatePath(sessionId);
7067
+ if (!fs6.existsSync(filePath)) {
7068
+ return {
7069
+ sessionId,
7070
+ turnCount: 0,
7071
+ lastCheckedTurn: 0,
7072
+ lastPrompt: "",
7073
+ lastReason: "init",
7074
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7075
+ };
7076
+ }
7077
+ const data = fs6.readFileSync(filePath, "utf8");
7078
+ const parsed = JSON.parse(data);
7079
+ if (parsed.sessionId !== sessionId)
7080
+ throw new Error("session mismatch");
7081
+ return parsed;
7082
+ } catch {
7083
+ return {
7084
+ sessionId,
7085
+ turnCount: 0,
7086
+ lastCheckedTurn: 0,
7087
+ lastPrompt: "",
7088
+ lastReason: "init",
7089
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7090
+ };
7091
+ }
7092
+ }
7093
+ function writeAdherenceState(state) {
7094
+ try {
7095
+ if (!fs6.existsSync(ADHERENCE_STATE_DIR)) {
7096
+ fs6.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
7097
+ }
7098
+ const filePath = getAdherenceStatePath(state.sessionId);
7099
+ const tempPath = filePath + ".tmp";
7100
+ fs6.writeFileSync(tempPath, JSON.stringify(state));
7101
+ fs6.renameSync(tempPath, filePath);
7102
+ } catch {
7103
+ }
7104
+ }
7105
+ function hasWriteIntent(prompt) {
7106
+ return /(fix|refactor|implement|change|modify|edit|update|rewrite|patch|create|add|remove|delete|버그|수정|리팩터|구현|추가|삭제|개선)/i.test(prompt);
7107
+ }
7108
+ function tokenize(text) {
7109
+ const stopwords = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "have", "what", "when", "where", "how", "why", "\uADF8\uB9AC\uACE0", "\uADF8\uB9AC\uACE0\uC694", "\uC774\uAC70", "\uADF8\uAC70", "\uD574\uC8FC\uC138\uC694", "\uD574\uC918", "\uC880", "\uC5D0\uC11C", "\uC73C\uB85C", "\uD558\uB294", "\uD574"]);
7110
+ return text.toLowerCase().replace(/[^a-z0-9가-힣\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !stopwords.has(w));
7111
+ }
7112
+ function isTopicShift(currentPrompt, lastPrompt) {
7113
+ if (!lastPrompt || lastPrompt.length < 10)
7114
+ return false;
7115
+ const a = new Set(tokenize(currentPrompt));
7116
+ const b = new Set(tokenize(lastPrompt));
7117
+ if (a.size === 0 || b.size === 0)
7118
+ return false;
7119
+ let intersection = 0;
7120
+ for (const token of a) {
7121
+ if (b.has(token))
7122
+ intersection++;
7123
+ }
7124
+ const union = a.size + b.size - intersection;
7125
+ const similarity = union > 0 ? intersection / union : 0;
7126
+ return similarity < 0.2;
7127
+ }
7128
+ function shouldRunAdherenceCheck(turnCount, prompt, state) {
7129
+ if (turnCount === 1)
7130
+ return { run: true, reason: "first-turn" };
7131
+ if (hasWriteIntent(prompt))
7132
+ return { run: true, reason: "write-intent" };
7133
+ if (isTopicShift(prompt, state.lastPrompt))
7134
+ return { run: true, reason: "topic-shift" };
7135
+ if (turnCount - state.lastCheckedTurn >= ADHERENCE_INTERVAL_TURNS)
7136
+ return { run: true, reason: "interval" };
7137
+ return { run: false, reason: "skip" };
7138
+ }
7139
+ function logAdherenceDecision(sessionId, turn, run, reason) {
7140
+ if (!process.env.CLAUDE_MEMORY_DEBUG)
7141
+ return;
7142
+ const mode = run ? "enforced" : "skipped";
7143
+ console.error(`[adherence] session=${sessionId} turn=${turn} mode=${mode} reason=${reason}`);
7144
+ }
6989
7145
  async function main() {
6990
7146
  const inputData = await readStdin();
6991
7147
  const input = JSON.parse(inputData);
@@ -6993,49 +7149,107 @@ async function main() {
6993
7149
  writeTurnState(input.session_id, turnId);
6994
7150
  const memoryService = getLightweightMemoryService(input.session_id);
6995
7151
  try {
7152
+ let context = "";
7153
+ const adherenceState = readAdherenceState(input.session_id);
7154
+ const currentTurn = adherenceState.turnCount + 1;
7155
+ const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
7156
+ logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
6996
7157
  if (shouldStorePrompt(input.prompt)) {
6997
7158
  await memoryService.storeUserPrompt(
6998
7159
  input.session_id,
6999
7160
  input.prompt,
7000
- { turnId }
7161
+ {
7162
+ turnId,
7163
+ adherence: {
7164
+ checked: adherenceDecision.run,
7165
+ reason: adherenceDecision.reason,
7166
+ turn: currentTurn
7167
+ }
7168
+ }
7001
7169
  );
7002
7170
  }
7003
- let context = "";
7004
- if (ENABLE_SEARCH && input.prompt.length > 10) {
7171
+ if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
7005
7172
  const minScore = getDynamicMinScore(input.prompt);
7006
- let results = await memoryService.keywordSearch(input.prompt, {
7007
- topK: MAX_MEMORIES,
7008
- minScore
7009
- });
7010
- if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7011
- results = await memoryService.keywordSearch(input.prompt, {
7173
+ let mergedMemories = [];
7174
+ const canUseSemantic = RETRIEVAL_MODE === "semantic" || RETRIEVAL_MODE === "hybrid";
7175
+ if (canUseSemantic) {
7176
+ try {
7177
+ const semanticService = getMemoryServiceForSession(input.session_id);
7178
+ const semantic = await withTimeout(
7179
+ semanticService.retrieveMemories(input.prompt, {
7180
+ topK: MAX_MEMORIES,
7181
+ minScore,
7182
+ sessionId: input.session_id,
7183
+ intentRewrite: true,
7184
+ adaptiveRerank: true,
7185
+ projectScopeMode: "strict"
7186
+ }),
7187
+ SEMANTIC_TIMEOUT_MS
7188
+ );
7189
+ mergedMemories = semantic.memories.map((m) => ({
7190
+ type: m.event.eventType,
7191
+ content: m.event.content,
7192
+ id: m.event.id,
7193
+ score: m.score
7194
+ }));
7195
+ } catch {
7196
+ }
7197
+ }
7198
+ const shouldUseKeywordFallback = RETRIEVAL_MODE === "keyword" || RETRIEVAL_MODE === "hybrid" || mergedMemories.length === 0;
7199
+ if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
7200
+ let results = await memoryService.keywordSearch(input.prompt, {
7012
7201
  topK: MAX_MEMORIES,
7013
- minScore: FALLBACK_MIN_SCORE
7202
+ minScore
7014
7203
  });
7015
- }
7016
- if (results.length > 0) {
7017
- const eventIds = results.map((r) => r.event.id);
7018
- await memoryService.incrementMemoryAccess(eventIds);
7204
+ if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
7205
+ results = await memoryService.keywordSearch(input.prompt, {
7206
+ topK: MAX_MEMORIES,
7207
+ minScore: FALLBACK_MIN_SCORE
7208
+ });
7209
+ }
7210
+ const existingIds = new Set(mergedMemories.map((m) => m.id).filter(Boolean));
7019
7211
  for (const r of results) {
7212
+ if (existingIds.has(r.event.id))
7213
+ continue;
7214
+ mergedMemories.push({
7215
+ type: r.event.eventType,
7216
+ content: r.event.content,
7217
+ id: r.event.id,
7218
+ score: r.score
7219
+ });
7220
+ if (mergedMemories.length >= MAX_MEMORIES)
7221
+ break;
7222
+ }
7223
+ }
7224
+ if (mergedMemories.length > 0) {
7225
+ const eventIds = mergedMemories.map((m) => m.id).filter((v) => Boolean(v));
7226
+ if (eventIds.length > 0) {
7227
+ await memoryService.incrementMemoryAccess(eventIds);
7228
+ }
7229
+ for (const m of mergedMemories) {
7230
+ if (!m.id)
7231
+ continue;
7020
7232
  try {
7021
7233
  await memoryService.recordRetrieval(
7022
- r.event.id,
7234
+ m.id,
7023
7235
  input.session_id,
7024
- r.score,
7236
+ m.score ?? minScore,
7025
7237
  input.prompt
7026
7238
  );
7027
7239
  } catch {
7028
7240
  }
7029
7241
  }
7030
- const memories = results.map((r) => {
7031
- const preview = r.event.content.length > 300 ? r.event.content.substring(0, 300) + "..." : r.event.content;
7032
- return `- [${r.event.eventType}] ${preview}`;
7033
- });
7034
- context = `\u{1F4A1} **Related memories found:**
7035
-
7036
- ${memories.join("\n\n")}`;
7242
+ context = formatMemoryContext(mergedMemories);
7037
7243
  }
7038
7244
  }
7245
+ writeAdherenceState({
7246
+ sessionId: input.session_id,
7247
+ turnCount: currentTurn,
7248
+ lastCheckedTurn: adherenceDecision.run ? currentTurn : adherenceState.lastCheckedTurn,
7249
+ lastPrompt: input.prompt,
7250
+ lastReason: adherenceDecision.reason,
7251
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7252
+ });
7039
7253
  const output = { context };
7040
7254
  console.log(JSON.stringify(output));
7041
7255
  } catch (error) {