claude-mem-lite 3.39.0 → 3.39.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.39.0",
13
+ "version": "3.39.2",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.39.0",
3
+ "version": "3.39.2",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/commands/bug.md CHANGED
@@ -43,13 +43,16 @@ Map severity to importance:
43
43
  Build the body as `<description>\n\nRepro:\n<repro-steps>` (or just
44
44
  `<description>` if `--repro` is absent).
45
45
 
46
- Run via Bash — the body is the positional content; the description goes in
47
- `--lesson` so it lands in the high-weight `lesson_learned` field:
46
+ Run via Bash — the full body is the positional content; a distilled description
47
+ goes in `--lesson` so it lands in the high-weight `lesson_learned` field.
48
+ `--lesson` is capped at 500 chars (longer values are rejected), so keep the full
49
+ body in the positional content and trim `--lesson` to the core description if
50
+ longer:
48
51
 
49
52
  node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<body>" \
50
53
  --type bugfix \
51
54
  --title "<first 60 chars of description>" \
52
- --lesson "<description>" \
55
+ --lesson "<description, ≤500 chars>" \
53
56
  [--files f1,f2,...] \
54
57
  --importance <1|2|3>
55
58
 
@@ -36,13 +36,16 @@ Don't use for trivial fixes (typos, renames), for rules that belong in memdir
36
36
 
37
37
  ## Execution
38
38
 
39
- Run via Bash — pass the lesson text as the positional content and repeat it in
40
- `--lesson` so it lands in the high-weight `lesson_learned` field:
39
+ Run via Bash — pass the full lesson text as the positional content, and a
40
+ distilled version in `--lesson` so it lands in the high-weight `lesson_learned`
41
+ field. `--lesson` is capped at 500 chars (longer values are rejected), so for a
42
+ long lesson keep the full text in the positional content and trim `--lesson` to
43
+ the core insight:
41
44
 
42
45
  node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<full text>" \
43
46
  --type discovery \
44
47
  --title "<first 60 chars of text>" \
45
- --lesson "<full text>" \
48
+ --lesson "<core insight, ≤500 chars>" \
46
49
  [--files f1,f2,...] \
47
50
  --importance <1|2|3>
48
51
 
package/hook-optimize.mjs CHANGED
@@ -171,6 +171,10 @@ Give 3-6 aliases: words a user might search for the SAME concept but that are NO
171
171
  const safe = scrubRecord('observations', { text: appendedText, search_aliases: searchAliases });
172
172
  db.prepare(`UPDATE observations SET search_aliases = ?, text = ? WHERE id = ?`)
173
173
  .run(safe.search_aliases, safe.text, cand.id);
174
+ // Refresh the TF-IDF vector from the just-updated FTS text so the new
175
+ // aliases reach the vector arm too — the narrow/wide branch rebuilds, this
176
+ // one must as well. No-ops when the vector arm is off / vocab unbuilt.
177
+ rebuildVector(db, cand.id, [safe.text]);
174
178
  processed++;
175
179
  continue;
176
180
  }
package/lib/activity.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  import { sanitizeFtsQuery } from '../utils.mjs';
8
8
  import { scrubRecord } from './scrub-record.mjs';
9
9
  import { saveObservation } from './save-observation.mjs';
10
+ import { notLowSignalTitleClause } from '../scoring-sql.mjs';
10
11
 
11
12
  // Observation types (mirrors the observations.type enum) — events carry a wider
12
13
  // set, so promotion maps the extras (lesson/bug/observation) onto valid obs types.
@@ -130,30 +131,40 @@ export function recentEvents(db, { project, type = null, limit = 20 } = {}) {
130
131
  * importance>=minImportance) into searchable observations. mem_search / passive
131
132
  * injection never read the events table, so explicit /bug /lesson history and
132
133
  * high-value auto-captured events were unfindable. Each promoted event is marked
133
- * (superseded_at_epoch + superseded_by_id the new obs id) so re-runs are
134
- * idempotent. The source event row is kept (activity log intact), just retired.
134
+ * with superseded_at_epoch ONLY (superseded_by_id is a self-FK events(id), so an
135
+ * observation id there fails the FK on the real DB the marker alone gives
136
+ * idempotency) so re-runs skip it. The source event row is kept (activity log
137
+ * intact), just retired.
135
138
  *
136
- * @returns {{eligible:number, promoted:number, deduped:number}}
139
+ * @returns {{eligible:number, promoted:number, deduped:number, skipped:number}}
137
140
  */
