claude-mem-lite 6.7.0 → 6.7.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.
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.7.
|
|
12
|
+
"version": "6.7.1",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.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/hook-context.mjs
CHANGED
|
@@ -580,7 +580,8 @@ export function buildSessionContextLines(
|
|
|
580
580
|
SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
|
|
581
581
|
WHERE o.project = ? AND ${liveObsFilterSql('o')}
|
|
582
582
|
AND COALESCE(o.importance, 1) >= 2
|
|
583
|
-
|
|
583
|
+
AND ${notLowSignalTitleClause('o')}
|
|
584
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC LIMIT ${KEY_CONTEXT_LIMIT}
|
|
584
585
|
`,
|
|
585
586
|
)
|
|
586
587
|
.all(project);
|
package/lib/file-edge-match.mjs
CHANGED
|
@@ -105,6 +105,114 @@ export function jsonArrayLikeNeedle(s) {
|
|
|
105
105
|
return likeLiteral(JSON.stringify(String(s ?? '')).slice(1, -1));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* How much a token LOOKS like a file path, from its shape alone. Higher wins.
|
|
110
|
+
*
|
|
111
|
+
* Only ever used to ORDER candidates, never to drop one — see rankFileCandidates.
|
|
112
|
+
* Every term below is a character-class property of the token, not a list of
|
|
113
|
+
* known-bad spellings: a blacklist is what the next unusual version string walks
|
|
114
|
+
* around, and this repo has thrown three hand-drawn classes away already.
|
|
115
|
+
*/
|
|
116
|
+
function fileShapeScore(token) {
|
|
117
|
+
const raw = String(token ?? '');
|
|
118
|
+
const name = basenameAnySep(raw);
|
|
119
|
+
const dot = name.lastIndexOf('.');
|
|
120
|
+
// `dot === 0` is a DOTFILE (`.env`, `.gitignore`, `.npmrc`), not an
|
|
121
|
+
// extensionless token — score `env` as the extension. The first cut rejected
|
|
122
|
+
// it with `dot <= 0`, which scored every dotfile 0, i.e. BELOW the version
|
|
123
|
+
// numbers this function exists to demote. Not theoretical: `extractFiles`
|
|
124
|
+
// emits `src/.env`, and six version tokens then evicted it from a window the
|
|
125
|
+
// pre-ranking code reached (pre-ship review, 2026-09-11).
|
|
126
|
+
if (dot < 0 || dot === name.length - 1) return 0;
|
|
127
|
+
const ext = name.slice(dot + 1);
|
|
128
|
+
let score = 0;
|
|
129
|
+
// A real extension starts with a LETTER. Version numbers ('v4.0.1' -> '1'),
|
|
130
|
+
// decimals and timestamp fragments ('…39.602Z' -> '602Z') do not — and those
|
|
131
|
+
// three are what `extractFiles`' regex actually emits on this corpus.
|
|
132
|
+
if (/^[A-Za-z]/.test(ext)) score += 2;
|
|
133
|
+
// Real extensions are short. A member expression borrows the letter-initial
|
|
134
|
+
// shape but not the length ('JSON.stringify', 'e.target_id').
|
|
135
|
+
if (ext.length <= 5) score += 1;
|
|
136
|
+
// A separator is the strongest evidence available without touching disk.
|
|
137
|
+
// Ask the token, and strip TRAILING separators first: comparing against the
|
|
138
|
+
// basename says yes for `foo.mjs/` too, because basenameAnySep strips those,
|
|
139
|
+
// so a token with no internal separator collected the bonus.
|
|
140
|
+
if (/[/\\]/.test(raw.replace(/[/\\]+$/, ''))) score += 2;
|
|
141
|
+
return score;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Order file candidates most-path-like first and drop exact repeats, for the
|
|
146
|
+
* callers that can only afford to probe the first few.
|
|
147
|
+
*
|
|
148
|
+
* `searchByFile` runs one prepared-statement execution per candidate on the
|
|
149
|
+
* UserPromptSubmit hot path, so it caps at FILE_PROBE_CAP — and it used to take
|
|
150
|
+
* the ones `extractFiles` happened to match FIRST, which is text order.
|
|
151
|
+
*
|
|
152
|
+
* Measured in one run over 216 live user_prompts (2026-09-11), denominator = the
|
|
153
|
+
* 50 prompts naming at least one file the corpus can reach, each lever isolated
|
|
154
|
+
* at the cap that ships:
|
|
155
|
+
*
|
|
156
|
+
* all reachable lost >=1 lost
|
|
157
|
+
* text order, cap 3 (pre-fix) 14 (28.0%) 34 (68.0%)
|
|
158
|
+
* text order, cap 6 7 (14.0%) 30 (60.0%)
|
|
159
|
+
* ranked, cap 6 (shipped) 2 ( 4.0%) 25 (50.0%)
|
|
160
|
+
*
|
|
161
|
+
* Neither lever alone gets there, and at cap 6 the ORDERING is the larger of
|
|
162
|
+
* the two. An earlier revision quoted "ranking alone 28.0% -> 24.0%", which was
|
|
163
|
+
* measured at cap 3 with a scorer that ranked dotfiles last: the wrong ablation
|
|
164
|
+
* on a window too small for ordering to matter.
|
|
165
|
+
*
|
|
166
|
+
* Oracle: a candidate counts as reachable when its basename matches one in
|
|
167
|
+
* `observation_files`. That is a strict SUPERSET of what the shipped query can
|
|
168
|
+
* return — it ignores the project, lookback, importance and low-signal
|
|
169
|
+
* predicates — measured at 26 of 152 candidates over-counted, with no false
|
|
170
|
+
* negatives (pre-ship review). Applied identically to every arm, so the
|
|
171
|
+
* direction holds; the exact percentages are not properties of the shipped
|
|
172
|
+
* query, and "flat beyond 6" is true of this oracle only (under the strict one
|
|
173
|
+
* the reviewer measured, the curve drops again at 10).
|
|
174
|
+
*
|
|
175
|
+
* The noise was never buying precision either: only 1 prompt reached
|
|
176
|
+
* `hasExplicitSignal` via extractFiles alone — that figure is from the audit's
|
|
177
|
+
* separate 533-prompt transcript corpus, NOT from the 216 rows above.
|
|
178
|
+
*
|
|
179
|
+
* SORT, not filter, and that is the safety property — with one caveat the
|
|
180
|
+
* caller owns: downstream of a `.slice(cap)` a position IS a candidate, so a
|
|
181
|
+
* wrong score can still evict. That is why fileShapeScore leans on structural
|
|
182
|
+
* properties and never on a list of known-bad spellings.
|
|
183
|
+
*
|
|
184
|
+
* Dedup folds ASCII A-Z ONLY, because that is the alphabet the SQL folds:
|
|
185
|
+
* SQLite's `COLLATE NOCASE` and `LIKE` are ASCII-case-insensitive (this file's
|
|
186
|
+
* own header, arm 1/2). JS `toLowerCase()` folds the whole Unicode table, so it
|
|
187
|
+
* collapsed `Ä.mjs` and `ä.mjs` into one probe while SQLite returns distinct
|
|
188
|
+
* rows for each — a DROP, which this function contractually never does.
|
|
189
|
+
* Unreachable through today's only caller (`extractFiles`' class is `[\w./-]`
|
|
190
|
+
* and `\w` is ASCII without the `u` flag), fixed because the exported contract
|
|
191
|
+
* is what the next caller reads.
|
|
192
|
+
*
|
|
193
|
+
* The index tiebreak keeps text order inside a tier explicitly rather than
|
|
194
|
+
* leaning on sort stability.
|
|
195
|
+
*/
|
|
196
|
+
export function rankFileCandidates(files) {
|
|
197
|
+
// A non-array would be iterated by character (`'a.mjs'` -> five candidates)
|
|
198
|
+
// or throw. One caller exists and it passes an array; fail closed for the next.
|
|
199
|
+
if (!Array.isArray(files)) return [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
const uniq = [];
|
|
202
|
+
for (const f of files) {
|
|
203
|
+
const s = String(f ?? '');
|
|
204
|
+
if (!s) continue;
|
|
205
|
+
const key = s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
206
|
+
if (seen.has(key)) continue;
|
|
207
|
+
seen.add(key);
|
|
208
|
+
uniq.push(s);
|
|
209
|
+
}
|
|
210
|
+
return uniq
|
|
211
|
+
.map((f, i) => ({ f, i, score: fileShapeScore(f) }))
|
|
212
|
+
.sort((a, b) => b.score - a.score || a.i - b.i)
|
|
213
|
+
.map((x) => x.f);
|
|
214
|
+
}
|
|
215
|
+
|
|
108
216
|
/** Bind values for fileMatchClause, in placeholder order. */
|
|
109
217
|
export function fileMatchParams(filePath) {
|
|
110
218
|
const fname = basenameAnySep(filePath);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.7.
|
|
9
|
+
"version": "6.7.1",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.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",
|
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
import { readHookStdin } from '../lib/hook-stdin.mjs';
|
|
19
19
|
import { resolveRuntimeDir } from '../lib/resolve-data-dir.mjs';
|
|
20
20
|
import { liveObsFilterSql, injectionRelevanceSql } from '../lib/inject-search-core.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
fileMatchClause,
|
|
23
|
+
fileMatchParams,
|
|
24
|
+
basenameAnySep,
|
|
25
|
+
rankFileCandidates,
|
|
26
|
+
} from '../lib/file-edge-match.mjs';
|
|
22
27
|
import { cjkPrecisionOk } from '../nlp.mjs';
|
|
23
28
|
import { upsFtsQuery } from '../lib/ups-query.mjs';
|
|
24
29
|
import { corpusFloorScale } from '../lib/relevance-floor.mjs';
|
|
@@ -465,6 +470,41 @@ export function searchByFts(
|
|
|
465
470
|
return { rows, mode };
|
|
466
471
|
}
|
|
467
472
|
|
|
473
|
+
/**
|
|
474
|
+
* How many candidates the file leg probes, one prepared-statement execution each.
|
|
475
|
+
*
|
|
476
|
+
* Was 3, chosen when the leg was written and never measured.
|
|
477
|
+
*
|
|
478
|
+
* R12 B-4 named both halves of the mechanism in its own title —
|
|
479
|
+
* `files.slice(0, 3)` AND `extractFiles` text order — but its prescribed REMEDY
|
|
480
|
+
* is ordering only, and ordering alone does not get there. Measured over 216
|
|
481
|
+
* live user_prompts (2026-09-11), denominator 50 (prompts naming >=1 reachable
|
|
482
|
+
* file), all-reachable-lost, one run, each lever isolated:
|
|
483
|
+
*
|
|
484
|
+
* text order, cap 3 (pre-fix) 28.0% text order, cap 6 14.0%
|
|
485
|
+
* ranked, cap 6 (shipped) 4.0%
|
|
486
|
+
*
|
|
487
|
+
* Decomposing the residue at cap 3 showed why ordering alone stalls: all 12
|
|
488
|
+
* still-harmed prompts were blocked by other file-SHAPED candidates and NONE by
|
|
489
|
+
* noise, and the 50-prompt denominator names a median of 3 distinct reachable
|
|
490
|
+
* files (mean 3.04) — more than the window held. Sweeping the cap, ranked arm:
|
|
491
|
+
*
|
|
492
|
+
* cap 3 -> 28.0% 4 -> 20.0% 5 -> 6.0% 6 -> 4.0% 8/10/12 -> 4.0%
|
|
493
|
+
*
|
|
494
|
+
* Six is the knee under this oracle (see rankFileCandidates for what the oracle
|
|
495
|
+
* over-counts, and why flatness past 6 is oracle-dependent). The residual two
|
|
496
|
+
* prompts name their shallowest reachable candidate 23 and 13 deep.
|
|
497
|
+
*
|
|
498
|
+
* Cost is linear in probes, and the ceiling is not the expectation: only 20.0%
|
|
499
|
+
* of candidate-bearing prompts have more than 3 unique candidates, so the mean
|
|
500
|
+
* is 0.56 extra probes per UserPromptSubmit (~31µs), against a ceiling of 3
|
|
501
|
+
* (+0.156ms measured on the live 41-observation store). On a synthetic
|
|
502
|
+
* 3747-observation store per-probe cost ranged 50-489µs, so the worst case is
|
|
503
|
+
* ~+1.5ms; that spread is an artifact of how the fixture was generated and is
|
|
504
|
+
* quoted as a bound, not as a property of any real corpus.
|
|
505
|
+
*/
|
|
506
|
+
const FILE_PROBE_CAP = 6;
|
|
507
|
+
|
|
468
508
|
function searchByFile(db, files, project, limit) {
|
|
469
509
|
if (files.length === 0) return [];
|
|
470
510
|
|
|
@@ -484,11 +524,16 @@ function searchByFile(db, files, project, limit) {
|
|
|
484
524
|
AND o.created_at_epoch > ?
|
|
485
525
|
AND ${fileMatchClause('of2')}
|
|
486
526
|
AND ${notLowSignalTitleClause('o')}
|
|
487
|
-
ORDER BY o.created_at_epoch DESC
|
|
527
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
488
528
|
LIMIT ?
|
|
489
529
|
`);
|
|
490
530
|
|
|
491
|
-
|
|
531
|
+
// Rank before capping (R12 B-4). `files` arrives in the order `extractFiles`
|
|
532
|
+
// matched it, which is TEXT order — so three version tokens ahead of the file
|
|
533
|
+
// the prompt is about evicted it from this window entirely. rankFileCandidates
|
|
534
|
+
// reorders and de-duplicates; it never drops, so the cap still sees every
|
|
535
|
+
// candidate it used to, just best-first.
|
|
536
|
+
for (const file of rankFileCandidates(files).slice(0, FILE_PROBE_CAP)) {
|
|
492
537
|
// Shared predicate (pre-tag review of v3.76.2, SF-1/S2). This leg used
|
|
493
538
|
// `file.split('/').pop()` — weaker than node:path `basename`, since it misses '\'
|
|
494
539
|
// even ON a Windows host — plus a bare `%<basename>` suffix LIKE with no path
|