claude-mem-lite 2.3.3 → 2.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.3.3",
3
+ "version": "2.5.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,7 +24,7 @@ ${content}
24
24
  </content>
25
25
 
26
26
  JSON format:
27
- {"intent_tags":"comma-separated intent keywords (e.g. test,debug,deploy,review)","domain_tags":"comma-separated tech/language tags (e.g. javascript,react,python)","action_type":"analyze|generate|transform|validate|deploy|configure|review","trigger_patterns":"natural language describing when to use this (e.g. when user needs to write tests; when debugging errors)","capability_summary":"50-100 char description of what it does","input_type":"code|file|directory|url|text","output_type":"report|file|diff|terminal_output|analysis"}`;
27
+ {"intent_tags":"comma-separated intent keywords (e.g. test,debug,deploy,review)","domain_tags":"comma-separated tech/language tags (e.g. javascript,react,python)","action_type":"analyze|generate|transform|validate|deploy|configure|review","trigger_patterns":"natural language describing when to use this (e.g. when user needs to write tests; when debugging errors)","capability_summary":"50-100 char description of what it does","keywords":"specific technical terms, framework names, method names not duplicating intent_tags (e.g. jest,vitest,red-green-refactor,pytest)","tech_stack":"specific frameworks and tools this works with (e.g. jest,vitest,mocha,pytest)","use_cases":"3-5 specific usage scenarios separated by semicolons","input_type":"code|file|directory|url|text","output_type":"report|file|diff|terminal_output|analysis"}`;
28
28
  }
29
29
 
30
30
  // ─── Fallback Extraction ─────────────────────────────────────────────────────
