claude-mem-lite 3.73.0 → 3.74.1

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.73.0",
13
+ "version": "3.74.1",
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.73.0",
3
+ "version": "3.74.1",
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/bash-utils.mjs CHANGED
@@ -117,35 +117,128 @@ const ERROR_STOP_WORDS = new Set([
117
117
  'node', 'require', 'stack', 'trace',
118
118
  ]);
119
119
 
120
+ const ERROR_LINE_RE = /error|fail|exception|cannot|not found|undefined|null/i;
121
+ const ERROR_RECALL_MAX_TERMS = 6;
122
+
120
123
  /**
121
- * Extract discriminative keywords from a failed command and its error output.
122
- * Filters out common stop words to produce useful FTS5 search terms.
123
- * @param {string} cmd The command that was executed
124
- * @param {string} response The error output text
125
- * @returns {string[]|null} Array of 1-6 keywords or null if none found
124
+ * Split a failed command + its output into command-derived and error-derived terms.
125
+ * Shared by extractErrorKeywords (merged view, unchanged contract) and
126
+ * planErrorRecall (which needs the two classes kept apart). Dedup is deliberately
127
+ * ACROSS both classes, command-first, so the merged view is byte-identical to the
128
+ * pre-split single-Set implementation.
129
+ * @returns {{cmdWords: string[], errWords: string[]}}
126
130
  */
