claude-mem-lite 6.13.1 → 6.13.3
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/README.md +2 -1
- package/README.zh-CN.md +1 -1
- package/bash-utils.mjs +349 -12
- package/hook-handoff.mjs +32 -12
- package/lib/citation-tracker.mjs +18 -4
- package/mem-cli.mjs +4 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.13.
|
|
12
|
+
"version": "6.13.3",
|
|
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.13.
|
|
3
|
+
"version": "6.13.3",
|
|
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/README.md
CHANGED
|
@@ -249,7 +249,8 @@ the database, so reverting is pinning `claude-mem-lite@6.12.2`.
|
|
|
249
249
|
|
|
250
250
|
**Citation readings drop at this version, and that is a measurement change.** A lesson the
|
|
251
251
|
agent answers with `#NN n/a` is no longer counted as cited, so `citation-stats` per-face rates
|
|
252
|
-
read lower from here on
|
|
252
|
+
read lower from here on (`--sidechain` excepted: it counts an `n/a` as an answer). Do not compare
|
|
253
|
+
a reading taken before 6.13.0 with one taken after.
|
|
253
254
|
|
|
254
255
|
## Upgrading to 6.11.0
|
|
255
256
|
|
package/README.zh-CN.md
CHANGED
|
@@ -208,7 +208,7 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
208
208
|
仍能打开数据库,回退就是固定到 `claude-mem-lite@6.12.2`。
|
|
209
209
|
|
|
210
210
|
**引用率读数会从这个版本起下降,这是口径变化。** agent 用 `#NN n/a` 回应的 lesson 不再计为引用,
|
|
211
|
-
所以 `citation-stats`
|
|
211
|
+
所以 `citation-stats` 各注入面的引用率从此偏低(`--sidechain` 除外:它把 `n/a` 算作已回应)。不要拿 6.13.0 之前和之后的读数直接比较。
|
|
212
212
|
|
|
213
213
|
## 升级到 6.11.0
|
|
214
214
|
|
package/bash-utils.mjs
CHANGED
|
@@ -62,6 +62,7 @@ const GIT_READ_SUBCMDS = new Set([
|
|
|
62
62
|
'shortlog',
|
|
63
63
|
'reflog',
|
|
64
64
|
'status',
|
|
65
|
+
'rev-parse',
|
|
65
66
|
]);
|
|
66
67
|
|
|
67
68
|
// Hard failure fingerprints — a real crash / thrown exception / non-zero-exit marker,
|
|
@@ -92,32 +93,320 @@ const NEUTRAL_VERBS = new Set([
|
|
|
92
93
|
'export',
|
|
93
94
|
'set',
|
|
94
95
|
'exit',
|
|
96
|
+
// Print a path or a date; used inside `$(…)` to build a read's arguments.
|
|
97
|
+
'pwd',
|
|
98
|
+
'date',
|
|
99
|
+
'basename',
|
|
100
|
+
'dirname',
|
|
101
|
+
'realpath',
|
|
102
|
+
'readlink',
|
|
95
103
|
]);
|
|
96
104
|
|
|
105
|
+
// code-graph-mcp subcommands that only query the index (plus `snapshot inspect`, handled in
|
|
106
|
+
// WRITES_OR_RUNS). The rest (serve, the index rebuilds, doctor's repairs, benchmark,
|
|
107
|
+
// adopt / unadopt / uninstall, snapshot create) run work whose failures error-recall should see.
|
|
108
|
+
const CODE_GRAPH_READ_SUBCMDS = new Set([
|
|
109
|
+
'grep',
|
|
110
|
+
'search',
|
|
111
|
+
'ast-search',
|
|
112
|
+
'callgraph',
|
|
113
|
+
'impact',
|
|
114
|
+
'affected',
|
|
115
|
+
'show',
|
|
116
|
+
'map',
|
|
117
|
+
'tour',
|
|
118
|
+
'overview',
|
|
119
|
+
'deps',
|
|
120
|
+
'trace',
|
|
121
|
+
'similar',
|
|
122
|
+
'refs',
|
|
123
|
+
'dead-code',
|
|
124
|
+
'centrality',
|
|
125
|
+
'cycles',
|
|
126
|
+
'surprising',
|
|
127
|
+
'report',
|
|
128
|
+
'health-check',
|
|
129
|
+
'stats',
|
|
130
|
+
'outcome',
|
|
131
|
+
'help',
|
|
132
|
+
'--help',
|
|
133
|
+
'--version',
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Forms of a SEARCH_VERBS verb that write a file or run a program, so their output is not
|
|
137
|
+
// file content (v6.13.0 defect review P3-5). Args are the whitespace tokens after the verb;
|
|
138
|
+
// quotes are not stripped, which only matters for a flag written inside quotes.
|
|
139
|
+
const WRITES_OR_RUNS = {
|
|
140
|
+
sed: (args) => args.some((a) => /^-[A-Za-z]*i/.test(a) || a.startsWith('--in-place')),
|
|
141
|
+
sort: (args) => args.some((a) => /^-[A-Za-z]*o/.test(a) || a.startsWith('--output')),
|
|
142
|
+
// `-f` reads the program from a file (or a heredoc on stdin) this check cannot see.
|
|
143
|
+
awk: (args, text) =>
|
|
144
|
+
args.some((a) => /^-f/.test(a) || a.startsWith('--file')) ||
|
|
145
|
+
/\bsystem\s*\(|\|\s*(?:getline\b|")|\|&/.test(text),
|
|
146
|
+
find: (args) => args.some((a) => /^-(?:exec|execdir|ok|okdir|delete|fprint0?|fprintf|fls)$/.test(a)),
|
|
147
|
+
'code-graph-mcp': (args) =>
|
|
148
|
+
!CODE_GRAPH_READ_SUBCMDS.has(args[0]) && !(args[0] === 'snapshot' && args[1] === 'inspect'),
|
|
149
|
+
};
|
|
150
|
+
|
|
97
151
|
/** 'read' | 'neutral' | 'other' for one simple command (one element of a pipeline). */
|
|
98
152
|
function classifySimpleCommand(text) {
|
|
99
|
-
const toks = text
|
|
153
|
+
const toks = shellWords(text);
|
|
100
154
|
let i = 0;
|
|
101
155
|
while (i < toks.length && (/^\w+=/.test(toks[i]) || CMD_WRAPPERS.has(toks[i]))) i++;
|
|
102
156
|
const first = toks[i];
|
|
103
157
|
if (!first || NEUTRAL_VERBS.has(first)) return 'neutral';
|
|
104
|
-
if (SEARCH_VERBS.has(first)) return 'read';
|
|
158
|
+
if (SEARCH_VERBS.has(first)) return WRITES_OR_RUNS[first]?.(toks.slice(i + 1), text) ? 'other' : 'read';
|
|
105
159
|
return first === 'git' && GIT_READ_SUBCMDS.has(toks[i + 1]) ? 'read' : 'other';
|
|
106
160
|
}
|
|
107
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Split one simple command into words on whitespace OUTSIDE quotes, so `x="a b" grep …`
|
|
164
|
+
* stays an assignment followed by grep (v6.13.2 delta review FALSE-2). Quotes are kept in
|
|
165
|
+
* the words; only the split point is quote-aware.
|
|
166
|
+
*/
|
|
167
|
+
function shellWords(text) {
|
|
168
|
+
const words = [];
|
|
169
|
+
let cur = '';
|
|
170
|
+
let inWord = false;
|
|
171
|
+
let quote = null;
|
|
172
|
+
for (let k = 0; k < text.length; k++) {
|
|
173
|
+
const ch = text[k];
|
|
174
|
+
if (quote) {
|
|
175
|
+
if (ch === '\\' && quote !== "'") {
|
|
176
|
+
cur += ch + (text[k + 1] ?? '');
|
|
177
|
+
k++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === quote[quote.length - 1]) quote = null;
|
|
181
|
+
cur += ch;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (/\s/.test(ch)) {
|
|
185
|
+
if (inWord) words.push(cur);
|
|
186
|
+
cur = '';
|
|
187
|
+
inWord = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
inWord = true;
|
|
191
|
+
if (ch === '\\') {
|
|
192
|
+
cur += ch + (text[k + 1] ?? '');
|
|
193
|
+
k++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === '$' && text[k + 1] === "'") {
|
|
197
|
+
quote = ANSI_QUOTE;
|
|
198
|
+
cur += "$'";
|
|
199
|
+
k++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
203
|
+
cur += ch;
|
|
204
|
+
}
|
|
205
|
+
if (inWord) words.push(cur);
|
|
206
|
+
return words;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// `$'…'` (ANSI-C quoting): a backslash escapes the next character, including `'`.
|
|
210
|
+
const ANSI_QUOTE = "$'";
|
|
211
|
+
// Stands in for a cut-out substitution. No surrounding spaces: `f=$(…)` must stay one
|
|
212
|
+
// assignment word, or the placeholder becomes the verb.
|
|
213
|
+
const SUBST = '__SUBST__';
|
|
214
|
+
const MAX_SUBST_DEPTH = 32;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove what the shell does not run as a command: heredoc bodies (they are stdin) and
|
|
218
|
+
* `#` comments. Quote-aware. An apostrophe in either used to unbalance the quotes and send
|
|
219
|
+
* the whole line to a first-word fallback (v6.13.0 defect review P3-4).
|
|
220
|
+
*
|
|
221
|
+
* An UNQUOTED delimiter (`<<EOF`) is the exception: bash expands `$(…)` and backticks in
|
|
222
|
+
* that body, so each body line is kept as `: "<line>"`, a no-op whose substitutions
|
|
223
|
+
* splitStatements still cuts out and judges. `$((…))` and `((…))` are copied whole so a
|
|
224
|
+
* `<<` shift inside them is not read as a heredoc (v6.13.2 pre-ship defect review).
|
|
225
|
+
*/
|
|
226
|
+
function stripNonCommands(cmd) {
|
|
227
|
+
let out = '';
|
|
228
|
+
let quote = null;
|
|
229
|
+
// Set once a `((` never closes: every later attempt would rescan to the end of the input,
|
|
230
|
+
// which made unclosed parentheses quadratic (v6.13.2 delta review P3-1).
|
|
231
|
+
let arithUnclosed = false;
|
|
232
|
+
const pending = []; // heredoc delimiters opened on the current line: { word, dash, expand }
|
|
233
|
+
for (let k = 0; k < cmd.length; k++) {
|
|
234
|
+
const ch = cmd[k];
|
|
235
|
+
if (quote === ANSI_QUOTE) {
|
|
236
|
+
if (ch === '\\') {
|
|
237
|
+
out += ch + (cmd[k + 1] ?? '');
|
|
238
|
+
k++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (ch === "'") quote = null;
|
|
242
|
+
out += ch;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (quote) {
|
|
246
|
+
// Keep the backslash AND the character it escapes (the old `out += cmd[k++]; out += ch`
|
|
247
|
+
// wrote the backslash twice and dropped the escaped character).
|
|
248
|
+
if (ch === '\\' && quote === '"') {
|
|
249
|
+
out += ch + (cmd[k + 1] ?? '');
|
|
250
|
+
k++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ch === quote) quote = null;
|
|
254
|
+
out += ch;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (ch === '\\') {
|
|
258
|
+
out += ch + (cmd[k + 1] ?? '');
|
|
259
|
+
k++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (ch === '$' && cmd[k + 1] === "'") {
|
|
263
|
+
quote = ANSI_QUOTE;
|
|
264
|
+
out += "$'";
|
|
265
|
+
k++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (ch === "'" || ch === '"') {
|
|
269
|
+
quote = ch;
|
|
270
|
+
out += ch;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const opensArith =
|
|
274
|
+
(ch === '$' && cmd[k + 1] === '(' && cmd[k + 2] === '(') ||
|
|
275
|
+
(ch === '(' && cmd[k + 1] === '(' && cmd[k - 1] !== '$');
|
|
276
|
+
if (opensArith && !arithUnclosed) {
|
|
277
|
+
const end = arithmeticEnd(cmd, ch === '$' ? k + 1 : k);
|
|
278
|
+
if (end >= 0) {
|
|
279
|
+
out += cmd.slice(k, end);
|
|
280
|
+
k = end - 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (end === ARITH_UNCLOSED) arithUnclosed = true;
|
|
284
|
+
}
|
|
285
|
+
if (ch === '#' && (k === 0 || /[\s;&|()]/.test(cmd[k - 1]))) {
|
|
286
|
+
while (k + 1 < cmd.length && cmd[k + 1] !== '\n') k++;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === '<' && cmd[k + 1] === '<' && cmd[k + 2] !== '<' && cmd[k - 1] !== '<') {
|
|
290
|
+
const m = /^<<(-?)[ \t]*((?:'[^'\n]*'|"[^"\n]*"|\\.|[^\s;&|<>()'"\\])+)/.exec(cmd.slice(k));
|
|
291
|
+
if (m) {
|
|
292
|
+
pending.push({
|
|
293
|
+
word: m[2].replace(/\\(.)/g, '$1').replace(/['"]/g, ''),
|
|
294
|
+
dash: m[1] === '-',
|
|
295
|
+
expand: !/['"\\]/.test(m[2]),
|
|
296
|
+
});
|
|
297
|
+
out += m[0];
|
|
298
|
+
k += m[0].length - 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (ch === '\n' && pending.length > 0) {
|
|
303
|
+
out += ch;
|
|
304
|
+
// Consume each body in order, up to and including its delimiter line.
|
|
305
|
+
let pos = k + 1;
|
|
306
|
+
for (const { word, dash, expand } of pending.splice(0)) {
|
|
307
|
+
while (pos < cmd.length) {
|
|
308
|
+
let end = cmd.indexOf('\n', pos);
|
|
309
|
+
if (end === -1) end = cmd.length;
|
|
310
|
+
const line = cmd.slice(pos, end);
|
|
311
|
+
pos = end + 1;
|
|
312
|
+
if ((dash ? line.replace(/^\t+/, '') : line) === word) break;
|
|
313
|
+
if (expand) {
|
|
314
|
+
const esc = line.replace(/\\(?=")/g, '\\\\').replace(/"/g, '\\"');
|
|
315
|
+
// A trailing backslash would escape the wrapper's closing quote.
|
|
316
|
+
const odd = /\\*$/.exec(esc)[0].length % 2 === 1;
|
|
317
|
+
out += `: "${esc}${odd ? '\\' : ''}"\n`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
k = pos - 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
out += ch;
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const ARITH_UNCLOSED = -2;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* For `((` whose first `(` is at `open` (after the `$` of `$((`, or a bare `((`): the index
|
|
333
|
+
* just past the closing `))` when bash reads it as arithmetic, -1 when it is a command form
|
|
334
|
+
* instead, ARITH_UNCLOSED when the inner `(` never closes. bash tries arithmetic first and
|
|
335
|
+
* falls back when the inner `(` does not close on `))`, so `$((npm test) | head)` is a
|
|
336
|
+
* command substitution holding a subshell (v6.13.2 delta review P3-3).
|
|
337
|
+
*/
|
|
338
|
+
function arithmeticEnd(cmd, open) {
|
|
339
|
+
const inner = closingParen(cmd, open + 2);
|
|
340
|
+
if (inner === -1) return ARITH_UNCLOSED;
|
|
341
|
+
return cmd[inner] === ')' ? inner + 1 : -1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Index just past the `)` that closes a substitution whose body starts at `start`, or -1
|
|
346
|
+
* when it never closes. Quote-aware; nested parentheses count.
|
|
347
|
+
*/
|
|
348
|
+
function closingParen(cmd, start) {
|
|
349
|
+
let depth = 1;
|
|
350
|
+
let quote = null;
|
|
351
|
+
for (let k = start; k < cmd.length; k++) {
|
|
352
|
+
const ch = cmd[k];
|
|
353
|
+
if (quote) {
|
|
354
|
+
if (ch === '\\' && quote !== "'") k++;
|
|
355
|
+
else if (ch === quote[quote.length - 1]) quote = null;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (ch === '\\') k++;
|
|
359
|
+
else if (ch === '$' && cmd[k + 1] === "'") {
|
|
360
|
+
quote = ANSI_QUOTE;
|
|
361
|
+
k++;
|
|
362
|
+
} else if (ch === "'" || ch === '"') quote = ch;
|
|
363
|
+
else if (ch === '(') depth++;
|
|
364
|
+
else if (ch === ')' && --depth === 0) return k + 1;
|
|
365
|
+
}
|
|
366
|
+
return -1;
|
|
367
|
+
}
|
|
368
|
+
|
|
108
369
|
/**
|
|
109
370
|
* Split a command line into statements (on `;`, newline, `&&`, `||`, `&`), each a list
|
|
110
371
|
* of its pipeline elements (on `|` and `|&`). Quote-aware, so the `;` in
|
|
111
372
|
* `grep -E "a;b"` separates nothing, and the `&` of a redirection (`2>&1`, `&>f`) is not
|
|
112
|
-
* a statement break. A backslash-newline continues the line.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
373
|
+
* a statement break. A backslash-newline continues the line. The bodies of `$(…)`,
|
|
374
|
+
* backticks, `<(…)` and `>(…)` — outside quotes or inside double quotes, where they run
|
|
375
|
+
* all the same — are cut out into `subs` for the caller to judge on their own. Returns
|
|
376
|
+
* null when the quotes or a substitution do not close.
|
|
115
377
|
*/
|
|
116
378
|
function splitStatements(cmd) {
|
|
117
379
|
const statements = [];
|
|
380
|
+
const subs = [];
|
|
118
381
|
let pipeline = [];
|
|
119
382
|
let cur = '';
|
|
120
383
|
let quote = null;
|
|
384
|
+
// Cut a substitution body out at `k` (the index of `$`, `<`, `>` or the backtick);
|
|
385
|
+
// returns the index of its last character, or -1 when it never closes.
|
|
386
|
+
const takeSub = (k) => {
|
|
387
|
+
if (cmd[k] === '`') {
|
|
388
|
+
const end = cmd.indexOf('`', k + 1);
|
|
389
|
+
if (end === -1) return -1;
|
|
390
|
+
subs.push(cmd.slice(k + 1, end));
|
|
391
|
+
cur += SUBST;
|
|
392
|
+
return end;
|
|
393
|
+
}
|
|
394
|
+
if (cmd[k] === '$' && cmd[k + 2] === '(') {
|
|
395
|
+
const arithEnd = arithmeticEnd(cmd, k + 1);
|
|
396
|
+
if (arithEnd >= 0) {
|
|
397
|
+
// An arithmetic body is not a command, but a substitution inside it still runs.
|
|
398
|
+
const body = cmd.slice(k + 3, arithEnd - 2);
|
|
399
|
+
if (/\$\(|`/.test(body)) subs.push(`: ${body}`);
|
|
400
|
+
cur += SUBST;
|
|
401
|
+
return arithEnd - 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const end = closingParen(cmd, k + 2);
|
|
405
|
+
if (end === -1) return -1;
|
|
406
|
+
subs.push(cmd.slice(k + 2, end - 1));
|
|
407
|
+
cur += SUBST;
|
|
408
|
+
return end - 1;
|
|
409
|
+
};
|
|
121
410
|
const endElement = () => {
|
|
122
411
|
pipeline.push(cur);
|
|
123
412
|
cur = '';
|
|
@@ -129,9 +418,29 @@ function splitStatements(cmd) {
|
|
|
129
418
|
};
|
|
130
419
|
for (let k = 0; k < cmd.length; k++) {
|
|
131
420
|
const ch = cmd[k];
|
|
421
|
+
const opensSub = ch === '`' || ((ch === '$' || ch === '<' || ch === '>') && cmd[k + 1] === '(');
|
|
422
|
+
if (quote === ANSI_QUOTE) {
|
|
423
|
+
if (ch === '\\') {
|
|
424
|
+
cur += ch + (cmd[k + 1] ?? '');
|
|
425
|
+
k++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (ch === "'") quote = null;
|
|
429
|
+
cur += ch;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
132
432
|
if (quote) {
|
|
433
|
+
if (quote === '"' && (ch === '`' || (ch === '$' && cmd[k + 1] === '('))) {
|
|
434
|
+
k = takeSub(k);
|
|
435
|
+
if (k === -1) return null;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (ch === '\\' && quote === '"') {
|
|
439
|
+
cur += ch + (cmd[k + 1] ?? '');
|
|
440
|
+
k++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
133
443
|
if (ch === quote) quote = null;
|
|
134
|
-
else if (ch === '\\' && quote === '"') cur += cmd[k++];
|
|
135
444
|
cur += ch;
|
|
136
445
|
continue;
|
|
137
446
|
}
|
|
@@ -140,11 +449,22 @@ function splitStatements(cmd) {
|
|
|
140
449
|
k++;
|
|
141
450
|
continue;
|
|
142
451
|
}
|
|
452
|
+
if (ch === '$' && cmd[k + 1] === "'") {
|
|
453
|
+
quote = ANSI_QUOTE;
|
|
454
|
+
cur += "$'";
|
|
455
|
+
k++;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
143
458
|
if (ch === "'" || ch === '"') {
|
|
144
459
|
quote = ch;
|
|
145
460
|
cur += ch;
|
|
146
461
|
continue;
|
|
147
462
|
}
|
|
463
|
+
if (opensSub) {
|
|
464
|
+
k = takeSub(k);
|
|
465
|
+
if (k === -1) return null;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
148
468
|
const next = cmd[k + 1];
|
|
149
469
|
if (ch === '|' && next !== '|') {
|
|
150
470
|
endElement();
|
|
@@ -161,7 +481,7 @@ function splitStatements(cmd) {
|
|
|
161
481
|
}
|
|
162
482
|
if (quote) return null;
|
|
163
483
|
endStatement();
|
|
164
|
-
return statements;
|
|
484
|
+
return { statements, subs };
|
|
165
485
|
}
|
|
166
486
|
|
|
167
487
|
// True when the command only READS: every element of every pipeline is a read/search
|
|
@@ -171,18 +491,35 @@ function splitStatements(cmd) {
|
|
|
171
491
|
// `git grep`, `cat f | head` and `cd repo && sed -n 1,9p f` are exempt. Every statement
|
|
172
492
|
// and every pipe consumer is checked, so `grep x f; npm test | tail` and
|
|
173
493
|
// `printf '…' | node server.mjs` are not exempted on the strength of their first word.
|
|
494
|
+
// Substitution bodies are judged the same way, so `grep x $(npm test)` runs a program.
|
|
495
|
+
// A line that still does not parse once heredoc bodies and comments are gone is 'other':
|
|
496
|
+
// the old fallback judged it by its first word and silenced exactly the heredoc-then-run
|
|
497
|
+
// shape (v6.13.0 defect review P3-4).
|
|
174
498
|
function isReadOnlyCommand(cmd) {
|
|
175
|
-
|
|
176
|
-
|
|
499
|
+
return commandKind(stripNonCommands(cmd)) === 'read';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** 'read' | 'neutral' | 'other' for a whole command line (see isReadOnlyCommand). */
|
|
503
|
+
function commandKind(cmd, depth = 0) {
|
|
504
|
+
// Each level rescans its body; past this depth the line is judged 'other' rather than
|
|
505
|
+
// recursing into a RangeError on the hot path of every Bash event.
|
|
506
|
+
if (depth > MAX_SUBST_DEPTH) return 'other';
|
|
507
|
+
const parsed = splitStatements(cmd);
|
|
508
|
+
if (!parsed) return 'other';
|
|
177
509
|
let sawRead = false;
|
|
178
|
-
for (const
|
|
510
|
+
for (const body of parsed.subs) {
|
|
511
|
+
const kind = commandKind(body, depth + 1);
|
|
512
|
+
if (kind === 'other') return 'other';
|
|
513
|
+
if (kind === 'read') sawRead = true;
|
|
514
|
+
}
|
|
515
|
+
for (const pipeline of parsed.statements) {
|
|
179
516
|
for (const element of pipeline) {
|
|
180
517
|
const kind = classifySimpleCommand(element);
|
|
181
|
-
if (kind === 'other') return
|
|
518
|
+
if (kind === 'other') return 'other';
|
|
182
519
|
if (kind === 'read') sawRead = true;
|
|
183
520
|
}
|
|
184
521
|
}
|
|
185
|
-
return sawRead;
|
|
522
|
+
return sawRead ? 'read' : 'neutral';
|
|
186
523
|
}
|
|
187
524
|
|
|
188
525
|
// Paths excluded from observation capture (ephemeral / virtual filesystems) — applied
|
package/hook-handoff.mjs
CHANGED
|
@@ -165,7 +165,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
165
165
|
WHERE project = ? AND ${liveObsFilterSql('')}
|
|
166
166
|
AND COALESCE(importance, 1) >= 3
|
|
167
167
|
AND ${notLowSignalTitleClause('')}
|
|
168
|
-
ORDER BY created_at_epoch DESC LIMIT 1
|
|
168
|
+
ORDER BY created_at_epoch DESC, id DESC LIMIT 1
|
|
169
169
|
`,
|
|
170
170
|
)
|
|
171
171
|
.get(project);
|
|
@@ -248,7 +248,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
248
248
|
`
|
|
249
249
|
SELECT title, type, narrative FROM observations
|
|
250
250
|
WHERE (memory_session_id = ? OR project = ?) AND COALESCE(compressed_into, 0) = 0 ${obsWindowClause}
|
|
251
|
-
ORDER BY created_at_epoch DESC LIMIT 15
|
|
251
|
+
ORDER BY created_at_epoch DESC, id DESC LIMIT 15
|
|
252
252
|
`,
|
|
253
253
|
)
|
|
254
254
|
.all(sessionId, project, ...obsWindowParams);
|
|
@@ -348,21 +348,36 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
348
348
|
if (episodeSnapshot?.files) episodeSnapshot.files.filter(isValidFile).forEach((f) => fileSet.add(f));
|
|
349
349
|
// Same namespace widening as `completed` above — see the reasoning there. Measured
|
|
350
350
|
// 2026-09-21: 8 of the 17 live handoff rows stored key_files as the empty array.
|
|
351
|
-
|
|
351
|
+
//
|
|
352
|
+
// The cap counts rows that CONTRIBUTE a new file (D#67, the D#40 shape). This read was
|
|
353
|
+
// `LIMIT 10` → isValidFile, so rows the filter empties ('[]', directories, /tmp paths)
|
|
354
|
+
// could use up the window. A latent defect: the v6.13.2 pre-ship claims review replayed
|
|
355
|
+
// the live DB 2026-09-26 with this read's own WHERE and window, and 0 of 45 stored
|
|
356
|
+
// handoffs and 0 of 310 sessions lost a file (the largest window held 11 rows).
|
|
357
|
+
// Streaming keeps the filter exactly as it was.
|
|
358
|
+
let contributing = 0;
|
|
359
|
+
for (const row of db
|
|
352
360
|
.prepare(
|
|
353
361
|
`
|
|
354
362
|
SELECT files_modified FROM observations
|
|
355
363
|
WHERE (memory_session_id = ? OR project = ?) AND files_modified IS NOT NULL ${obsWindowClause}
|
|
356
|
-
ORDER BY created_at_epoch DESC
|
|
364
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
357
365
|
`,
|
|
358
366
|
)
|
|
359
|
-
.
|
|
360
|
-
|
|
367
|
+
.iterate(sessionId, project, ...obsWindowParams)) {
|
|
368
|
+
let files;
|
|
361
369
|
try {
|
|
362
|
-
JSON.parse(row.files_modified)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
370
|
+
files = JSON.parse(row.files_modified);
|
|
371
|
+
} catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const valid = Array.isArray(files) ? files.filter(isValidFile) : [];
|
|
375
|
+
const before = fileSet.size;
|
|
376
|
+
valid.forEach((f) => fileSet.add(f));
|
|
377
|
+
// A row that only repeats a listed file (the common case: one file edited again and
|
|
378
|
+
// again) contributes nothing either (v6.13.2 pre-ship defect review P3-7).
|
|
379
|
+
if (fileSet.size === before) continue;
|
|
380
|
+
if (++contributing === 10) break;
|
|
366
381
|
}
|
|
367
382
|
|
|
368
383
|
// 5. Key decisions — high importance observations (skip low-signal degraded titles).
|
|
@@ -784,6 +799,11 @@ export function pickHandoffToInject(db, project, currentCcSessionId = null) {
|
|
|
784
799
|
const now = Date.now();
|
|
785
800
|
// Fetch recent handoffs and find the most recent non-expired one.
|
|
786
801
|
// A newer but expired 'clear' handoff must not shadow a still-valid 'exit' handoff.
|
|
802
|
+
// No id tiebreaker on this or the Stage -1/0/2 reads above, on purpose: session_handoffs has
|
|
803
|
+
// no id column, and its rowid is not recency because the writer is an UPSERT that keeps the
|
|
804
|
+
// row's original rowid. On a tie `rowid DESC` would therefore pick the older write exactly
|
|
805
|
+
// when the lower-rowid row was written last, a shape only a rewrite produces. A correct
|
|
806
|
+
// tiebreak needs a column; see findings.md, the created_at_epoch tie bullet.
|
|
787
807
|
const handoffs = currentCcSessionId
|
|
788
808
|
? db
|
|
789
809
|
.prepare(
|
|
@@ -1017,7 +1037,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
1017
1037
|
`
|
|
1018
1038
|
SELECT completed, next_steps, remaining_items FROM session_summaries
|
|
1019
1039
|
WHERE memory_session_id = ? AND project = ?
|
|
1020
|
-
ORDER BY created_at_epoch DESC LIMIT 1
|
|
1040
|
+
ORDER BY created_at_epoch DESC, id DESC LIMIT 1
|
|
1021
1041
|
`,
|
|
1022
1042
|
)
|
|
1023
1043
|
.get(handoff.session_id, project);
|
|
@@ -1031,7 +1051,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
1031
1051
|
`
|
|
1032
1052
|
SELECT completed, next_steps, remaining_items FROM session_summaries
|
|
1033
1053
|
WHERE project = ?
|
|
1034
|
-
ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
|
|
1054
|
+
ORDER BY ABS(created_at_epoch - ?) ASC, id DESC LIMIT 1
|
|
1035
1055
|
`,
|
|
1036
1056
|
)
|
|
1037
1057
|
.get(project, handoff.created_at_epoch ?? 0);
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -116,10 +116,21 @@ const CITATION_RE = citationIdRe();
|
|
|
116
116
|
// a false one withholds credit from a lesson that was used, so the marker must follow
|
|
117
117
|
// the id directly: past the rest of an id list (`#137 / #260 / #54 n/a`, `#1758、E#3520
|
|
118
118
|
// 与本次无关`) and at most one short parenthetical gloss, never "somewhere nearby".
|
|
119
|
+
// The English adjectives must not be followed by a noun: "#12 irrelevant rows are now
|
|
120
|
+
// filtered" describes the change (defect review P3-6). Only a word from ADJ_VERDICT_NEXT may
|
|
121
|
+
// follow them. Over 1,900 `#NN` mentions (citationIdRe) in 218 top-level transcripts across
|
|
122
|
+
// every project (2026-09-26T06:46Z) this changed exactly one verdict, the review's own
|
|
123
|
+
// example quoted back.
|
|
119
124
|
const DISMISSAL_LIST_TAIL = /^(?:\*\*|`|\s|[,,、/]|and\b|和|[A-Z]?#\d{1,7}\b)*/;
|
|
120
125
|
const DISMISSAL_GLOSS = /^\s*(?:([^()\n]{1,80})|\([^()\n]{1,80}\))/;
|
|
121
|
-
const
|
|
122
|
-
|
|
126
|
+
const ADJ_VERDICT_NEXT =
|
|
127
|
+
'here|to|for|in|on|now|this|since|because|as|so|and|but|at|with|anyway|anymore|either|too|though|today';
|
|
128
|
+
const DISMISSAL_MARKER = new RegExp(
|
|
129
|
+
"^[\\s*`::\\-—–(([]*(?:(?:本轮|本次|这次|此次|此处)\\s*)?(?:n\\/a\\b|not applicable|does(?:n't| not) apply|did(?:n't| not) apply|" +
|
|
130
|
+
`(?:irrelevant|not relevant|unrelated)(?![ \\t]+(?!(?:${ADJ_VERDICT_NEXT})\\b)[A-Za-z])|` +
|
|
131
|
+
'不适用|不相关|无关(?!紧要)|与(?:本|这|此)[^。\\n;;]{0,16}无关)',
|
|
132
|
+
'i',
|
|
133
|
+
);
|
|
123
134
|
|
|
124
135
|
/**
|
|
125
136
|
* True when the `#NN` that ENDS at `end` in `text` is answered as not applying.
|
|
@@ -261,8 +272,11 @@ export function classifyCitationContext(transcriptPath, opts = {}) {
|
|
|
261
272
|
continue;
|
|
262
273
|
}
|
|
263
274
|
// Only `text` blocks count as citing. `thinking` is deliberately excluded:
|
|
264
|
-
// extractCitationsFromTranscript scores text only
|
|
265
|
-
//
|
|
275
|
+
// extractCitationsFromTranscript scores text only. The numerator is still WIDER than
|
|
276
|
+
// the decay loop's by one class: a `#NN n/a` dismissal counts here, and the decay loop
|
|
277
|
+
// (extractCitationsFromTranscript's default) does not. Left as is because
|
|
278
|
+
// benchmark/citation-live-replay.mjs reads this split, and changing it would move
|
|
279
|
+
// that ruler's caliber (v6.13.0 defect review P3-7).
|
|
266
280
|
if (block.type !== 'text' || typeof block.text !== 'string') continue;
|
|
267
281
|
CITATION_RE.lastIndex = 0;
|
|
268
282
|
let m;
|
package/mem-cli.mjs
CHANGED
|
@@ -2794,7 +2794,10 @@ function cmdMemdirAudit(args) {
|
|
|
2794
2794
|
// main decay loop excludes (it runs mainOnly). aggregateProjectCiteRecall scans THIS
|
|
2795
2795
|
// project's transcripts: top-level <session>.jsonl = main, and
|
|
2796
2796
|
// <session>/subagents/agent-*.jsonl = sidechain (descends ONE level into the literal
|
|
2797
|
-
// subagents/ dir only, no unbounded recursion).
|
|
2797
|
+
// subagents/ dir only, no unbounded recursion). Main and sidechain use the same
|
|
2798
|
+
// methodology here, so they compare with each other. It is a compliance ratio that counts
|
|
2799
|
+
// a `#NN n/a` dismissal as an answer, so it does NOT compare with the per-face rates,
|
|
2800
|
+
// including the `subagent` face, which exclude dismissals since v6.13.0.
|
|
2798
2801
|
function _reportSidechainCiteRecall({ days, json }) {
|
|
2799
2802
|
const cutoff = Date.now() - days * 86400 * 1000;
|
|
2800
2803
|
// memdir = ~/.claude/projects/<encoded>/memory; transcripts are its siblings.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.13.
|
|
3
|
+
"version": "6.13.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.13.
|
|
9
|
+
"version": "6.13.3",
|
|
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.13.
|
|
3
|
+
"version": "6.13.3",
|
|
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",
|