claude-mem-lite 3.59.0 → 3.59.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.59.0",
13
+ "version": "3.59.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.59.0",
3
+ "version": "3.59.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/lib/activity.mjs CHANGED
@@ -7,7 +7,9 @@
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
+ // Pure title-only builder: this query runs on the EVENTS table, which has no
11
+ // lesson_learned column — the lesson-escape variant would be a SQL error here.
12
+ import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
11
13
  import { OBS_TYPE_SET } from './obs-types.mjs';
12
14
 
13
15
  // Observation types (mirrors the observations.type enum) — events carry a wider
@@ -150,7 +152,7 @@ export function promoteInsightEvents(db, { project = null, minImportance = 2, ex
150
152
  FROM events
151
153
  WHERE body IS NOT NULL AND TRIM(body) != '' AND importance >= ?
152
154
  AND superseded_at_epoch IS NULL
153
- AND ${notLowSignalTitleClause('')}
155
+ AND ${buildNotLowSignalSql('')}
154
156
  ${projClause}
155
157
  ORDER BY created_at_epoch DESC LIMIT ?
156
158
  `;
@@ -50,13 +50,29 @@ export function buildLowSignalRegex() {
50
50
  * Build the SQL NOT LIKE clause chain, optionally prefixed with a table alias.
51
51
  * Output is a single parenthesized AND-chain — safe to combine with other AND/OR.
52
52
  *
53
+ * `lessonEscape` (2026-07-24 audit P1, D#11): retrieval consumers on the
54
+ * observations table pass `{ lessonEscape: true }` to admit rows whose title
55
+ * matches a LOW_SIGNAL pattern but which carry a real lesson_learned — the
56
+ * read-side counterpart of the isNoiseObservation/capNoiseImportance write-side
57
+ * signal escapes. Without it, a substantive obs titled "npm pack drops …" is
58
+ * unsearchable forever. Escape is lesson-only by design: an importance>=2
59
+ * escape would resurrect pre-v2.47 Haiku-inflated noise on legacy DBs.
60
+ * Default stays title-only because two consumers query the events table
61
+ * (no lesson_learned column) and two are noise-title METRICS, not filters.
62
+ *
53
63
  * @param {string} [alias=''] Table alias (e.g. 'o') — empty for unqualified.
64
+ * @param {object} [opts]
65
+ * @param {boolean} [opts.lessonEscape=false] Admit rows with non-empty, non-'none' lesson_learned.
54
66
  * @returns {string} SQL boolean expression
55
67
  */
56
- export function buildNotLowSignalSql(alias = '') {
68
+ export function buildNotLowSignalSql(alias = '', { lessonEscape = false } = {}) {
57
69
  const p = alias ? `${alias}.` : '';
58
70
  const clauses = LOW_SIGNAL_PATTERNS.map(({ like }) => `${p}title NOT LIKE '${like}'`);
59
- return '(\n ' + clauses.join('\n AND ') + '\n )';
71
+ const chain = '(\n ' + clauses.join('\n AND ') + '\n )';
72
+ if (!lessonEscape) return chain;
73
+ // Mirrors the write-side lesson test: String(lesson).trim().toLowerCase() not in ('', 'none').
74
+ const escape = `(${p}lesson_learned IS NOT NULL AND LOWER(TRIM(${p}lesson_learned)) NOT IN ('', 'none'))`;
75
+ return `(${chain} OR ${escape})`;
60
76
  }
61
77
 
62
78
  // Cached singleton — isNoiseObservation is called once per observation insert.
@@ -3,7 +3,7 @@
3
3
  // Splits pure data aggregation from text rendering so MCP handlers don't
4
4
  // collide with CLI's `out()` stdout-write pattern.
5
5
 
6
- import { notLowSignalTitleClause } from '../scoring-sql.mjs';
6
+ import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
7
7
  import { truncate } from '../format-utils.mjs';
8
8
  import { COMPRESSED_PENDING_PURGE } from '../utils.mjs';
9
9
 
@@ -27,9 +27,11 @@ export function computeQualityStats(db, { project, days }) {
27
27
  const baseParams = project ? [project] : [];
28
28
  const cutoff = Date.now() - days * 86400000;
29
29
 
30
- // LOW_SIGNAL match = NOT notLowSignal. Shared helper keeps SQL in sync
31
- // with scoring-sql.mjs and pre-tool-recall.js Edit-fallback filter.
32
- const lowSignalIsMatchExpr = `NOT ${notLowSignalTitleClause('')}`;
30
+ // LOW_SIGNAL match = NOT notLowSignal. Pure title-only builder: this is a
31
+ // METRIC counting pattern-titled rows ("Low-signal titles" in stats output),
32
+ // not a retrieval filter — the lesson-escape variant would silently exclude
33
+ // lesson-bearing rows from the count and understate title degradation.
34
+ const lowSignalIsMatchExpr = `NOT ${buildNotLowSignalSql('')}`;
33
35
 
34
36
  // Narrative-text proxy for bugfix investigations that never landed a fix.
35
37
  const unresolvedNarrativeExpr = `(
package/memdir.mjs CHANGED
@@ -304,9 +304,30 @@ export function removePluginDoc(memdir, slug) {
304
304
  // noise.
305
305
 
306
306
  const AUDIT_FILE_RE = /^(feedback|project)_[A-Za-z0-9_-]+\.md$/;
307
+ // Legacy skip prefixes: user_*/reference_* have no Why/How requirement. A known
308
+ // filename prefix wins over frontmatter type (a user_*.md stays excluded even if
309
+ // its frontmatter says feedback).
310
+ const SKIP_FILE_RE = /^(user|reference)_[A-Za-z0-9_-]+\.md$/;
307
311
  const WHY_RE = /^\s*\*\*Why:\*\*/m;
308
312
  const HOW_RE = /^\s*\*\*How to apply:\*\*/m;
309
313
 
314
+ /**
315
+ * Extract the memory type from a file's YAML frontmatter (2026-07-24 audit P2).
316
+ * The current CC harness writes kebab-case filenames (ship-runbook.md) and puts
317
+ * the type in frontmatter — either top-level `type: X` or nested under
318
+ * `metadata:` as ` type: X`. Anchored `^\s*type:` cannot match `node_type:`.
319
+ *
320
+ * @param {string} raw Full file content
321
+ * @returns {string|null} Lowercased type, or null when absent/no frontmatter
322
+ */
323
+ function frontmatterType(raw) {
324
+ if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) return null;
325
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
326
+ if (!m) return null;
327
+ const t = m[1].match(/^\s*type:\s*([A-Za-z]+)\s*$/m);
328
+ return t ? t[1].toLowerCase() : null;
329
+ }
330
+
310
331
  /**
311
332
  * Strip the leading YAML frontmatter block (between `---` fences) so audit
312
333
  * checks run only against body content. Returns input unchanged if no
@@ -339,13 +360,29 @@ export function auditMemdir(memdir) {
339
360
  let entries;
340
361
  try { entries = readdirSync(memdir); } catch { return result; }
341
362
 
342
- const targets = entries.filter(n => AUDIT_FILE_RE.test(n)).sort();
343
- for (const name of targets) {
344
- let body = '';
345
- try {
346
- const raw = readFileSync(join(memdir, name), 'utf8');
347
- body = stripFrontmatter(raw);
348
- } catch { /* unreadable — count as missingBoth */ }
363
+ // Two selection paths (2026-07-24 audit P2):
364
+ // 1. legacy filename prefix feedback_*/project_* audited, user_*/reference_* skipped
365
+ // 2. kebab-case (current harness) — frontmatter type ∈ {feedback, project}
366
+ // Path 2 needs the file content; read once and reuse for the body check.
367
+ const candidates = entries
368
+ .filter(n => n.endsWith('.md') && n !== 'MEMORY.md' && !n.startsWith('.'))
369
+ .sort();
370
+ let total = 0;
371
+ for (const name of candidates) {
372
+ const legacyAudit = AUDIT_FILE_RE.test(name);
373
+ if (!legacyAudit && SKIP_FILE_RE.test(name)) continue;
374
+
375
+ let raw = null;
376
+ try { raw = readFileSync(join(memdir, name), 'utf8'); } catch { /* unreadable */ }
377
+
378
+ if (!legacyAudit) {
379
+ // Frontmatter decides; unreadable or untyped files carry no Why/How contract.
380
+ const type = raw === null ? null : frontmatterType(raw);
381
+ if (type !== 'feedback' && type !== 'project') continue;
382
+ }
383
+ // Legacy-prefixed unreadable file keeps the old behavior: counted as missingBoth.
384
+ const body = raw === null ? '' : stripFrontmatter(raw);
385
+ total += 1;
349
386
 
350
387
  const hasWhy = WHY_RE.test(body);
351
388
  const hasHow = HOW_RE.test(body);
@@ -354,6 +391,6 @@ export function auditMemdir(memdir) {
354
391
  else if (!hasWhy) result.missingWhy.push(name);
355
392
  else result.missingHowToApply.push(name);
356
393
  }
357
- result.total = targets.length;
394
+ result.total = total;
358
395
  return result;
359
396
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.59.0",
3
+ "version": "3.59.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.59.0",
9
+ "version": "3.59.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.59.0",
3
+ "version": "3.59.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/scoring-sql.mjs CHANGED
@@ -152,8 +152,17 @@ export function noisePenaltyClause(alias = 'o') {
152
152
  // The SQL path (this), the regex path (utils.mjs::LOW_SIGNAL_TITLE), and the
153
153
  // pre-tool-recall.js inline SQL now all derive from one authoritative
154
154
  // pattern list. Previously hand-mirrored with "keep in sync" comments.
155
+ //
156
+ // lessonEscape (2026-07-24 audit P1, D#11): every consumer of THIS clause is an
157
+ // observations-table retrieval surface (search, recall, error-recall, context/
158
+ // handoff/UPS injection, optimize candidates), so all get the read-side lesson
159
+ // escape — a low-signal TITLE no longer hides a row with a real lesson_learned.
160
+ // Consumers that must stay title-only import buildNotLowSignalSql directly:
161
+ // events-table queries (lib/activity.mjs, pre-tool-recall.js events fallback —
162
+ // no lesson_learned column) and noise-title metrics (lib/stats-core.mjs,
163
+ // lib/stats-quality.mjs — they COUNT pattern-titled rows, not filter them).
155
164
  export function notLowSignalTitleClause(alias = 'o') {
156
- return buildNotLowSignalSql(alias);
165
+ return buildNotLowSignalSql(alias, { lessonEscape: true });
157
166
  }
158
167
 
159
168
  // ─── Cite-history factor (A1, v2.83) ────────────────────────────────────────