127
- export function extractErrorKeywords(cmd, response) {
128
- const words = new Set();
129
- const cmdParts = cmd.split(/[\s/\\|&;]+/).filter(w => w.length > 2 && !/^-/.test(w));
131
+ function collectErrorTerms(cmd, response) {
132
+ const seen = new Set();
133
+ const cmdWords = [];
134
+ const cmdParts = String(cmd || '').split(/[\s/\\|&;]+/).filter(w => w.length > 2 && !/^-/.test(w));
130
135
  for (const w of cmdParts.slice(0, 3)) {
131
136
  const lw = w.toLowerCase();
132
- if (!ERROR_STOP_WORDS.has(lw)) words.add(lw);
137
+ if (!ERROR_STOP_WORDS.has(lw) && !seen.has(lw)) { seen.add(lw); cmdWords.push(lw); }
133
138
  }
134
- const errLines = response.split('\n').filter(l =>
135
- /error|fail|exception|cannot|not found|undefined|null/i.test(l)
136
- ).slice(0, 3);
139
+ const errWords = [];
140
+ // The line filter is the TRIGGER's pattern list OR'd with the prose one. Anything
141
+ // that made detectBashSignificance call this a hard error is, by construction, also
142
+ // something we will extract terms from — which closes the "trigger fired, extractor
143
+ // found nothing, so we queried the command's own words" class without enumerating
144
+ // failure shapes. ERROR_LINE_RE alone missed `npm ERR! code ENOENT` (no `error`, no
145
+ // `fail`, no `not found` — npm says "no such file") and `panic: assignment to entry
146
+ // in nil map`, while letting `panic: runtime error: …` through purely because that
147
+ // message happens to contain the substring `error`.
148
+ // Note HARD_ERROR_RE's `\n\s+at\s+\S` alternative cannot match a single line (it
149
+ // needs the preceding newline); that is fine — it is a stack-frame anchor, and the
150
+ // frames it guards are accompanied by a line the other alternatives do catch.
151
+ const errLines = String(response || '')
152
+ .split('\n')
153
+ .filter((l) => ERROR_LINE_RE.test(l) || HARD_ERROR_RE.test(l))
154
+ .slice(0, 3);
137
155
  for (const line of errLines) {
138
156
  const tokens = line.replace(/[^a-zA-Z0-9_.-]/g, ' ').split(/\s+/)
139
157
  .filter(w => w.length > 3 && !/^\d+$/.test(w));
140
158
  for (const t of tokens.slice(0, 5)) {
141
159
  const lt = t.toLowerCase();
142
- if (!ERROR_STOP_WORDS.has(lt)) words.add(lt);
160
+ if (!ERROR_STOP_WORDS.has(lt) && !seen.has(lt)) { seen.add(lt); errWords.push(lt); }
143
161
  }
144
162
  }
145
- const result = [...words].slice(0, 6);
163
+ return { cmdWords, errWords };
164
+ }
165
+
166
+ /**
167
+ * Extract discriminative keywords from a failed command and its error output.
168
+ * Filters out common stop words to produce useful FTS5 search terms.
169
+ * @param {string} cmd The command that was executed
170
+ * @param {string} response The error output text
171
+ * @returns {string[]|null} Array of 1-6 keywords or null if none found
172
+ */
173
+ export function extractErrorKeywords(cmd, response) {
174
+ const { cmdWords, errWords } = collectErrorTerms(cmd, response);
175
+ const result = [...cmdWords, ...errWords].slice(0, ERROR_RECALL_MAX_TERMS);
146
176
  return result.length >= 1 ? result : null;
147
177
  }
148
178
 
179
+ /**
180
+ * Decide whether the error-recall surface should fire, and with which terms (D#136).
181
+ *
182
+ * Two defects this closes, both measured against the live DB on 2026-08-22 (obs
183
+ * #10730 carries the readings):
184
+ *
185
+ * 1. THE SELECTION FILTER IS A SUPERSET OF THE TRIGGER. This surface fires on
186
+ * detectBashSignificance's isHardError (HARD_ERROR_RE), but term extraction used to
187
+ * keep only lines matching ERROR_LINE_RE — a DIFFERENT list. The two diverge:
188
+ * HARD_ERROR_RE accepts `ERR!`, `enoent`, `panic`, `traceback`; ERROR_LINE_RE takes
189
+ * `error|fail|exception|cannot|not found|undefined|null` as SUBSTRINGS (no word
190
+ * boundaries — `AssertionError` matches on `error`). npm's own output sits in the
191
+ * gap: `npm ERR! code ENOENT / npm ERR! enoent ENOENT: no such file or directory`
192
+ * has no `error`, no `fail`, no `not found` (npm says "no such file"), so it cleared
193
+ * the trigger and then yielded ZERO lines to extract from. The keyword set degraded
194
+ * to pure command words — literally ['npm','run','build'] — and the surface searched
195
+ * the COMMAND'S TOPIC instead of the failure.
196
+ * The sharpest symptom was Go: `panic: assignment to entry in nil map` was silenced
197
+ * while `panic: runtime error: index out of range` was not, purely because the
198
+ * second message happens to contain the substring `error`. Recall depending on the
199
+ * wording of a panic is the same divergence, relocated.
200
+ * OR-ing HARD_ERROR_RE into the line filter closes the class BY CONSTRUCTION rather
201
+ * than by enumerating shapes: whatever convinced the trigger this was a hard error
202
+ * is, by definition, also something we will read terms from. (Widening ERROR_LINE_RE
203
+ * ad hoc WOULD be enumeration; making it a superset of the trigger is not.)
204
+ *
205
+ * 2. COMMAND WORDS STAY IN THE QUERY — a demotion was TRIED AND REJECTED on data.
206
+ * The obvious follow-up is to drop `npm` / `run` / `grep` from the query, since
207
+ * they demonstrably let BM25 return release records for a missing-module failure.
208
+ * Replaying five real failures against the live DB (2026-08-22) says the trade is
209
+ * not one-way: error-terms-only did fix `npm run build` (it surfaced #8721
210
+ * ERR_MODULE_NOT_FOUND and #8185 SOURCE_FILES, the rows that actually explain it),
211
+ * but it REGRESSED two others — dropping `database` lost #8673 (plugin-mode
212
+ * data-dir skew) for a failed DB open, and dropping `vitest` lost #8725 (test
213
+ * fails locally) for a test failure. Command words are carrying domain anchoring,
214
+ * not just noise. A demote-to-fallback variant measured byte-identical to
215
+ * error-terms-only (12 rows either way): the primary query always filled its
216
+ * LIMIT 3, so the fallback never ran.
217
+ *
218
+ * 3. THE RESIDUAL GATE. With (1) in place this fires rarely, but it is not dead: a
219
+ * failure can still yield no usable term — empty output, or a line whose tokens are
220
+ * all stop words (`Error: it failed`). There is then nothing to recall ON, and
221
+ * silence beats querying the command's topic.
222
+ * Read the predicate precisely: `errWords` excludes anything ALREADY taken as a
223
+ * command word, because collectErrorTerms dedups across both classes with the
224
+ * command filled first. So this is "no error term that is not also in the command",
225
+ * not "no error term". `docker compose up -d` and `docker stack deploy` on the SAME
226
+ * output decide differently for exactly that reason — the first has `compose` in the
227
+ * command, the second does not. That asymmetry is inherited from the pre-split
228
+ * single-Set implementation and is preserved deliberately; it is documented here
229
+ * rather than silently "fixed" because changing it would change extractErrorKeywords
230
+ * for every caller, which is a separate decision from this one.
231
+ *
232
+ * @param {string} cmd The command that was executed
233
+ * @param {string} response The error output text
234
+ * @returns {{terms: string[]}|null} null ⇒ do not inject
235
+ */
236
+ export function planErrorRecall(cmd, response) {
237
+ const { cmdWords, errWords } = collectErrorTerms(cmd, response);
238
+ if (errWords.length === 0) return null;
239
+ return { terms: [...cmdWords, ...errWords].slice(0, ERROR_RECALL_MAX_TERMS) };
240
+ }
241
+
149
242
  // ─── File Paths ──────────────────────────────────────────────────────────────
150
243
 
151
244
  /**
package/hook.mjs CHANGED
@@ -24,7 +24,7 @@ import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statS
24
24
  import { homedir } from 'os';
25
25
  import {
26
26
  truncate, inferProject, detectBashSignificance,
27
- extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
27
+ planErrorRecall, extractFilePaths, isRelatedToEpisode,
28
28
  makeEntryDesc, scrubSecrets, stripPrivate, EDIT_TOOLS, debugCatch, debugLog,
29
29
  COMPRESSED_AUTO, OBS_BM25, notLowSignalTitleClause, formatErrorRecallHints,
30
30
  MAX_HOOK_STDIN_BYTES,
@@ -473,13 +473,20 @@ function triggerErrorRecall(db, toolInput, response) {
473
473
  try {
474
474
  const project = inferProject();
475
475
 
476
- // Extract error keywords
476
+ // Extract error keywords (D#136). The extractor's line filter is HARD_ERROR_RE —
477
+ // the very predicate isHardError above used — OR'd with the prose one, so anything
478
+ // that reaches this line is also something we can read terms from. Before that,
479
+ // the two lists diverged and npm's own output fell in the gap: `npm ERR! code
480
+ // ENOENT` has no `error`/`fail`/`not found`, so extraction yielded nothing and the
481
+ // query degraded to ['npm','run','build'] — the command's topic, not the failure.
482
+ // planErrorRecall still returns null when nothing usable survives (empty output, or
483
+ // only stop words), and then we stay silent rather than query the command's topic.
477
484
  const cmd = toolInput.command || '';
478
- const keywords = extractErrorKeywords(cmd, response);
479
- if (!keywords || keywords.length === 0) return;
485
+ const plan = planErrorRecall(cmd, response);
486
+ if (!plan) return;
480
487
 
481
488
  // FTS5 OR query for broader recall
482
- const ftsQuery = keywords.map(t => `"${t.replace(/"/g, '""')}"`).join(' OR ');
489
+ const ftsQuery = plan.terms.map(t => `"${t.replace(/"/g, '""')}"`).join(' OR ');
483
490
  if (!ftsQuery) return;
484
491
 
485
492
  const nowR = Date.now();
package/install.mjs CHANGED
@@ -1737,8 +1737,17 @@ async function doctor() {
1737
1737
  try { currentVersion = JSON.parse(readFileSync(join(PROJECT_DIR, 'package.json'), 'utf8')).version; } catch { /* fall through with empty version */ }
