claude-mem-lite 5.2.0 → 5.3.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": "5.2.0",
13
+ "version": "5.3.0",
14
14
  "source": "./",
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)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
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
@@ -120,7 +120,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
120
120
  - **Schema auto-migration** -- Idempotent `ALTER TABLE` migrations run on every startup, safely adding new columns and indexes without data loss
121
121
  - **LLM concurrency control** -- File-based semaphore limits background workers to 2 concurrent LLM calls, preventing resource contention
122
122
  - **stdin overflow protection** -- Hook input truncated at 256KB with regex-based action salvage for oversized tool outputs
123
- - **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/clear` or `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap
123
+ - **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap. **The `/clear` and `/compact` arm does not currently fire** (tracked as R10-P1-1). The measurement, first: on the maintainer's own install `session_handoffs` holds 4 `exit` rows and **0** `clear` rows — the `clear` snapshot has never once been written. The mechanism, as a hypothesis: SessionStart treats a session file left on disk as the marker of a previous session that ended without `Stop` (`hook.mjs:2393-2405`, whose own comment reads "Normal `/exit` deletes the file, so this only triggers for `/clear`, `/compact`, or crash recovery"), and that file is exactly what `Stop` deletes (`hook.mjs:1542`) — which would make the branch unreachable if `Stop` runs at the end of every assistant turn rather than once per session. **That last clause is NOT verified**, and it is the whole question: the fix differs depending on whether Claude Code rotates its session id across `/clear`, so a real `/clear` stdin capture comes before any code change
124
124
  - **Git-SHA continuation anchor** (v2.31.0) -- Handoff rows include `git_sha_at_handoff`; any handoff matching the current `HEAD` counts as continuation regardless of TTL. Code state is a stronger continuation signal than wall-clock time
125
125
  - **Startup dashboard** (v2.31.0) -- SessionStart hook aggregates `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + most-recent exit handoff + recent event count into a single structured block injected via `hookSpecificOutput.additionalContext`
126
126
  - **Activity namespace** (v2.31.0) -- Dedicated `events` table + FTS5 for non-memdir types (`bugfix`, `lesson`, `bug`, `discovery`, `refactor`, `feature`, `observation`, `decision`) that don't compete with `WHAT_NOT_TO_SAVE` semantics on the observations table. CLI: `claude-mem-lite activity save|search|recent|show`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic. (v3.39: the `/lesson` and `/bug` slash commands were redirected from this events table to searchable **observations** — `mem_search` never read the events table, so explicit saves were unfindable; the events table remains the auto-capture activity log.)
@@ -431,7 +431,9 @@ FTS5 indexes: `observations_fts` (title, subtitle, narrative, text, facts, conce
431
431
 
432
432
  ```
433
433
  SessionStart
434
- -> Generate session ID (or save handoff snapshot on /clear)
434
+ -> Generate session ID
435
+ (the /clear|/compact handoff branch here is currently unreachable — R10-P1-1,
436
+ see Cross-session handoff above)
435
437
  -> Mark stale sessions (>24h active) as abandoned
436
438
  -> Clean orphaned/stale lock files
437
439
  -> Query recent observations (24h)
@@ -458,8 +460,9 @@ UserPromptSubmit (two parallel paths)
458
460
 
459
461
  Stop
460
462
  -> Flush final episode buffer
461
- -> Save handoff snapshot (on /exit)
463
+ -> Save handoff snapshot (type 'exit')
462
464
  -> Mark session completed
465
+ -> Delete the session file <- what makes the SessionStart /clear branch unreachable
463
466
  -> Spawn LLM summary worker (poll-based wait)
