claude-mem-lite 2.36.0 → 2.37.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook-memory.mjs +23 -7
- package/package.json +1 -1
- package/schema.mjs +8 -1
- package/scoring-sql.mjs +38 -0
- package/scripts/user-prompt-search.js +22 -2
- package/utils.mjs +1 -1
package/hook-memory.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// claude-mem-lite — Semantic Memory Injection
|
|
2
2
|
// Search past observations for relevant memories to inject as context at user-prompt time.
|
|
3
3
|
|
|
4
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, OBS_BM25, notLowSignalTitleClause } from './utils.mjs';
|
|
4
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause } from './utils.mjs';
|
|
5
5
|
|
|
6
6
|
const MAX_MEMORY_INJECTIONS = 3;
|
|
7
7
|
const MEMORY_LOOKBACK_MS = 60 * 86400000; // 60 days
|
|
@@ -42,9 +42,14 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
42
42
|
// R1: notLowSignalTitleClause() excludes hook-llm fallback titles
|
|
43
43
|
// ("Modified X", "Worked on X", "Reviewed N files:", raw error logs, etc.)
|
|
44
44
|
// that almost never get referenced (3.3% access rate) but compete for BM25 rank.
|
|
45
|
+
// v26 P0: noise_penalty is multiplied AFTER sort-BM25 so the column used
|
|
46
|
+
// for ORDER BY stays the penalty-adjusted `relevance` applied downstream
|
|
47
|
+
// in JS (scored.sort). SELECT exposes both raw BM25 (for sort) and the
|
|
48
|
+
// penalty factor (for the final JS score).
|
|
45
49
|
const selectStmt = db.prepare(`
|
|
46
50
|
SELECT o.id, o.type, o.title, o.importance, o.lesson_learned, o.project,
|
|
47
|
-
${OBS_BM25} as relevance
|
|
51
|
+
${OBS_BM25} as relevance,
|
|
52
|
+
${noisePenaltyClause('o')} as noise_penalty
|
|
48
53
|
FROM observations_fts
|
|
49
54
|
JOIN observations o ON o.id = observations_fts.rowid
|
|
50
55
|
WHERE observations_fts MATCH ?
|
|
@@ -80,7 +85,8 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
80
85
|
try {
|
|
81
86
|
const crossStmt = db.prepare(`
|
|
82
87
|
SELECT o.id, o.type, o.title, o.importance, o.lesson_learned, o.project,
|
|
83
|
-
${OBS_BM25} as relevance
|
|
88
|
+
${OBS_BM25} as relevance,
|
|
89
|
+
${noisePenaltyClause('o')} as noise_penalty
|
|
84
90
|
FROM observations_fts
|
|
85
91
|
JOIN observations o ON o.id = observations_fts.rowid
|
|
86
92
|
WHERE observations_fts MATCH ?
|
|
@@ -105,12 +111,14 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
105
111
|
|
|
106
112
|
// Merge and score: same-project full weight, cross-project 0.7x
|
|
107
113
|
// OR-fallback results get 0.4x penalty — they matched individual words, not the full intent
|
|
114
|
+
// v26 P0: noise_penalty (from SQL) shrinks high-inject/low-cite rows.
|
|
108
115
|
const allRows = [...rows.map(r => ({ ...r, _or: usedOrFallback })), ...crossRows.map(r => ({ ...r, _or: crossUsedOr }))];
|
|
109
116
|
const scored = allRows
|
|
110
117
|
.filter(r => !excludeSet.has(r.id))
|
|
111
118
|
.map(r => {
|
|
112
119
|
const crossProjectPenalty = r.project === project ? 1.0 : 0.7;
|
|
113
120
|
const orFallbackPenalty = r._or ? 0.4 : 1.0;
|
|
121
|
+
const noisePenalty = typeof r.noise_penalty === 'number' ? r.noise_penalty : 1.0;
|
|
114
122
|
return {
|
|
115
123
|
...r,
|
|
116
124
|
score: Math.abs(r.relevance)
|
|
@@ -118,7 +126,8 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
118
126
|
* (r.lesson_learned ? 1.5 : 1.0)
|
|
119
127
|
* (r.importance >= 2 ? 1.0 : 0.6)
|
|
120
128
|
* crossProjectPenalty
|
|
121
|
-
* orFallbackPenalty
|
|
129
|
+
* orFallbackPenalty
|
|
130
|
+
* noisePenalty,
|
|
122
131
|
};
|
|
123
132
|
})
|
|
124
133
|
.sort((a, b) => b.score - a.score);
|
|
@@ -133,12 +142,19 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
133
142
|
const aboveThreshold = scored.filter(r => r.score >= threshold);
|
|
134
143
|
if (aboveThreshold.length === 0) return [];
|
|
135
144
|
|
|
136
|
-
//
|
|
145
|
+
// v26 P0: bump injection_count (NOT access_count) for injected rows.
|
|
146
|
+
// Before v26 this was bumping access_count, which conflated auto-injection
|
|
147
|
+
// with real cites/recalls/opens — polluting the noise-ratio signal the
|
|
148
|
+
// penalty clause now depends on. access_count is reserved for explicit
|
|
149
|
+
// access (cmdRecall/cmdGet/cmdTimeline/pre-tool-recall/citation-tracker).
|
|
150
|
+
// Per-row try/catch for FTS trigger safety (project_non_obvious.md).
|
|
137
151
|
const result = aboveThreshold.slice(0, MAX_MEMORY_INJECTIONS);
|
|
138
152
|
const now = Date.now();
|
|
139
|
-
const
|
|
153
|
+
const bumpStmt = db.prepare(
|
|
154
|
+
'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
|
|
155
|
+
);
|
|
140
156
|
for (const r of result) {
|
|
141
|
-
|
|
157
|
+
try { bumpStmt.run(now, r.id); } catch {}
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
return result;
|
package/package.json
CHANGED
package/schema.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export const DB_PATH = join(DB_DIR, 'claude-mem-lite.db');
|
|
|
13
13
|
export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
14
14
|
|
|
15
15
|
// Increment when schema changes (tables, columns, indexes, FTS, migrations)
|
|
16
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
16
|
+
export const CURRENT_SCHEMA_VERSION = 26;
|
|
17
17
|
|
|
18
18
|
const CORE_SCHEMA = `
|
|
19
19
|
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
|
@@ -112,6 +112,13 @@ const MIGRATIONS = [
|
|
|
112
112
|
'ALTER TABLE observations ADD COLUMN superseded_by INTEGER DEFAULT NULL',
|
|
113
113
|
'ALTER TABLE observations ADD COLUMN last_accessed_at INTEGER DEFAULT NULL',
|
|
114
114
|
'ALTER TABLE observations ADD COLUMN optimized_at INTEGER DEFAULT NULL',
|
|
115
|
+
// v26 (P0 injection-noise): per-obs injection tracking for noise-ratio
|
|
116
|
+
// penalty. injection_count bumps only on UserPromptSubmit / hook-memory
|
|
117
|
+
// auto-injection (not on explicit recall/get/timeline — those keep bumping
|
|
118
|
+
// access_count). Pair with access_count to compute noise ratio: high
|
|
119
|
+
// injection_count + low access_count = low-signal, deprioritize.
|
|
120
|
+
'ALTER TABLE observations ADD COLUMN injection_count INTEGER NOT NULL DEFAULT 0',
|
|
121
|
+
'ALTER TABLE observations ADD COLUMN last_injected_at INTEGER DEFAULT NULL',
|
|
115
122
|
];
|
|
116
123
|
|
|
117
124
|
/**
|
package/scoring-sql.mjs
CHANGED
|
@@ -61,6 +61,44 @@ export const TYPE_QUALITY_CASE = `(
|
|
|
61
61
|
END
|
|
62
62
|
)`;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Noise-ratio penalty: deprioritizes observations that get auto-injected often
|
|
66
|
+
* but rarely "used" (cited via Stop-hook citation tracker, or explicitly
|
|
67
|
+
* recalled/opened via pre-tool-recall / cmdRecall / cmdGet / cmdTimeline).
|
|
68
|
+
*
|
|
69
|
+
* Signal sources:
|
|
70
|
+
* - injection_count: bumped ONLY on UserPromptSubmit / hook-memory auto-inject
|
|
71
|
+
* - access_count: bumped on citation (c039352 P4), explicit recall, get, timeline
|
|
72
|
+
*
|
|
73
|
+
* Empirical thresholds (see docs/p0-injection-noise-baseline.txt, 53 transcripts):
|
|
74
|
+
* • High-noise legitimate use (#5597 29/10=2.9x): kept at 1.0× (below tier-1)
|
|
75
|
+
* • Moderate noise (#4352 44/9=4.89x): drops to 0.5× (tier-1 hit)
|
|
76
|
+
* • Pure noise (#4046 14/0=inf): drops to 0.5× (tier-1; count≥10 gate protects
|
|
77
|
+
* cold-start obs with legitimately no cites yet)
|
|
78
|
+
* • Entrenched noise (≥20 inject, ≥5× ratio): drops to 0.2× (tier-2)
|
|
79
|
+
*
|
|
80
|
+
* Applied as: BM25 × time_decay × TYPE_QUALITY × (0.5 + 0.5·importance) × NOISE_PENALTY
|
|
81
|
+
* Note: multiplicative so ORDER BY relevance ASC (negative scores) still works —
|
|
82
|
+
* penalty shrinks magnitude, making the row less preferable.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} [alias='o'] Table alias for the observations row.
|
|
85
|
+
* @returns {string} SQL CASE expression (already parenthesized).
|
|
86
|
+
*/
|
|
87
|
+
export function noisePenaltyClause(alias = 'o') {
|
|
88
|
+
const a = alias ? `${alias}.` : '';
|
|
89
|
+
return `(
|
|
90
|
+
CASE
|
|
91
|
+
WHEN COALESCE(${a}injection_count, 0) >= 20
|
|
92
|
+
AND COALESCE(${a}injection_count, 0) > COALESCE(${a}access_count, 0) * 5
|
|
93
|
+
THEN 0.2
|
|
94
|
+
WHEN COALESCE(${a}injection_count, 0) >= 10
|
|
95
|
+
AND COALESCE(${a}injection_count, 0) > COALESCE(${a}access_count, 0) * 3
|
|
96
|
+
THEN 0.5
|
|
97
|
+
ELSE 1.0
|
|
98
|
+
END
|
|
99
|
+
)`;
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
/**
|
|
65
103
|
* SQL WHERE clause fragment excluding LOW_SIGNAL degraded titles — the fallback
|
|
66
104
|
* titles hook-llm.mjs writes when Haiku summarization is unavailable or skipped
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Lightweight: only imports schema.mjs and utils.mjs, no MCP SDK
|
|
5
5
|
|
|
6
6
|
import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
|
|
7
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause } from '../utils.mjs';
|
|
7
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause, noisePenaltyClause } from '../utils.mjs';
|
|
8
8
|
import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import Database from 'better-sqlite3';
|
|
@@ -87,12 +87,16 @@ function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
87
87
|
const now = Date.now();
|
|
88
88
|
// R1: notLowSignalTitleClause() excludes hook-llm degraded titles
|
|
89
89
|
// ("Modified X", "Worked on X", "Reviewed N files:", raw error logs).
|
|
90
|
+
// v26 P0: noise penalty shrinks relevance magnitude for obs with high
|
|
91
|
+
// inject:access ratio (auto-injected often, never cited/opened). See
|
|
92
|
+
// docs/p0-injection-noise-baseline.txt.
|
|
90
93
|
const sql = `
|
|
91
94
|
SELECT o.id, o.type, o.title, o.lesson_learned,
|
|
92
95
|
${OBS_BM25}
|
|
93
96
|
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${TYPE_DECAY_CASE}))
|
|
94
97
|
* ${TYPE_QUALITY_CASE}
|
|
95
|
-
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
98
|
+
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
99
|
+
* ${noisePenaltyClause('o')} as relevance
|
|
96
100
|
FROM observations_fts
|
|
97
101
|
JOIN observations o ON o.id = observations_fts.rowid
|
|
98
102
|
WHERE observations_fts MATCH ?
|
|
@@ -460,6 +464,22 @@ async function main() {
|
|
|
460
464
|
count: prevCount + 1,
|
|
461
465
|
}));
|
|
462
466
|
} catch {}
|
|
467
|
+
// v26 P0: bump injection_count for obs-based emits only (prompt-corpus
|
|
468
|
+
// rows have "P<id>" string IDs; skip those — they live in user_prompts).
|
|
469
|
+
// Per-row try/catch: observations_au trigger reinserts FTS on any UPDATE
|
|
470
|
+
// (project_non_obvious.md); an FTS corruption on one row must not abort
|
|
471
|
+
// counter bumps for other rows.
|
|
472
|
+
if (rows.length > 0) {
|
|
473
|
+
try {
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
const bumpStmt = db.prepare(
|
|
476
|
+
'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
|
|
477
|
+
);
|
|
478
|
+
for (const r of rows) {
|
|
479
|
+
try { bumpStmt.run(now, r.id); } catch {}
|
|
480
|
+
}
|
|
481
|
+
} catch {}
|
|
482
|
+
}
|
|
463
483
|
}
|
|
464
484
|
|
|
465
485
|
// ─── L1: Registry skill pointer (T4 v2.31) ──────────────────────────
|
package/utils.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { buildLowSignalRegex } from './lib/low-signal-patterns.mjs';
|
|
|
9
9
|
// ─── Re-exports from extracted modules ──────────────────────────────────────
|
|
10
10
|
// Backward compatibility: all consumers import from utils.mjs
|
|
11
11
|
|
|
12
|
-
export { DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, OBS_FTS_COLUMNS, notLowSignalTitleClause } from './scoring-sql.mjs';
|
|
12
|
+
export { DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, OBS_FTS_COLUMNS, notLowSignalTitleClause, noisePenaltyClause } from './scoring-sql.mjs';
|
|
13
13
|
export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, extractCjkLikePatterns, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
|
|
14
14
|
export { resolveProject, _resetProjectCache } from './project-utils.mjs';
|
|
15
15
|
export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
|