1738
1738
  const stale = lines.filter(l => isStaleMemProcess(l, currentVersion));
1739
1739
  if (stale.length > 0) {
1740
+ // ⚠-level ONLY, deliberately not `issues++`. buildDoctorSummary's contract is
1741
+ // "issues are ✗-level (action required); warnings are ⚠-level (informational)",
1742
+ // and an old process is the one finding here the user cannot act on from a
1743
+ // doctor run: auto-update bumps installed_plugins.json but cannot kill the MCP
1744
+ // process an active session already spawned, so a correct, healthy install
1745
+ // reports this for as long as that session lives. Counting it made `doctor`
1746
+ // exit 1 while every line on screen was ✓ or ⚠ — it failed the v3.70.0 release
1747
+ // `validate` job (where the "old processes" were vitest's own workers) and it
1748
+ // reddens doctor-install-shape-e2e's "instead of going red forever" case on any
1749
+ // dev box with a previous-version session still open.
1740
1750
  warn(`Old processes running${currentVersion ? ` (current: v${currentVersion})` : ''}:\n ` + stale.join('\n '));
1741
- issues++;
1742
1751
  } else {
1743
1752
  ok('No stale processes');
1744
1753
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.73.0",
3
+ "version": "3.74.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.73.0",
9
+ "version": "3.74.1",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.26.0",
12
12
  "better-sqlite3": "^12.6.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.73.0",
3
+ "version": "3.74.1",
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",
package/utils.mjs CHANGED
@@ -16,7 +16,7 @@ export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
16
16
  export { stripPrivate } from './lib/private-strip.mjs';
17
17
  export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey, formatErrorRecallHints, neutralizeContextDelimiters } from './format-utils.mjs';
18
18
  export { computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from './hash-utils.mjs';
19
- export { detectBashSignificance, extractErrorKeywords, extractFilePaths, stripTestSuffix } from './bash-utils.mjs';
19
+ export { detectBashSignificance, extractErrorKeywords, planErrorRecall, extractFilePaths, stripTestSuffix } from './bash-utils.mjs';
20
20
 
21
21
  // Internal imports for functions that remain in this module
22
22
  import { truncate } from './format-utils.mjs';