@@ -127,6 +127,9 @@ export async function indexResource(db, resource) {
127
127
  capability_summary: metadata.capability_summary || '',
128
128
  input_type: metadata.input_type || '',
129
129
  output_type: metadata.output_type || '',
130
+ keywords: metadata.keywords || '',
131
+ tech_stack: metadata.tech_stack || '',
132
+ use_cases: metadata.use_cases || '',
130
133
  prerequisites: typeof metadata.prerequisites === 'object'
131
134
  ? JSON.stringify(metadata.prerequisites) : '{}',
132
135
  indexed_at: now,
package/registry.mjs CHANGED
@@ -106,6 +106,7 @@ const INVOCATIONS_SCHEMA = `
106
106
  adopted INTEGER DEFAULT 0,
107
107
  outcome TEXT CHECK(outcome IN ('success','partial','failure','skipped','ignored') OR outcome IS NULL),
108
108
  score REAL,
109
+ rejection_reason TEXT CHECK(rejection_reason IN ('alternative','manual','context_switch','session_end','unknown') OR rejection_reason IS NULL),
109
110
  created_at TEXT DEFAULT (datetime('now'))
110
111
  );
111
112
 
@@ -210,6 +211,14 @@ export function ensureRegistryDb(dbPath) {
210
211
  }
211
212
  } catch (e) { debugCatch(e, 'ensureRegistryDb-ignored-migration'); }
212
213
 
214
+ // Migrate: add rejection_reason column if missing
215
+ try {
216
+ const cols = db.prepare("PRAGMA table_info(invocations)").all();
217
+ if (!cols.some(c => c.name === 'rejection_reason')) {
218
+ db.exec("ALTER TABLE invocations ADD COLUMN rejection_reason TEXT");
219
+ }
220
+ } catch (e) { debugCatch(e, 'rejection_reason-migration'); }
221
+
213
222
  db.exec(PREINSTALLED_SCHEMA);
214
223
 
215
224
  return db;
@@ -229,7 +238,8 @@ const UPSERT_SQL = `
229
238
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
230
239
  ON CONFLICT(type, name) DO UPDATE SET
231
240
  status=excluded.status, source=excluded.source, repo_url=excluded.repo_url,
232
- repo_stars=excluded.repo_stars, local_path=excluded.local_path, file_hash=excluded.file_hash,
241
+ repo_stars=CASE WHEN excluded.repo_stars > 0 THEN excluded.repo_stars ELSE repo_stars END,
242
+ local_path=excluded.local_path, file_hash=excluded.file_hash,
233
243
  invocation_name=CASE WHEN excluded.invocation_name != '' THEN excluded.invocation_name ELSE invocation_name END,
234
244
  intent_tags=excluded.intent_tags, domain_tags=excluded.domain_tags,
235
245
  action_type=excluded.action_type, trigger_patterns=excluded.trigger_patterns,
@@ -369,7 +379,7 @@ export function getSessionInvocations(db, sessionId) {
369
379
  * @param {object} update Fields to update
370
380
  */
371
381
  export function updateInvocation(db, id, update) {
372
- const allowed = new Set(['adopted', 'outcome', 'score']);
382
+ const allowed = new Set(['adopted', 'outcome', 'score', 'rejection_reason']);
373
383
  const sets = [];
374
384
  const vals = [];
375
385
  for (const [key, val] of Object.entries(update)) {
package/schema.mjs CHANGED
@@ -99,6 +99,10 @@ const MIGRATIONS = [
99
99
  'ALTER TABLE observations ADD COLUMN access_count INTEGER DEFAULT 0',
100
100
  'ALTER TABLE observations ADD COLUMN compressed_into INTEGER DEFAULT NULL',
101
101
  'ALTER TABLE session_summaries ADD COLUMN remaining_items TEXT',
102
+ 'ALTER TABLE observations ADD COLUMN lesson_learned TEXT DEFAULT NULL',
103
+ 'ALTER TABLE observations ADD COLUMN search_aliases TEXT DEFAULT NULL',
104
+ 'ALTER TABLE session_summaries ADD COLUMN lessons TEXT DEFAULT NULL',
105
+ 'ALTER TABLE session_summaries ADD COLUMN key_decisions TEXT DEFAULT NULL',
102
106
  ];
103
107
 
104
108
  /**
@@ -1,7 +1,7 @@
1
1
  // claude-mem-lite server internal functions
2
2
  // Extracted from server.mjs for testability (server.mjs has top-level side effects)
3
3
 
4
- import { debugCatch } from './utils.mjs';
4
+ import { debugCatch, COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE } from './utils.mjs';
5
5
 
6
6
  // ─── Search Re-ranking Helpers ────────────────────────────────────────────
7
7
 
@@ -193,3 +193,70 @@ export function expandQueryByConcepts(db, ftsQuery, project) {
193
193
  .slice(0, 3)
194
194
  .map(([concept]) => concept);
195
195
  }
196
+
197
+ // ─── Auto-boost ─────────────────────────────────────────────────────────────
198
+
199
+ /**
200
+ * Boost importance to 2 for observations that have been accessed multiple times
201
+ * (access_count >= 2) but still have default importance (1).
202
+ * Called after incrementing access_count in mem_get.
203
+ * @param {object} db better-sqlite3 database handle
204
+ * @param {number[]} ids Array of observation IDs to check
205
+ */
206
+ export function autoBoostIfNeeded(db, ids) {
207
+ if (!ids || ids.length === 0) return;
208
+ const placeholders = ids.map(() => '?').join(',');
209
+ db.prepare(`
210
+ UPDATE observations SET importance = 2
211
+ WHERE id IN (${placeholders})
212
+ AND COALESCE(importance, 1) = 1
213
+ AND COALESCE(access_count, 0) >= 2
214
+ `).run(...ids);
215
+ }
216
+
217
+ // ─── Idle Cleanup ────────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Run type-differentiated idle cleanup on stale observations.
221
+ * Higher-value types (decision, discovery) survive longer than ephemeral types (change).
222
+ * - Marks low-quality (importance<=1, never accessed) as pending-purge (COMPRESSED_PENDING_PURGE).
223
+ * - Marks importance=1 accessed observations as auto-compressed (COMPRESSED_AUTO).
224
+ * @param {object} db better-sqlite3 database handle
225
+ * @returns {{ marked: number, compressed: number }}
226
+ */
227
+ export function runIdleCleanup(db) {
228
+ // SAFETY: type values are hardcoded constants, not user input
229
+ const staleThresholds = [
230
+ { types: "'decision','discovery'", days: 90 },
231
+ { types: "'feature'", days: 60 },
232
+ { types: "'bugfix','refactor'", days: 30 },
233
+ { types: "'change'", days: 14 },
234
+ ];
235
+
236
+ let totalMarked = 0;
237
+ let totalCompressed = 0;
238
+
239
+ db.transaction(() => {
240
+ for (const { types, days } of staleThresholds) {
241
+ const cutoff = Date.now() - days * 86400000;
242
+
243
+ const marked = db.prepare(`
244
+ UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
245
+ WHERE importance <= 1 AND COALESCE(access_count, 0) = 0
246
+ AND type IN (${types})
247
+ AND created_at_epoch < ? AND COALESCE(compressed_into, 0) = 0
248
+ `).run(cutoff);
249
+ totalMarked += marked.changes;
250
+
251
+ const compressed = db.prepare(`
252
+ UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
253
+ WHERE COALESCE(compressed_into, 0) = 0 AND importance = 1
254
+ AND type IN (${types})
255
+ AND created_at_epoch < ?
256
+ `).run(cutoff);
257
+ totalCompressed += compressed.changes;
258
+ }
259
+ })();
260
+
261
+ return { marked: totalMarked, compressed: totalCompressed };
262
+ }
package/server.mjs CHANGED
@@ -6,7 +6,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, fmtDate, isoWeekKey, debugLog, debugCatch, COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE } from './utils.mjs';
8
8
  import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
9
- import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts } from './server-internals.mjs';
9
+ import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup } from './server-internals.mjs';
10
10
  import { memSearchSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memRegistrySchema } from './tool-schemas.mjs';
11
11
  import { ensureRegistryDb, upsertResource } from './registry.mjs';
12
12
  import { createRequire } from 'module';
@@ -121,15 +121,28 @@ function safeHandler(fn) {
121
121
 
122
122
  // ─── Tool: mem_search — helper functions ────────────────────────────────────
123
123
 
124
+ // Type-differentiated recency decay: decisions persist longer, routine changes fade fast
125
+ const TYPE_DECAY_CASE = `(
126
+ CASE o.type
127
+ WHEN 'decision' THEN 7776000000.0
128
+ WHEN 'discovery' THEN 5184000000.0
129
+ WHEN 'feature' THEN 2592000000.0
130
+ WHEN 'bugfix' THEN 1209600000.0
131
+ WHEN 'refactor' THEN 1209600000.0
132
+ WHEN 'change' THEN 604800000.0
133
+ ELSE 1209600000.0
134
+ END
135
+ )`;
136
+
124
137
  // Score expression variants for FTS5 queries (see Scoring Model Constants above)
125
138
  const FULL_SCORE = `${OBS_BM25}
126
- * (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${RECENCY_HALF_LIFE_MS}.0))
139
+ * (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${TYPE_DECAY_CASE}))
127
140
  * (CASE WHEN ? IS NOT NULL AND o.project = ? THEN 2.0 ELSE 1.0 END)
128
141
  * (0.5 + 0.5 * COALESCE(o.importance, 1))
129
142
  * (1.0 + 0.1 * LN(1 + COALESCE(o.access_count, 0)))`;
130
143
 
131
144
  const SIMPLE_SCORE = `${OBS_BM25}
132
- * (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${RECENCY_HALF_LIFE_MS}.0))
145
+ * (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${TYPE_DECAY_CASE}))
133
146
  * (0.5 + 0.5 * COALESCE(o.importance, 1))`;
134
147
 
135
148
  /**
@@ -607,6 +620,8 @@ server.registerTool(
607
620
  db.prepare(
608
621
  `UPDATE observations SET access_count = COALESCE(access_count, 0) + 1 WHERE id IN (${placeholders})`
609
622
  ).run(...args.ids);
623
+ // Auto-boost importance for frequently accessed observations
624
+ autoBoostIfNeeded(db, args.ids);
610
625
  rows = db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...args.ids);
611
626
  allFields = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count'];
612
627
  prefix = '#';
@@ -1249,33 +1264,10 @@ const idleTimer = setInterval(() => {
1249
1264
  idleCleanupRan = true;
1250
1265
 
1251
1266
  try {
1252
- const thirtyDaysAgo = Date.now() - 30 * 86400000;
1253
-
1254
- db.transaction(() => {
1255
- // Mark old low-quality observations as pending-purge (importance<=1, never accessed, 30+ days).
1256
- // Actual deletion only happens when user confirms via mem_maintain execute purge_stale.
1257
- // NOTE: no project filter — MCP server is global, operates across all projects.
1258
- const marked = db.prepare(`
1259
- UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
1260
- WHERE importance <= 1 AND COALESCE(access_count, 0) = 0
1261
- AND created_at_epoch < ? AND COALESCE(compressed_into, 0) = 0
1262
- `).run(thirtyDaysAgo);
1263
- if (marked.changes > 0) {
1264
- debugLog('INFO', 'idle-cleanup', `Marked ${marked.changes} stale observations as pending-purge`);
1265
- }
1266
-
1267
- // Mark old importance=1 with access_count>0 as compressed (30+ days).
1268
- // Note: importance=1, access_count=0 rows were already marked pending-purge above,
1269
- // so this only catches importance=1 rows that HAVE been accessed.
1270
- const compressed = db.prepare(`
1271
- UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
1272
- WHERE COALESCE(compressed_into, 0) = 0 AND importance = 1
1273
- AND created_at_epoch < ?
1274
- `).run(thirtyDaysAgo);
1275
- if (compressed.changes > 0) {
1276
- debugLog('INFO', 'idle-cleanup', `Compressed ${compressed.changes} old observations`);
1277
- }
1278
- })();
1267
+ // Type-differentiated cleanup: higher-value types survive longer
1268
+ const { marked, compressed } = runIdleCleanup(db);
1269
+ if (marked > 0) debugLog('INFO', 'idle-cleanup', `Marked ${marked} stale observations as pending-purge`);
1270
+ if (compressed > 0) debugLog('INFO', 'idle-cleanup', `Compressed ${compressed} old observations`);
1279
1271
 
1280
1272
  // FTS5 index optimization (outside transaction — WAL-friendly)
1281
1273
  db.exec("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
package/utils.mjs CHANGED
@@ -10,6 +10,19 @@ export const COMPRESSED_AUTO = -1;
10
10
  /** compressed_into sentinel: pending user-confirmed purge (marked by idle cleanup) */
11
11
  export const COMPRESSED_PENDING_PURGE = -2;
12
12
 
13
+ // ─── Type-Differentiated Recency Decay ──────────────────────────────────────
14
+
15
+ /** Recency half-life per observation type (in milliseconds) */
16
+ export const DECAY_HALF_LIFE_BY_TYPE = {
17
+ decision: 90 * 86400000, // 90 days — architectural decisions persist
18
+ discovery: 60 * 86400000, // 60 days — learned patterns last
19
+ feature: 30 * 86400000, // 30 days — feature work is mid-range
20
+ bugfix: 14 * 86400000, // 14 days — bugs are usually one-off
21
+ refactor: 14 * 86400000, // 14 days — code cleanup
22
+ change: 7 * 86400000, // 7 days — routine changes decay fast
23
+ };
24
+ export const DEFAULT_DECAY_HALF_LIFE_MS = 14 * 86400000;
25
+
13
26
  // ─── String Utilities ────────────────────────────────────────────────────────
14
27
 
15
28
  /**
@@ -255,6 +268,57 @@ const SYNONYM_PAIRS = [
255
268
  ['debug', 'troubleshoot'],
256
269
  ['error', 'failure'],
257
270
  ['migrate', 'migration'],
271
+ // ─── CJK ↔ EN cross-language synonyms ───
272
+ // Authentication & Authorization
273
+ ['认证', 'auth'], ['认证', 'authentication'], ['登录', 'login'], ['登录', 'auth'],
274
+ ['授权', 'authorization'], ['权限', 'permission'],
275
+ // Deployment & Operations
276
+ ['部署', 'deploy'], ['部署', 'deployment'], ['发布', 'release'], ['发布', 'publish'],
277
+ // Data & Storage
278
+ ['缓存', 'cache'], ['缓存', 'caching'],
279
+ ['数据库', 'database'], ['数据库', 'db'],
280
+ // Testing & Debugging
281
+ ['测试', 'test'], ['测试', 'testing'],
282
+ ['调试', 'debug'], ['调试', 'debugging'],
283
+ ['修复', 'fix'], ['修复', 'bugfix'],
284
+ // Code Quality
285
+ ['重构', 'refactor'], ['重构', 'refactoring'],
286
+ ['配置', 'config'], ['配置', 'configuration'],
287
+ // API & Networking
288
+ ['接口', 'api'], ['接口', 'endpoint'],
289
+ ['路由', 'route'], ['路由', 'routing'],
290
+ ['中间件', 'middleware'],
291
+ // UI & Components
292
+ ['组件', 'component'], ['模板', 'template'],
293
+ // Database Operations
294
+ ['迁移', 'migration'], ['迁移', 'migrate'],
295
+ ['索引', 'index'], ['查询', 'query'], ['查询', 'search'],
296
+ ['排序', 'sort'], ['分页', 'pagination'],
297
+ // Validation & Security
298
+ ['验证', 'validate'], ['验证', 'validation'],
299
+ ['加密', 'encrypt'], ['加密', 'encryption'],
300
+ ['会话', 'session'], ['令牌', 'token'],
301
+ // Patterns & Architecture
302
+ ['钩子', 'hook'], ['回调', 'callback'],
303
+ ['异步', 'async'], ['同步', 'sync'],
304
+ ['并发', 'concurrent'], ['线程', 'thread'],
305
+ // Performance
306
+ ['性能', 'performance'], ['性能', 'perf'],
307
+ ['内存', 'memory'], ['泄漏', 'leak'],
308
+ ['超时', 'timeout'], ['重试', 'retry'],
309
+ // Observability
310
+ ['日志', 'log'], ['日志', 'logging'],
311
+ ['监控', 'monitor'], ['告警', 'alert'],
312
+ // Build & Dependencies
313
+ ['依赖', 'dependency'], ['构建', 'build'], ['构建', 'compile'],
314
+ ['打包', 'bundle'], ['类型', 'type'], ['类型', 'typescript'],
315
+ // Errors
316
+ ['错误', 'error'], ['异常', 'exception'],
317
+ // Infrastructure
318
+ ['容器', 'container'], ['容器', 'docker'],
319
+ ['集群', 'cluster'], ['集群', 'kubernetes'],
320
+ ['网关', 'gateway'], ['负载', 'load balancing'],
321
+ ['队列', 'queue'], ['序列化', 'serialize'],
258
322
  ];
259
323
  // Build bidirectional lookup (case-insensitive)
260
324
  for (const [abbr, full] of SYNONYM_PAIRS) {