138
141
  export function promoteInsightEvents(db, { project = null, minImportance = 2, execute = false, limit = 5000 } = {}) {
139
142
  const projClause = project ? 'AND project = ?' : '';
143
+ // Exclude low-signal titles (Modified X / Error while … / Worked on X / raw tool
144
+ // logs): those are the activity-log noise the events split was meant to contain,
145
+ // so "lesson-bearing" must not sweep them into search. Same canonical clause the
146
+ // re-enrich candidate query uses.
140
147
  const sql = `
141
148
  SELECT id, project, event_type, title, body, file_paths, importance, created_at_epoch
142
149
  FROM events
143
150
  WHERE body IS NOT NULL AND TRIM(body) != '' AND importance >= ?
144
- AND superseded_at_epoch IS NULL ${projClause}
151
+ AND superseded_at_epoch IS NULL
152
+ AND ${notLowSignalTitleClause('')}
153
+ ${projClause}
145
154
  ORDER BY created_at_epoch DESC LIMIT ?
146
155
  `;
147
156
  const rows = project ? db.prepare(sql).all(minImportance, project, limit) : db.prepare(sql).all(minImportance, limit);
148
- if (!execute) return { eligible: rows.length, promoted: 0, deduped: 0 };
157
+ if (!execute) return { eligible: rows.length, promoted: 0, deduped: 0, skipped: 0 };
149
158
 
150
- const mark = db.prepare('UPDATE events SET superseded_at_epoch = ?, superseded_by_id = ? WHERE id = ?');
151
- let promoted = 0;
152
- let deduped = 0;
153
- for (const ev of rows) {
154
- const type = OBS_TYPES.has(ev.event_type) ? ev.event_type : (EVENT_TO_OBS_TYPE[ev.event_type] || 'discovery');
155
- let files = [];
156
- try { const p = JSON.parse(ev.file_paths || '[]'); if (Array.isArray(p)) files = p; } catch { /* keep [] */ }
159
+ // Mark the source event promoted. Only `superseded_at_epoch` NOT
160
+ // superseded_by_id: that column is a self-FK (REFERENCES events(id)), so storing
161
+ // an observation id there fails a FOREIGN KEY constraint on the real DB (the
162
+ // :memory: test had FK enforcement off, which masked it). The marker alone gives
163
+ // idempotency (superseded_at_epoch IS NULL selects unpromoted rows).
164
+ const mark = db.prepare('UPDATE events SET superseded_at_epoch = ? WHERE id = ?');
165
+ // Each event's (observation insert + mark) is one transaction so a failure can
166
+ // never leave an orphan observation with an unmarked source event.
167
+ const promoteOne = db.transaction((ev, type, files) => {
157
168
  const r = saveObservation(db, {
158
169
  content: ev.body,
159
170
  title: ev.title || undefined,
@@ -166,9 +177,20 @@ export function promoteInsightEvents(db, { project = null, minImportance = 2, ex
166
177
  lesson_learned: ev.body.slice(0, 500),
167
178
  now: new Date(ev.created_at_epoch),
168
179
  });
169
- const linkId = r.kind === 'saved' ? r.id : r.existingId;
170
- mark.run(Date.now(), linkId, ev.id);
171
- if (r.kind === 'saved') promoted++; else deduped++;
180
+ mark.run(Date.now(), ev.id);
181
+ return r;
182
+ });
183
+ let promoted = 0;
184
+ let deduped = 0;
185
+ let skipped = 0;
186
+ for (const ev of rows) {
187
+ const type = OBS_TYPES.has(ev.event_type) ? ev.event_type : (EVENT_TO_OBS_TYPE[ev.event_type] || 'discovery');
188
+ let files = [];
189
+ try { const p = JSON.parse(ev.file_paths || '[]'); if (Array.isArray(p)) files = p; } catch { /* keep [] */ }
190
+ try {
191
+ const r = promoteOne(ev, type, files);
192
+ if (r.kind === 'saved') promoted++; else deduped++;
193
+ } catch { skipped++; } // a single bad event must not abort the whole backfill
172
194
  }
173
- return { eligible: rows.length, promoted, deduped };
195
+ return { eligible: rows.length, promoted, deduped, skipped };
174
196
  }
@@ -128,8 +128,11 @@ const ACTIVITY_SAVE_LESSON_RE = /activity\s+save\s+--type\s+(lesson|bugfix)\b/;
128
128
  // P2(a): /bug and /lesson were redirected from `activity save` (events) to
129
129
  // `cli.mjs save … --lesson` (searchable observations). Recognize that shape too,
130
130
  // or the unsaved-bugfix nudge over-fires after an explicit save. Anchored on the
131
- // mem CLI + the --lesson flag both skills always pass.
132
- const OBS_INSIGHT_SAVE_RE = /(?:cli\.mjs|claude-mem-lite)['"]?\s+save\b[^\n]*--lesson(?:-learned)?\b/;
131
+ // mem CLI + the --lesson flag both skills always pass. Uses `[\s\S]*?` (not
132
+ // `[^\n]*`) because the skill templates document a multi-line, backslash-
133
+ // continued command — after JSON.parse the command carries real newlines, so the
134
+ // gap between `save` and `--lesson` must be allowed to span them (lazy → nearest).
135
+ const OBS_INSIGHT_SAVE_RE = /(?:cli\.mjs|claude-mem-lite)['"]?\s+save\b[\s\S]*?--lesson(?:-learned)?\b/;
133
136
  const MEM_SAVE_TOOL_NAMES = new Set([
134
137
  'mem_save',
135
138
  'mcp__claude_mem_lite__mem_save',
@@ -113,7 +113,10 @@ export function saveObservation(db, params) {
113
113
  });
114
114
 
115
115
  insertObservationFiles(db, savedId, files);
116
- insertObservationVector(db, savedId, safeTitle + ' ' + safeContent);
116
+ // Vector text mirrors the FTS-indexed content (title + content + lesson) so a
117
+ // lesson-only term isn't invisible to the vector arm (finding #8). Reuses the
118
+ // same indexText the FTS `text` field is built from.
119
+ insertObservationVector(db, savedId, indexText);
117
120
 
118
121
  return savedId;
119
122
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.39.0",
3
+ "version": "3.39.2",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",