claude-mem-lite 3.73.0 → 3.74.0

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.0",
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.0",
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,97 @@ 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
+ const errLines = String(response || '').split('\n').filter(l => ERROR_LINE_RE.test(l)).slice(0, 3);
137
141
  for (const line of errLines) {
138
142
  const tokens = line.replace(/[^a-zA-Z0-9_.-]/g, ' ').split(/\s+/)
139
143
  .filter(w => w.length > 3 && !/^\d+$/.test(w));
140
144
  for (const t of tokens.slice(0, 5)) {
141
145
  const lt = t.toLowerCase();
142
- if (!ERROR_STOP_WORDS.has(lt)) words.add(lt);
146
+ if (!ERROR_STOP_WORDS.has(lt) && !seen.has(lt)) { seen.add(lt); errWords.push(lt); }
143
147
  }
144
148
  }
145
- const result = [...words].slice(0, 6);
149
+ return { cmdWords, errWords };
150
+ }
151
+
152
+ /**
153
+ * Extract discriminative keywords from a failed command and its error output.
154
+ * Filters out common stop words to produce useful FTS5 search terms.
155
+ * @param {string} cmd The command that was executed
156
+ * @param {string} response The error output text
157
+ * @returns {string[]|null} Array of 1-6 keywords or null if none found
158
+ */
159
+ export function extractErrorKeywords(cmd, response) {
160
+ const { cmdWords, errWords } = collectErrorTerms(cmd, response);
161
+ const result = [...cmdWords, ...errWords].slice(0, ERROR_RECALL_MAX_TERMS);
146
162
  return result.length >= 1 ? result : null;
147
163
  }
148
164
 
165
+ /**
166
+ * Decide whether the error-recall surface should fire, and with which terms (D#136).
167
+ *
168
+ * Two defects this closes, both measured against the live DB on 2026-08-22 (obs
169
+ * #10730 carries the readings):
170
+ *
171
+ * 1. NO ERROR SIGNAL ⇒ NO INJECTION. This surface fires on detectBashSignificance's
172
+ * isHardError, and that gate's HARD_ERROR_RE is NOT in sync with ERROR_LINE_RE
173
+ * here: HARD_ERROR_RE accepts `ERR!`, `enoent` and `traceback`, while ERROR_LINE_RE
174
+ * only matches the whole word `error`. So npm's own failure output clears the
175
+ * trigger and then yields ZERO error lines — `npm ERR! code ENOENT / npm ERR!
176
+ * enoent ENOENT: no such file or directory` contains no `error`, no `fail`, and no
177
+ * `not found` (it says "no such file"). The keyword set then degraded to pure
178
+ * command words — literally ['npm','run','build'] — and the surface searched the
179
+ * COMMAND'S TOPIC instead of the failure. Same for a Python traceback whose head
180
+ * lines carry `Traceback (most recent call last):` and a bare `File "x.py"`.
181
+ * Both are among the most common failures a session produces.
182
+ * With no error term there is nothing to recall ON, so the honest answer is
183
+ * silence rather than a topic match. Widening ERROR_LINE_RE is NOT the fix —
184
+ * enumeration always misses one more shape, and this gate is correct for every
185
+ * shape it misses. (Verified: a `grep` killed by seccomp does NOT reach here at
186
+ * all — isHardError is false for it — so that shape is not evidence for this gate.)
187
+ *
188
+ * 2. COMMAND WORDS STAY IN THE QUERY — a demotion was TRIED AND REJECTED on data.
189
+ * The obvious follow-up is to drop `npm` / `run` / `grep` from the query, since
190
+ * they demonstrably let BM25 return release records for a missing-module failure.
191
+ * Replaying five real failures against the live DB (2026-08-22) says the trade is
192
+ * not one-way: error-terms-only did fix `npm run build` (it surfaced #8721
193
+ * ERR_MODULE_NOT_FOUND and #8185 SOURCE_FILES, the rows that actually explain it),
194
+ * but it REGRESSED two others — dropping `database` lost #8673 (plugin-mode
195
+ * data-dir skew) for a failed DB open, and dropping `vitest` lost #8725 (test
196
+ * fails locally) for a test failure. Command words are carrying domain anchoring,
197
+ * not just noise. A demote-to-fallback variant measured byte-identical to
198
+ * error-terms-only (12 rows either way): the primary query always filled its
199
+ * LIMIT 3, so the fallback never ran. Net: gate only, selection unchanged.
200
+ *
201
+ * @param {string} cmd The command that was executed
202
+ * @param {string} response The error output text
203
+ * @returns {{terms: string[]}|null} null ⇒ do not inject
204
+ */
205
+ export function planErrorRecall(cmd, response) {
206
+ const { cmdWords, errWords } = collectErrorTerms(cmd, response);
207
+ if (errWords.length === 0) return null;
208
+ return { terms: [...cmdWords, ...errWords].slice(0, ERROR_RECALL_MAX_TERMS) };
209
+ }
210
+
149
211
  // ─── File Paths ──────────────────────────────────────────────────────────────
150
212
 
151
213
  /**
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. planErrorRecall (D#136) returns null when the output
477
+ // carried NO error-signal token. isHardError above and that check do NOT use the
478
+ // same pattern list: HARD_ERROR_RE accepts `ERR!`/`enoent`/`traceback`, while the
479
+ // line filter wants the whole word `error`. npm's own failure text clears the
480
+ // former and yields zero lines to the latter, so `npm run build` failing on
481
+ // ENOENT used to query ['npm','run','build'] — the command's topic, not the
482
+ // failure. Silence is the honest answer there.
483
+ // The term list itself is unchanged when the gate passes (rationale at the seam).
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.0",
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.0",
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.0",
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';