claude-mem-lite 3.35.1 → 3.35.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.35.1",
13
+ "version": "3.35.2",
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": "3.35.1",
3
+ "version": "3.35.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/hook-episode.mjs CHANGED
@@ -136,6 +136,39 @@ export function createEpisode(sessionId, project) {
136
136
  };
137
137
  }
138
138
 
139
+ /**
140
+ * Split an episode's entries by originating CC session so each concurrent
141
+ * session flushes as its own observation. Common path (single session, or all
142
+ * untagged/legacy entries) returns [episode] BY REFERENCE — behavior identical
143
+ * to pre-grouping. When >=2 sessions interleaved in one buffer, returns one
144
+ * sub-episode per session with its own entries + recomputed file union;
145
+ * filesRead (untagged bash reads-file) is inherited by every sub. Each sub
146
+ * resets savedId so it receives its own from its immediate-save (savedId is
147
+ * load-bearing: llm-episode upgrades the pre-saved obs by it).
148
+ * @param {object} episode
149
+ * @returns {object[]}
150
+ */
151
+ export function planEpisodeFlush(episode) {
152
+ const groups = new Map();
153
+ for (const e of episode.entries) {
154
+ const key = e.ccSession ?? '__none__';
155
+ if (!groups.has(key)) groups.set(key, []);
156
+ groups.get(key).push(e);
157
+ }
158
+ if (groups.size <= 1) return [episode];
159
+ const subs = [];
160
+ for (const [, entries] of groups) {
161
+ subs.push({
162
+ ...episode,
163
+ entries,
164
+ files: [...new Set(entries.flatMap(e => e.files || []))],
165
+ filesRead: episode.filesRead,
166
+ savedId: undefined,
167
+ });
168
+ }
169
+ return subs;
170
+ }
171
+
139
172
  /**
140
173
  * Add file paths to an episode's file tracking set (deduped).
141
174
  * @param {object} episode The episode to update
package/hook.mjs CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import {
32
32
  readEpisodeRaw, episodeFile,
33
33
  acquireLock, releaseLock, readEpisode, writeEpisode,
34
- createEpisode, addFileToEpisode,
34
+ createEpisode, addFileToEpisode, planEpisodeFlush,
35
35
  writePendingEntry, mergePendingEntries, episodeHasSignificantContent,
36
36
  } from './hook-episode.mjs';
37
37
  import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
@@ -162,77 +162,104 @@ function flushEpisode(episode, hookEventName = 'PostToolUse') {
162
162
  episode.filesRead = episode.filesRead || [];
163
163
  }
164
164
 
165
- const isSignificant = episodeHasSignificantContent(episode);
165
+ // Split by CC session so concurrent same-project sessions flush as separate
166
+ // observations. planEpisodeFlush returns [episode] BY REFERENCE for the common
167
+ // single-session (or all-legacy) case → flushEpisodeGroup(episode) is identical
168
+ // to pre-grouping. Two+ interleaved sessions each get their own sub-episode.
169
+ const subs = planEpisodeFlush(episode);
170
+ let anySignificant = false;
171
+ for (const sub of subs) {
172
+ const r = flushEpisodeGroup(sub);
173
+ if (r === 'writefail') {
174
+ // Single-group: preserve the original early return — buffer left un-unlinked
175
+ // for a later retry, no receipt. Multi-group: skip only the failed group and
176
+ // keep the rest. The asymmetry is safe: each group's immediate obs is persisted
177
+ // BEFORE its flush-file write, so re-flushing the whole buffer would re-emit
178
+ // already-saved groups as duplicate observations.
179
+ if (subs.length === 1) return;
180
+ continue;
181
+ }
182
+ if (r === 'significant') anySignificant = true;
183
+ }
184
+
185
+ // Aggregate receipt over the whole episode, gated exactly as before
186
+ // (isSignificant → anySignificant). v2.33.4: Stop rejects hookSpecificOutput.
187
+ if (anySignificant && RECEIPT_EVENTS.has(hookEventName)) {
188
+ try {
189
+ const entries = episode.entries || [];
190
+ const toolCounts = {};
191
+ for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
192
+ const toolSummary = Object.entries(toolCounts)
193
+ .sort((a, b) => b[1] - a[1])
194
+ .slice(0, 3)
195
+ .map(([t, n]) => `${t}×${n}`)
196
+ .join(', ');
197
+ const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
198
+ // v2.83: error→fix nudge lifted to lib/cite-back-hint.mjs::buildUnsavedBugfixHint
199
+ // so the wording (count + "Save now" verb) stays in sync with cite-back.
200
+ const bugfixHint = buildUnsavedBugfixHint(episode);
201
+ if (bugfixHint) lines.push(bugfixHint);
202
+ // v2.81: cite-back hint — fires when this episode edits a file that
203
+ // PreToolUse:Read/Edit nudged earlier in the same session. Precision
204
+ // signal (we know the file was warned about); orthogonal to the
205
+ // bugfix-shape nudge above and may co-fire.
206
+ const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
207
+ if (citeBack) lines.push(citeBack);
208
+ // Trailing newline is REQUIRED: when this receipt flushes at SessionStart
209
+ // (leftover episode after /clear or /compact), the startup dashboard writes a
210
+ // second hookSpecificOutput object right after. Without the '\n' the two land
211
+ // back-to-back as `}{` on one line and Claude Code's line-based JSON parser
212
+ // drops both — losing the episode-flush / cite-back context exactly at the
213
+ // session boundary. Every other hookSpecificOutput write appends '\n'; this
214
+ // was the lone exception.
215
+ process.stdout.write(JSON.stringify({
216
+ suppressOutput: true,
217
+ hookSpecificOutput: {
218
+ hookEventName,
219
+ additionalContext: lines.join('\n'),
220
+ },
221
+ }) + '\n');
222
+ } catch { /* never block on receipt */ }
223
+ }
166
224
 
