claude-mem-lite 6.7.1 → 6.7.2
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/lib/file-edge-match.mjs +20 -0
- package/lib/import-jsonl.mjs +62 -23
- package/lib/recall-core.mjs +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/post-tool-recall.js +2 -1
- package/scripts/pre-tool-recall.js +2 -1
- package/scripts/user-prompt-search.js +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.7.
|
|
12
|
+
"version": "6.7.2",
|
|
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.2",
|
|
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/file-edge-match.mjs
CHANGED
|
@@ -213,6 +213,26 @@ export function rankFileCandidates(files) {
|
|
|
213
213
|
.map((x) => x.f);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The path a tool-use touched, whichever key the tool spells it with.
|
|
218
|
+
*
|
|
219
|
+
* `Edit` / `Write` / `Read` carry `file_path`; `NotebookEdit` carries
|
|
220
|
+
* `notebook_path` and NEVER `file_path`. That one rule had three separate
|
|
221
|
+
* spellings in this repo (an inline `??` in each recall script, a regex
|
|
222
|
+
* alternation in lib/hook-stdin.mjs) and a FOURTH site that simply did not know
|
|
223
|
+
* it — lib/import-jsonl.mjs gated its file edges on `file_path` alone, so every
|
|
224
|
+
* imported notebook edit built no (obs,file) edge at all and was unreachable
|
|
225
|
+
* through the recall path this module exists to serve. A second copy is exactly
|
|
226
|
+
* what produced R12 B-1; this is the home.
|
|
227
|
+
*
|
|
228
|
+
* @param {object|null|undefined} input a tool-use `input` / `tool_input` object
|
|
229
|
+
* @returns {string|undefined} the path, or undefined when the shape carries none
|
|
230
|
+
*/
|
|
231
|
+
export function toolEditPath(input) {
|
|
232
|
+
if (!input || typeof input !== 'object') return undefined;
|
|
233
|
+
return input.file_path ?? input.notebook_path;
|
|
234
|
+
}
|
|
235
|
+
|
|
216
236
|
/** Bind values for fileMatchClause, in placeholder order. */
|
|
217
237
|
export function fileMatchParams(filePath) {
|
|
218
238
|
const fname = basenameAnySep(filePath);
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import { readFileSync, statSync } from 'fs';
|
|
|
18
18
|
import { createHash } from 'crypto';
|
|
19
19
|
import { scrubSecrets } from '../secret-scrub.mjs';
|
|
20
20
|
import { scrubRecord } from './scrub-record.mjs';
|
|
21
|
+
import { toolEditPath } from './file-edge-match.mjs';
|
|
22
|
+
import { insertObservationFiles } from './observation-write.mjs';
|
|
21
23
|
|
|
22
24
|
const TOOL_TO_TYPE = {
|
|
23
25
|
Edit: 'change',
|
|
@@ -130,11 +132,16 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
130
132
|
? toolResult.content
|
|
131
133
|
: JSON.stringify(toolResult?.content ?? '').slice(0, 4000);
|
|
132
134
|
|
|
135
|
+
// D#35: this gated on `input.file_path` alone, and `NotebookEdit` carries
|
|
136
|
+
// `notebook_path` and never `file_path` — so the branch named NotebookEdit
|
|
137
|
+
// while being unable to fire for it, and every imported notebook edit built
|
|
138
|
+
// no (obs,file) edge. `toolEditPath` is the single home for that rule.
|
|
139
|
+
const editedPath = toolEditPath(toolUse.input);
|
|
133
140
|
const filesModified =
|
|
134
|
-
(toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') &&
|
|
135
|
-
? [
|
|
141
|
+
(toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') && editedPath
|
|
142
|
+
? [editedPath]
|
|
136
143
|
: [];
|
|
137
|
-
const filesRead = toolName === 'Read' &&
|
|
144
|
+
const filesRead = toolName === 'Read' && editedPath ? [editedPath] : [];
|
|
138
145
|
|
|
139
146
|
// `narrative` carries the body and `text` is the derived search blob
|
|
140
147
|
// (lib/observation-write.mjs rebuildObservationDerived). Writing the payload to `text`
|
|
@@ -144,7 +151,18 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
144
151
|
// write directly.
|
|
145
152
|
const body = `${inputJson}\n---\n${resultText}`;
|
|
146
153
|
const safe = scrubRecord('observations', {
|
|
147
|
-
|
|
154
|
+
// This string IS the cross-run dedup key: tryImportToolPair synthesizes it
|
|
155
|
+
// byte-for-byte from the same expression to decide whether a row was already
|
|
156
|
+
// imported, so the two sites must be widened TOGETHER or every import
|
|
157
|
+
// duplicates. An earlier cut of this round kept the title on `file_path`
|
|
158
|
+
// alone to avoid the migration — and shipped `NotebookEdit: ` with an empty
|
|
159
|
+
// label for exactly the rows D#35 had just made reachable. Pre-ship review:
|
|
160
|
+
// "the surface the round unblocked renders a row that identifies nothing."
|
|
161
|
+
//
|
|
162
|
+
// The cost is real and bounded: NotebookEdit rows imported before v6.7.2
|
|
163
|
+
// carry the old key, so the next import re-adds each of them ONCE and then
|
|
164
|
+
// matches forever. Stated in the CHANGELOG rather than absorbed silently.
|
|
165
|
+
title: `${toolName}: ${(toolUse.input?.command || toolEditPath(toolUse.input) || '').slice(0, 80)}`,
|
|
148
166
|
subtitle: '',
|
|
149
167
|
text: body,
|
|
150
168
|
narrative: body,
|
|
@@ -154,28 +172,49 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
154
172
|
search_aliases: null,
|
|
155
173
|
});
|
|
156
174
|
|
|
157
|
-
db
|
|
158
|
-
|
|
175
|
+
const inserted = db
|
|
176
|
+
.prepare(
|
|
177
|
+
`
|
|
159
178
|
INSERT INTO observations
|
|
160
179
|
(memory_session_id, project, text, type, title, subtitle, narrative, concepts, facts, files_read, files_modified, importance, created_at, created_at_epoch)
|
|
161
180
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
162
181
|
`,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
)
|
|
183
|
+
.run(
|
|
184
|
+
memId(sessionId),
|
|
185
|
+
project,
|
|
186
|
+
safe.text,
|
|
187
|
+
type,
|
|
188
|
+
safe.title,
|
|
189
|
+
safe.subtitle,
|
|
190
|
+
safe.narrative,
|
|
191
|
+
safe.concepts,
|
|
192
|
+
safe.facts,
|
|
193
|
+
JSON.stringify(filesRead),
|
|
194
|
+
JSON.stringify(filesModified),
|
|
195
|
+
1,
|
|
196
|
+
ts,
|
|
197
|
+
Date.parse(ts) || Date.now(),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Import wrote `files_modified` as a JSON column and stopped there, so no
|
|
201
|
+
// imported observation had a row in the `observation_files` junction — and
|
|
202
|
+
// that junction is what the file-recall paths JOIN. Two of them can now return
|
|
203
|
+
// an imported row: `recallByFile` (CLI `recall` + `mem_recall`) and
|
|
204
|
+
// `searchByFile` (the UserPromptSubmit leg). The pre-tool recall leg JOINs it
|
|
205
|
+
// too but still CANNOT — pre-ship review measured three independent structural
|
|
206
|
+
// gates, two of them literals: `importance >= 2` against the `1` written
|
|
207
|
+
// below, and `lesson_learned` non-empty OR `type IN ('bugfix','decision')`
|
|
208
|
+
// against a NULL lesson and a type that is only ever `change`/`discovery`.
|
|
209
|
+
// Do not list it as a beneficiary without changing one of those.
|
|
210
|
+
//
|
|
211
|
+
// Measured before the fix: an
|
|
212
|
+
// `Edit` with `file_path` set produced `files_modified=["/repo/alpha.mjs"]`
|
|
213
|
+
// and ZERO junction rows, so the defect was never NotebookEdit-specific —
|
|
214
|
+
// D#35 named a symptom of it. Same call and same list as the canonical save
|
|
215
|
+
// path (lib/save-observation.mjs:324), which is why `insertObs` in the test
|
|
216
|
+
// helpers mirrors it and no existing test could see the gap.
|
|
217
|
+
insertObservationFiles(db, Number(inserted.lastInsertRowid), filesModified);
|
|
179
218
|
return true;
|
|
180
219
|
}
|
|
181
220
|
|
|
@@ -256,7 +295,7 @@ export async function importJsonl(db, path, { project }) {
|
|
|
256
295
|
// Cross-call dedup: synthesize the title the previous run would have
|
|
257
296
|
// written and check the seenObs set seeded from the DB.
|
|
258
297
|
const toolName = useEv.name || 'unknown';
|
|
259
|
-
const titlePreview = `${toolName}: ${(useEv.input?.command || useEv.input
|
|
298
|
+
const titlePreview = `${toolName}: ${(useEv.input?.command || toolEditPath(useEv.input) || '').slice(0, 80)}`;
|
|
260
299
|
const ts = useEv.timestamp || new Date().toISOString();
|
|
261
300
|
// Match the storage convention from importToolPair (memId-prefixed) so
|
|
262
301
|
// the seenObs entries seeded from the DB can be matched on a re-run.
|
package/lib/recall-core.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export function recallByFile(db, file, { limit = 10, includeNoise = false } = {}
|
|
|
42
42
|
WHERE ${liveObsFilterSql('o')}
|
|
43
43
|
AND ${fileMatchClause('of2')}
|
|
44
44
|
${noiseClause}
|
|
45
|
-
ORDER BY o.created_at_epoch DESC
|
|
45
|
+
ORDER BY o.importance DESC, o.created_at_epoch DESC, o.id DESC
|
|
46
46
|
LIMIT ?
|
|
47
47
|
`,
|
|
48
48
|
)
|
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.2",
|
|
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.2",
|
|
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.2",
|
|
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",
|
|
@@ -30,6 +30,7 @@ import { queueHookContext, flushHookStdout } from '../lib/hook-stdout.mjs';
|
|
|
30
30
|
// P1-9: one bounded stdin reader. Import-free, like hook-stdout.mjs beside it.
|
|
31
31
|
import { readHookStdin, TOOL_INPUT_FILE_MAX_BYTES } from '../lib/hook-stdin.mjs';
|
|
32
32
|
import { cooldownPathFor as sharedCooldownPathFor } from '../lib/cooldown-path.mjs';
|
|
33
|
+
import { toolEditPath } from '../lib/file-edge-match.mjs';
|
|
33
34
|
|
|
34
35
|
const SALIENCE_BIND = process.env.CLAUDE_MEM_SALIENCE === 'bind';
|
|
35
36
|
|
|
@@ -59,7 +60,7 @@ async function main() {
|
|
|
59
60
|
// v6.7.0 and this leg did not, which is the repo's most repeated failure shape:
|
|
60
61
|
// a fix that closes ONE of the inputs reaching the same line. Caught in pre-ship
|
|
61
62
|
// review of that very round.
|
|
62
|
-
filePath = e.tool_input
|
|
63
|
+
filePath = toolEditPath(e.tool_input);
|
|
63
64
|
sessionId = e.session_id || null;
|
|
64
65
|
} catch {
|
|
65
66
|
return;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
fileMatchParams,
|
|
26
26
|
basenameAnySep,
|
|
27
27
|
jsonArrayLikeNeedle,
|
|
28
|
+
toolEditPath,
|
|
28
29
|
} from '../lib/file-edge-match.mjs';
|
|
29
30
|
import { fileIntelFor } from '../lib/file-intel.mjs';
|
|
30
31
|
import { shouldWarnReread, buildRereadWarning, readFileMeta } from '../lib/reread-guard.mjs';
|
|
@@ -406,7 +407,7 @@ try {
|
|
|
406
407
|
// additionalProperties:false. Reading only `file_path` made this hook a
|
|
407
408
|
// no-op on every .ipynb edit (R12 audit, partition B-2). utils.mjs's
|
|
408
409
|
// `case 'NotebookEdit'` already knew the shape differs; this leg did not.
|
|
409
|
-
filePath = event.tool_input
|
|
410
|
+
filePath = toolEditPath(event.tool_input);
|
|
410
411
|
sessionId = event.session_id || null;
|
|
411
412
|
toolName = event.tool_name || null;
|
|
412
413
|
const off = event.tool_input?.offset;
|
|
@@ -524,7 +524,7 @@ function searchByFile(db, files, project, limit) {
|
|
|
524
524
|
AND o.created_at_epoch > ?
|
|
525
525
|
AND ${fileMatchClause('of2')}
|
|
526
526
|
AND ${notLowSignalTitleClause('o')}
|
|
527
|
-
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
527
|
+
ORDER BY o.importance DESC, o.created_at_epoch DESC, o.id DESC
|
|
528
528
|
LIMIT ?
|
|
529
529
|
`);
|
|
530
530
|
|