464
467
  ```
465
468
 
@@ -796,6 +799,7 @@ benchmark and A/B harness are calibrated against — changing them invalidates t
796
799
  | `MEM_OR_FALLBACK_MAX_TOKENS` | Max query tokens allowed into the OR fallback (∈ [0,50]). | `8` |
797
800
  | `CLAUDE_MEM_CJK_PREC_MIN` | Precision floor for CJK segmentation candidates. | `0.2` |
798
801
  | `CLAUDE_MEM_AUTO_DEEP` | `0` disables automatic deep-search escalation (one Haiku call rewriting a weak query into keyword/concept/HyDE variants). Explicit `deep: true` still works. | _(auto)_ |
802
+ | `CLAUDE_MEM_DEEP_DISCLOSURE` | `off` suppresses the one-line caveat appended to a multi-variant deep result. The caveat exists because deep search fills the page even when the corpus cannot answer — measured at 10 of 10 slots on queries whose answers had been removed (`benchmark/deep-search-holdout.mjs`) — and `deep` is AUTO by default on the MCP surface, i.e. it escalates precisely when the honest answer is "nothing". It does not change retrieval, ranking, or which rows are returned. | _(on)_ |
799
803
  | `CLAUDE_MEM_AUTO_DEEP_CLI` | `0` disables the same auto-escalation on the CLI path only. | _(auto)_ |
800
804
  | `CLAUDE_MEM_VECTORS` | `1` re-enables the persisted TF-IDF vector arm (off by default; also needs a vector rebuild via `maintain`). | _(off)_ |
801
805
  | `CLAUDE_MEM_SCOPE_FILTER` | `1` stops environment-scoped observations from firing on file-triggered recall. They stay reachable via search. **Leave it off**: on the face it gates, `environment` is not the low-relevance class its premise assumes — it cites at least as well as `project` (47.5% vs 44.3%, intervals overlapping), and an earlier measurement left 173 recall groups empty with it on. | _(off)_ |
@@ -823,7 +827,7 @@ what is already stored — only whether new work runs.
823
827
 
824
828
  | Variable | Description | Default |
825
829
  |----------|-------------|---------|
826
- | `CLAUDE_MEM_SKIP_SUMMARY` | Skip the LLM session summary at Stop. | _(runs)_ |
830
+ | `CLAUDE_MEM_SKIP_SUMMARY` | Skip the background LLM session summary at **both** of its spawn sites — `Stop`, and the SessionStart `/clear`-handoff path. Until v5.3.0 only the `Stop` one honoured it. | _(runs)_ |
827
831
  | `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush — observations are still batched, just not summarized. | _(runs)_ |
828
832
  | `CLAUDE_MEM_SKIP_SAVE_ENRICH` | Skip the background Haiku call that backfills `lesson_learned` / search aliases after a save. | _(runs)_ |
829
833
  | `CLAUDE_MEM_SKIP_COMPRESS` | Skip auto-compression of old observations. | _(runs)_ |
