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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.7.1",
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.1",
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"
@@ -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);
@@ -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') && toolUse.input?.file_path
135
- ? [toolUse.input.file_path]
141
+ (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') && editedPath
142
+ ? [editedPath]
136
143
  : [];
137
- const filesRead = toolName === 'Read' && toolUse.input?.file_path ? [toolUse.input.file_path] : [];
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
- title: `${toolName}: ${(toolUse.input?.command || toolUse.input?.file_path || '').slice(0, 80)}`,
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.prepare(
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
- ).run(
164
- memId(sessionId),
165
- project,
166
- safe.text,
167
- type,
168
- safe.title,
169
- safe.subtitle,
170
- safe.narrative,
171
- safe.concepts,
172
- safe.facts,
173
- JSON.stringify(filesRead),
174
- JSON.stringify(filesModified),
175
- 1,
176
- ts,
177
- Date.parse(ts) || Date.now(),
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?.file_path || '').slice(0, 80)}`;
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.
@@ -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
  )
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.7.1",
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.1",
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.1",
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?.file_path ?? e.tool_input?.notebook_path;
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?.file_path ?? event.tool_input?.notebook_path;
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