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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/commands/bug.md +6 -3
- package/commands/lesson.md +6 -3
- package/hook-optimize.mjs +4 -0
- package/lib/activity.mjs +38 -16
- package/lib/cite-back-hint.mjs +5 -2
- package/lib/save-observation.mjs +4 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.39.
|
|
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.
|
|
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;
|
|
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
|
|
package/commands/lesson.md
CHANGED
|
@@ -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
|
|
40
|
-
`--lesson` so it lands in the high-weight `lesson_learned`
|
|
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 "<
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
}
|
package/lib/cite-back-hint.mjs
CHANGED
|
@@ -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
|
-
|
|
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',
|
package/lib/save-observation.mjs
CHANGED
|
@@ -113,7 +113,10 @@ export function saveObservation(db, params) {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
insertObservationFiles(db, savedId, files);
|
|
116
|
-
|
|
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.
|
|
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",
|