package/deep-search.mjs CHANGED
@@ -185,6 +185,66 @@ export function resolveDeepMode(explicitDeep, { surface, env = process.env } = {
185
185
  return surface === 'mcp' ? 'auto' : 'normal';
186
186
  }
187
187
 
188
+ /**
189
+ * One-line disclosure for a deep result set, written to the channel the CALLER reads.
190
+ *
191
+ * D#3. benchmark/deep-search-holdout.mjs asks the suite's own queries of a corpus with
192
+ * their relevant_ids deleted, so the correct answer is zero rows and every returned row is
193
+ * a false positive by construction. It reads mean FP@10 = 10.00 across 12/12 queries: deep
194
+ * fills every slot, every time. The single-query baseline returns 1-2 rows on the same
195
+ * negatives — the flood is the UNION across paraphrase variants, which is also where deep's
196
+ * recall win comes from, so this is not a bug to be thresholded away. Three gates were
197
+ * tested against both arms and rejected; suppressing OR-fallback on rewrites takes deep
198
+ * R@10 from 0.7383 to 0.3962, because the vocab-mismatch win IS that fallback. rrfFuseN
199
+ * fuses by RANK, so no magnitude signal survives the merge for a downstream floor to read.
200
+ *
201
+ * The discrimination is not available at this layer, so the honest move is to hand the
202
+ * caller what the caller cannot otherwise see. Two things were missing:
203
+ * 1. `escalated` — that the widening happened BECAUSE the plain search was weak — was
204
+ * announced on stderr only. On the MCP surface stderr never reaches the model, which
205
+ * reads tool results; auto is the default there (resolveDeepMode, surface 'mcp'), so
206
+ * the one caller who most needs the caveat was the one who could not see it.
207
+ * 2. Nothing said a full page can be entirely adjacent rows.
208
+ *
209
+ * Silent in two cases, and both silences are load-bearing:
210
+ * - `variantCount <= 1`: with no usable rewrite, deep IS the baseline, and the existing
211
+ * "== baseline" note already says so. Crying flood there would train callers to ignore
212
+ * the line on the runs where it matters.
213
+ * - `rowCount <= 0`: "rows above may be adjacent" is nonsense with no rows above, and
214
+ * both zero-result faces ALREADY say the rewrite ran and found nothing — so the note
215
+ * would restate the page's own conclusion in more words. Caught in pre-ship review,
216
+ * which is also why `rowCount` is a parameter rather than a check in each face: two
217
+ * surfaces deciding this separately is how they drift.
218
+ *
219
+ * @param {object} [opts]
220
+ * @param {boolean} [opts.escalated] the result came from auto-escalation, not an explicit ask
221
+ * @param {number} [opts.escalatedObsCount] hits the plain search returned before widening
222
+ * @param {number} [opts.variantCount] query variants fused (1 = rewrite produced nothing)
223
+ * @param {number} [opts.rowCount] rows actually shown to the caller
224
+ * @param {object} [opts.env=process.env] opt-out: CLAUDE_MEM_DEEP_DISCLOSURE=off
225
+ * @returns {string} the note, or '' when it should not be shown
226
+ */
227
+ export function deepDisclosureNote({
228
+ escalated = false,
229
+ escalatedObsCount = 0,
230
+ variantCount = 0,
231
+ rowCount = 0,
232
+ env = process.env,
233
+ } = {}) {
234
+ if (String(env.CLAUDE_MEM_DEEP_DISCLOSURE || '').toLowerCase() === 'off') return '';
235
+ if (!(variantCount > 1)) return '';
236
+ if (!(rowCount > 0)) return '';
237
+ const why = escalated
238
+ ? `auto-escalated after the plain search returned ${escalatedObsCount} hit(s)`
239
+ : 'explicitly requested';
240
+ return (
241
+ `[deep search: ${why}. Rows above may be ADJACENT to the query rather than answers to it — ` +
242
+ `on a corpus that cannot answer, the widened query still fills the page (measured: 10 of 10 ` +
243
+ `slots on queries whose answers had been removed). Judge each row by its own text; ` +
244
+ `"nothing here actually answers this" is a valid conclusion.]`
245
+ );
246
+ }
247
+
188
248
  // Echoes hook-llm.mjs MEMORY_INPUT_GUARD (kept inline rather than imported so
189
249
  // this module — and the tests that import it — never pull in hook-llm's
190
250
  // native-heavy chain; see #8729). Same security intent: the query is untrusted.
package/hook.mjs CHANGED
@@ -2038,8 +2038,15 @@ function saveHandoffAndFastSummary(
2038
2038
  .get(prevProject || project, 'clear', handoffScopeId);
2039
2039
  } catch {}
2040
2040
 
2041
- // Generate session summary for previous session (background Haiku — richer version)
2042
- spawnBackground('llm-summary', prevSessionId, prevProject || project);
2041
+ // Generate session summary for previous session (background Haiku — richer version).
2042
+ // Honours CLAUDE_MEM_SKIP_SUMMARY like the handleStop site does. This is the SAME
2043
+ // worker, and the flag's whole purpose (see the comment at its other call site) is
2044
+ // that llm-summary recreates a test's sandbox tree behind its cleanup — timed at
2045
+ // 432ms there. Gating one of two call sites left the flag unable to do the one job
2046
+ // it exists for whenever this branch is reached.
2047
+ if (!process.env.CLAUDE_MEM_SKIP_SUMMARY) {
2048
+ spawnBackground('llm-summary', prevSessionId, prevProject || project);
2049
+ }
2043
2050
 
2044
2051
  // Build fast synchronous summary for immediate context availability.
2045
2052
  // Background llm-summary will produce a richer Haiku version later;
package/mem-cli.mjs CHANGED
@@ -35,7 +35,13 @@ import {
35
35
  BROWSE_TIERS,
36
36
  BROWSE_TIER_LABELS,
37
37
  } from './lib/browse-core.mjs';
38
- import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
38
+ import {
39
+ deepSearch,
40
+ resolveDeepMode,
41
+ shouldEscalateToDeep,
42
+ autoDeepLlmReady,
43
+ deepDisclosureNote,
44
+ } from './deep-search.mjs';
39
45
  import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
40
46
  import {
41
47
  runMaintainOps,
@@ -433,6 +439,19 @@ async function cmdSearch(db, args, { llm } = {}) {
433
439
  : '[mem] Deep search: rerank produced no usable order; kept fused order\n',
434
440
  );
435
441
  }
442
+ // D#3. Same disclosure the MCP surface puts in its payload, on the channel this face
443
+ // already uses for deep notes — a Bash caller reads stderr alongside stdout, so unlike
444
+ // MCP there is no split here. One home for the text (deep-search.mjs) because two faces
445
+ // writing their own wording is this repo's most-paid-for defect class.
446
+ if (isDeep) {
447
+ const disclosure = deepDisclosureNote({
448
+ escalated: res.escalated,
449
+ escalatedObsCount: res.escalatedObsCount,
450
+ variantCount: deepVariants?.length ?? 0,
451
+ rowCount: paged?.length ?? 0,
452
+ });
453
+ if (disclosure) process.stderr.write(`${disclosure}\n`);
454
+ }
436
455
 
437
456
  // "nothing matched" (no offset) vs "this page is empty" (with offset) — the two
438
457
  // CLI messages. preFinalizeCount is the pre-pagination population (post-tier).
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "5.2.0",
9
+ "version": "5.3.0",
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": "5.2.0",
3
+ "version": "5.3.0",
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",
package/server.mjs CHANGED
@@ -10,7 +10,13 @@ import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
10
10
  import { ensureDbWithWalRecovery, DB_PATH, DB_DIR } from './schema.mjs';
11
11
  import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
12
12
  import { searchObservationsHybrid } from './search-engine.mjs';
13
- import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
13
+ import {
14
+ deepSearch,
15
+ resolveDeepMode,
16
+ shouldEscalateToDeep,
17
+ autoDeepLlmReady,
18
+ deepDisclosureNote,
19
+ } from './deep-search.mjs';
14
20
  import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
15
21
  import {
16
22
  resolveAnchorToken,
@@ -577,6 +583,18 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
577
583
  if (r.reranked && output.content?.[0]?.type === 'text') {
578
584
  output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
579
585
  }
586
+ // D#3. The stderr line above is for humans and logs; an MCP client reads the tool RESULT,
587
+ // so the escalation fact and the adjacency caveat have to land in the payload or the one
588
+ // caller running deep=auto by default never sees either.
589
+ if (r.isDeep && output.content?.[0]?.type === 'text') {
590
+ const disclosure = deepDisclosureNote({
591
+ escalated: r.escalated,
592
+ escalatedObsCount: r.escalatedObsCount,
593
+ variantCount: r.variants?.length ?? 0,
594
+ rowCount: r.page?.length ?? 0,
595
+ });
596
+ if (disclosure) output.content[0].text += `\n\n${disclosure}`;
597
+ }
580
598
  appendDeferredTrailer(output);
581
599
 
582
600
  // Expose structured fields for tests + the MCP content blob.