167
- // Immediate save: create rule-based observation for instant visibility.
168
- // LLM background worker will upgrade title/narrative/importance later.
225
+ // Remove episode buffer AFTER spawning background workers to prevent concurrent overwrites
226
+ try { unlinkSync(episodeFile()); } catch {}
227
+ }
228
+
229
+ // Save one episode-shaped object: immediate rule-based observation (if
230
+ // significant) + flush file + llm-episode enrichment spawn. Extracted from
231
+ // flushEpisode so each CC-session slice (from planEpisodeFlush) flushes
232
+ // independently and carries its OWN savedId into its OWN flush file — the
233
+ // llm-episode worker upgrades the pre-saved obs by that id. Returns
234
+ // 'significant' | 'insignificant' | 'writefail'. CLAUDE_MEM_SKIP_EPISODE_LLM
235
+ // suppresses the detached enrichment spawn (test determinism; sibling of
236
+ // CLAUDE_MEM_SKIP_COMPRESS / _OPTIMIZE) — the synchronous immediate obs still lands.
237
+ function flushEpisodeGroup(ep) {
238
+ const isSignificant = episodeHasSignificantContent(ep);
239
+
240
+ // Immediate save: rule-based observation for instant visibility; the LLM
241
+ // background worker upgrades title/narrative/importance later.
169
242
  if (isSignificant) {
170
243
  try {
171
- const obs = buildImmediateObservation(episode);
172
- const id = saveObservation(obs, episode.project, episode.sessionId);
173
- if (id) episode.savedId = id;
244
+ const obs = buildImmediateObservation(ep);
245
+ const id = saveObservation(obs, ep.project, ep.sessionId);
246
+ if (id) ep.savedId = id;
174
247
  } catch (e) { debugCatch(e, 'flushEpisode-immediateSave'); }
175
248
  }
176
249
 
177
- // Write episode to flush file, then remove buffer AFTER spawn to prevent race
178
250
  const flushFile = join(RUNTIME_DIR, `ep-flush-${Date.now()}-${randomUUID().slice(0, 8)}.json`);
179
251
  try {
180
- writeFileSync(flushFile, JSON.stringify(episode));
252
+ writeFileSync(flushFile, JSON.stringify(ep));
181
253
  } catch {
182
- return;
254
+ return 'writefail';
183
255
  }
184
256
 
185
- if (isSignificant) {
257
+ if (isSignificant && !process.env.CLAUDE_MEM_SKIP_EPISODE_LLM) {
186
258
  spawnBackground('llm-episode', flushFile);
187
-
188
- // v2.33.1: structured flush receipt so Claude sees what mem just captured
189
- // and the legacy error→fix nudge consolidates here. PostToolUse JSON with
190
- // hookSpecificOutput.additionalContext reliably renders across CC variants;
191
- // the old plain-text stdout write was invisible on some variants.
192
- // v2.33.4: Stop event rejects hookSpecificOutput entirely — skip receipt.
193
- if (RECEIPT_EVENTS.has(hookEventName)) {
194
- try {
195
- const entries = episode.entries || [];
196
- const toolCounts = {};
197
- for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
198
- const toolSummary = Object.entries(toolCounts)
199
- .sort((a, b) => b[1] - a[1])
200
- .slice(0, 3)
201
- .map(([t, n]) => `${t}×${n}`)
202
- .join(', ');
203
- const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
204
- // v2.83: error→fix nudge lifted to lib/cite-back-hint.mjs::buildUnsavedBugfixHint
205
- // so the wording (count + "Save now" verb) stays in sync with cite-back.
206
- const bugfixHint = buildUnsavedBugfixHint(episode);
207
- if (bugfixHint) lines.push(bugfixHint);
208
- // v2.81: cite-back hint — fires when this episode edits a file that
209
- // PreToolUse:Read/Edit nudged earlier in the same session. Precision
210
- // signal (we know the file was warned about); orthogonal to the
211
- // bugfix-shape nudge above and may co-fire.
212
- const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
213
- if (citeBack) lines.push(citeBack);
214
- // Trailing newline is REQUIRED: when this receipt flushes at SessionStart
215
- // (leftover episode after /clear or /compact), the startup dashboard writes a
216
- // second hookSpecificOutput object right after. Without the '\n' the two land
217
- // back-to-back as `}{` on one line and Claude Code's line-based JSON parser
218
- // drops both — losing the episode-flush / cite-back context exactly at the
219
- // session boundary. Every other hookSpecificOutput write appends '\n'; this
220
- // was the lone exception.
221
- process.stdout.write(JSON.stringify({
222
- suppressOutput: true,
223
- hookSpecificOutput: {
224
- hookEventName,
225
- additionalContext: lines.join('\n'),
226
- },
227
- }) + '\n');
228
- } catch { /* never block on receipt */ }
229
- }
230
259
  } else {
231
260
  try { unlinkSync(flushFile); } catch {}
232
261
  }
233
-
234
- // Remove episode buffer AFTER spawning background worker to prevent concurrent overwrites
235
- try { unlinkSync(episodeFile()); } catch {}
262
+ return isSignificant ? 'significant' : 'insignificant';
236
263
  }
237
264
 
238
265
  // ─── PostToolUse Handler ────────────────────────────────────────────────────
@@ -294,6 +321,10 @@ async function handlePostToolUse() {
294
321
  isSignificant: EDIT_TOOLS.has(tool_name) ||
295
322
  bashSig?.isSignificant || false,
296
323
  bashSig: bashSig || null,
324
+ // CC UUID from hook stdin — lets flushEpisode split a buffer shared by
325
+ // concurrent same-project sessions into per-session observations. Null for
326
+ // legacy/stdin-less invocations (→ single __none__ group = old behavior).
327
+ ccSession: hookData.session_id || null,
297
328
  };
298
329
 
299
330
  // Episode buffer management (locked to prevent TOCTOU race)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.35.1",
3
+ "version": "3.35.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",