claude-mem-lite 2.39.1 → 2.40.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.39.1",
13
+ "version": "2.40.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.39.1",
3
+ "version": "2.40.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/mem-cli.mjs CHANGED
@@ -172,6 +172,9 @@ function cmdSearch(db, args) {
172
172
  const effectiveSource = source || ((type || tier || minImportance) ? 'observations' : null);
173
173
 
174
174
  const results = [];
175
+ // Tracks whether AND returned 0 and OR recovered non-empty. Mirrors server.mjs
176
+ // ctx.orFallbackFired so the header can surface a "(relaxed AND→OR)" hint.
177
+ let orFallbackFired = false;
175
178
 
176
179
  // Search observations
177
180
  if (!effectiveSource || effectiveSource === 'observations') {
@@ -179,7 +182,10 @@ function cmdSearch(db, args) {
179
182
  if (obsRows.length === 0) {
180
183
  const orQuery = relaxFtsQueryToOr(ftsQuery);
181
184
  if (orQuery) {
182
- try { obsRows = searchFts(db, orQuery, { type, project, limit, dateFrom, dateTo, minImportance, branch, includeNoise, offset: effectiveSource ? offset : 0 }); } catch {}
185
+ try {
186
+ obsRows = searchFts(db, orQuery, { type, project, limit, dateFrom, dateTo, minImportance, branch, includeNoise, offset: effectiveSource ? offset : 0 });
187
+ if (obsRows.length > 0) orFallbackFired = true;
188
+ } catch {}
183
189
  }
184
190
  }
185
191
  // Type-list fallback
@@ -388,7 +394,9 @@ function cmdSearch(db, args) {
388
394
 
389
395
  const showTime = sort === 'time';
390
396
  const hasMixed = paged.some(r => r._source === 'session' || r._source === 'prompt');
391
- out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}":${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
397
+ // Suppressed when --or was explicit user already asked for OR, no "fallback" there.
398
+ const fallbackHint = orFallbackFired && !useOr ? ' (relaxed AND→OR)' : '';
399
+ out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}"${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
392
400
  for (const r of paged) {
393
401
  const timeStr = showTime && r.created_at_epoch ? ` (${relativeTime(r.created_at_epoch)})` : '';
394
402
  if (r._source === 'session') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.39.1",
3
+ "version": "2.40.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -228,6 +228,10 @@ try {
228
228
  // but the total payload is bounded by the 3-row limit and the cooldown.
229
229
  const LESSON_MAX = isRead ? 120 : 240;
230
230
  if (allRows.length > 0) {
231
+ // Framing line mirrors #7758 handoff-injection fix: without an explicit
232
+ // "system-injected, continue" disclaimer, observed turn-end after Edit+reminder
233
+ // when the model misreads passive lesson context as a closing note.
234
+ lines.push(`[mem] PreToolUse recall — system-injected context, continue your planned action:`);
231
235
  lines.push(`[mem] Lessons for ${fname}:`);
232
236
  for (const r of allRows) {
233
237
  if (r.lesson_learned) {
@@ -250,6 +254,7 @@ try {
250
254
  // v2.34.6: Read does NOT emit this nudge. Read is passive — the agent
251
255
  // isn't necessarily about to solve anything, so /lesson prompts are noise.
252
256
  // Empty Reads exit silently, saving ~60 tokens × (every empty-file Read).
257
+ lines.push(`[mem] PreToolUse recall — system-injected context, continue your planned action:`);
253
258
  lines.push(`[mem] No prior lessons for ${fname} — if you solve a non-obvious bug here, run: /lesson --file ${fname} "<root cause + fix>"`);
254
259
  }
255
260
 
package/server.mjs CHANGED
@@ -235,13 +235,17 @@ function searchObservations(ctx) {
235
235
  .all(...buildObsFtsParams({ now, projectBoost, ftsQuery, args, epochFrom, epochTo, limit: perSourceLimit, offset: perSourceOffset }));
236
236
  for (const r of rows) results.push(ftsRowToResult(r, { snippet: true }));
237
237
 
238
- // OR fallback: when AND query returns 0 results, retry with OR semantics
238
+ // OR fallback: when AND query returns 0 results, retry with OR semantics.
239
+ // Sets ctx.orFallbackFired so the top-level formatter can surface a "relaxed
240
+ // AND→OR" hint — without it, callers can't distinguish a strict multi-term
241
+ // match from a partial single-term recovery.
239
242
  if (rows.length === 0) {
240
243
  const orQuery = relaxFtsQueryToOr(ftsQuery);
241
244
  if (orQuery) {
242
245
  try {
243
246
  const orRows = db.prepare(buildObsFtsQuery('full', { multiplier: 0.5, withSnippet: true, withOffset: true, includeNoise }))
244
247
  .all(...buildObsFtsParams({ now, projectBoost, ftsQuery: orQuery, args, epochFrom, epochTo, limit: perSourceLimit, offset: perSourceOffset }));
248
+ if (orRows.length > 0) ctx.orFallbackFired = true;
245
249
  for (const r of orRows) results.push(ftsRowToResult(r, { snippet: true }));
246
250
  } catch (e) { debugCatch(e, 'searchObservations-or-fallback'); }
247
251
  }
@@ -515,7 +519,7 @@ function searchPrompts(ctx) {
515
519
  return results;
516
520
  }
517
521
 
518
- function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCrossSource) {
522
+ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCrossSource, orFallbackFired = false) {
519
523
  if (paginatedResults.length === 0) {
520
524
  const hint = [];
521
525
  if (args.query && !ftsQuery) {
@@ -540,7 +544,11 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCros
540
544
  // P2-6: empty/omitted query falls through to a "listing recent" path — label it explicitly
541
545
  // so callers don't mistake BM25-less results for relevance-ranked ones.
542
546
  const qLabel = args.query ? ` for "${args.query}"` : ' (no query — listing recent)';
543
- lines.push(`Found ${countLabel} result(s)${qLabel}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}\n`);
547
+ // Surface AND→OR fallback so callers (incl. Claude) know a strict multi-term
548
+ // query actually matched only a subset of the terms. Suppressed when the caller
549
+ // explicitly requested OR semantics — there's no "fallback" in that path.
550
+ const fallbackHint = orFallbackFired && !args.or ? ' (relaxed AND→OR)' : '';
551
+ lines.push(`Found ${countLabel} result(s)${qLabel}${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}\n`);
544
552
 
545
553
  for (const r of paginatedResults) {
546
554
  if (r.source === 'obs') {
@@ -698,7 +706,7 @@ server.registerTool(
698
706
  // Always apply pagination — single-source results can exceed SQL LIMIT due to expansion (concept co-occurrence, PRF, vector search)
699
707
  const paginatedResults = (offset > 0 || results.length > limit) ? results.slice(offset, offset + limit) : results;
700
708
 
701
- return formatSearchOutput(paginatedResults, args, ftsQuery, totalBeforePagination, isCrossSource);
709
+ return formatSearchOutput(paginatedResults, args, ftsQuery, totalBeforePagination, isCrossSource, ctx.orFallbackFired === true);
702
710
  })
703
711
  );
704
712