claude-mem-lite 6.19.2 → 6.19.4

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.19.2",
12
+ "version": "6.19.4",
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. FTS5 BM25 keyword 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.19.2",
3
+ "version": "6.19.4",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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-shared.mjs CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  } from './lib/schema-skew.mjs';
31
31
  import { isDbUnusableError, DB_UNUSABLE_MARKER_PREFIX } from './lib/db-unusable.mjs';
32
32
  import { shouldRecordOnce } from './lib/record-once.mjs';
33
+ import { hookSessionId } from './lib/provenance.mjs';
33
34
  // Audit 2026-09-05 P1-2 (carried from 2026-09-02 P2-9): `callLLM`, the quiet/adoption
34
35
  // predicates and the handoff constants moved into `lib/` because two lib modules
35
36
  // imported them from here and dragged this file's whole import graph — haiku-client,
@@ -385,7 +386,7 @@ export function getSessionId() {
385
386
 
386
387
  export function createSessionId() {
387
388
  const project = inferProject();
388
- const id = `hook-${project}-${randomUUID().slice(0, 8)}`;
389
+ const id = hookSessionId(project, randomUUID().slice(0, 8));
389
390
  const file = sessionFile();
390
391
  const tmp = file + `.tmp-${process.pid}`;
391
392
  writeFileSync(tmp, JSON.stringify({ id, startedAt: Date.now(), project }), { mode: 0o600 });
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { TIER_CASE_SQL, tierSqlParams } from '../tier.mjs';
8
8
  import { liveObsFilterSql } from './inject-search-core.mjs';
9
+ import { HOOK_SESSION_ID_PREFIX } from './provenance.mjs';
9
10
 
10
11
  export const BROWSE_TIERS = ['working', 'active', 'archive'];
11
12
  export const BROWSE_TIER_LABELS = {
@@ -14,14 +15,23 @@ export const BROWSE_TIER_LABELS = {
14
15
  archive: '🔵 Archive',
15
16
  };
16
17
 
17
- /** Newest active memory_session_id for the project ('' when none) — the tier
18
- * classifier's "current session" input, needed identically by both faces. */
18
+ /** Newest active hook session id for the project ('' when none) — the tier classifier's
19
+ * "current session" input, needed identically by both faces. Only the hook's own sessions
20
+ * count: a writer's row (`manual-`, `compress-`, …) is inserted active on that writer's first
21
+ * write in a project and stays active until the 24h sweep, so taken as the current session it
22
+ * put all of that writer's rows, of any age, in the working tier (D#153).
23
+ * From a session's first Stop its own row is no longer active (Stop marks it completed on every
24
+ * turn and nothing marks it active again), so this is '' or another hook session of the project
25
+ * still marked active (a parallel one, or one that ended without a Stop); tier rule 2 holds for
26
+ * the session in progress only before its first reply, and rules 3-4 carry its rows of the last
27
+ * 2 hours. Accepted (D#161): measured 2026-09-28, 1 row in this machine's database was one
28
+ * rule 2 alone would have kept in the working tier; hook sessions had written 12 of 238 rows. */
19
29
  export function getActiveMemorySessionId(db, project) {
20
30
  const row = db
21
31
  .prepare(
22
- "SELECT memory_session_id FROM sdk_sessions WHERE project = ? AND status = 'active' ORDER BY started_at_epoch DESC LIMIT 1",
32
+ "SELECT memory_session_id FROM sdk_sessions WHERE project = ? AND status = 'active' AND memory_session_id LIKE ? ORDER BY started_at_epoch DESC LIMIT 1",
23
33
  )
24
- .get(project);
34
+ .get(project, `${HOOK_SESSION_ID_PREFIX}%`);
25
35
  return row?.memory_session_id ?? '';
26
36
  }
27
37
 
@@ -11,8 +11,9 @@
11
11
  // twin-drift class permanently.
12
12
  //
13
13
  // Full round-trippable set: content + value-signals (access/cited/uncited/injection/decay)
14
- // + branch + timing. `id` + `memory_session_id` are informational (restore remaps id and
15
- // buckets under a synthetic restore session). Session-idempotency keys (last_decided/
14
+ // + branch + timing. `id` + `memory_session_id` are informational: restore remaps id, and of the
15
+ // session id it keeps only whether the row was an explicit save (`manual-<project>`) or not
16
+ // (`restore-<project>`; see cmdRestore). Session-idempotency keys (last_decided/
16
17
  // last_cited/last_access_session_id, demoted_at, optimized_at) are intentionally NOT
17
18
  // exported — they are meaningless after a row is re-bucketed under a restore session.
18
19
  // last_access_session_id (v48) joins that set for the same reason: it answers "did THIS cc
@@ -1,8 +1,9 @@
1
1
  // Which writer produced an observation, read back from its memory_session_id. saveObservation
2
2
  // writes every explicit save under MANUAL_SESSION_ID_PREFIX. The hook capture (`hook-`),
3
3
  // transcript import (`import-`), compression summaries and cluster-merge keepers (`compress-`),
4
- // rows narrow re-enrich rewrote (`enrich-`), promoted events (`promote-`) and rows imported from
5
- // older stores (a bare session uuid) are machine-written.
4
+ // rows narrow re-enrich rewrote (`enrich-`), promoted events (`promote-`), machine-written rows
5
+ // `restore` brought back from a backup (`restore-`) and rows imported from older stores (a bare
6
+ // session uuid) are machine-written.
6
7
  //
7
8
  // Search and get mark the machine-written side, because explicit saves are the large majority
8
9
  // of a typical store and a mark on nearly every line would carry no information. An unknown id
@@ -14,14 +15,22 @@ export const MANUAL_SESSION_ID_PREFIX = 'manual-';
14
15
  // uuid has them (schema.mjs, sdk_sessions_id_mix_check_ai). A writer row uses `<prefix><project>`
15
16
  // for both, which has that shape for one project length and dash layout (27 characters under
16
17
  // `compress-`, 29 under `manual-`), and the refusal failed every write in such a project (D#147).
17
- // That id gets one more character; every other id is unchanged. Counted in code points, as
18
- // SQLite's length() and LIKE's `_` count them.
18
+ // The hook's own id, `hook-<project>-<8 hex>`, has it for a 22-character project with dashes at
19
+ // 3, 8, 13 and 18 (D#156). That id gets one more character; every other id is unchanged. Counted
20
+ // in code points, as SQLite's length() and LIKE's `_` count them.
19
21
  const UUID_SHAPED_RE = /^[\s\S]{8}-[\s\S]{4}-[\s\S]{4}-[\s\S]{4}-[\s\S]{12}$/u;
22
+ const outOfUuidShape = (id) => (UUID_SHAPED_RE.test(id) ? `${id}~` : id);
20
23
 
21
- /** The session id a non-hook writer (`manual-`, `compress-`, `enrich-`, `promote-`) stores for a project. */
24
+ /** The session id a non-hook writer (`manual-`, `compress-`, `enrich-`, `promote-`, `restore-`) stores for a project. */
22
25
  export function writerSessionId(prefix, project) {
23
- const id = `${prefix}${project}`;
24
- return UUID_SHAPED_RE.test(id) ? `${id}~` : id;
26
+ return outOfUuidShape(`${prefix}${project}`);
27
+ }
28
+
29
+ export const HOOK_SESSION_ID_PREFIX = 'hook-';
30
+
31
+ /** The hook's id for one session in a project: `hook-<project>-<tail>`. */
32
+ export function hookSessionId(project, tail) {
33
+ return outOfUuidShape(`${HOOK_SESSION_ID_PREFIX}${project}-${tail}`);
25
34
  }
26
35
 
27
36
  const AUTO_MARK = '🤖';
package/mem-cli.mjs CHANGED
@@ -25,7 +25,7 @@ import { resolveProject } from './project-utils.mjs';
25
25
  import { resolveCliProject as cliProject } from './lib/cli-project.mjs';
26
26
  import { reRankWithContext } from './search-scoring.mjs';
27
27
  import { searchObservationsHybrid } from './search-engine.mjs';
28
- import { autoHeaderNote, autoLegend, autoTag } from './lib/provenance.mjs';
28
+ import { autoHeaderNote, autoLegend, autoTag, isAutoWritten, writerSessionId } from './lib/provenance.mjs';
29
29
  import {
30
30
  fetchObsDetail,
31
31
  fetchPromptDetail,
@@ -2268,8 +2268,18 @@ function cmdExport(db, args) {
2268
2268
  // (access/cited/uncited/injection/decay), branch, and concepts/facts/files_read that
2269
2269
  // saveObservation derives or zeros — so a restored backup keeps its citation-decay
2270
2270
  // history and original timing (created_at via the `now` param). Source ids are
2271
- // discarded (local AUTOINCREMENT; export omits related_ids); session provenance
2272
- // collapses to saveObservation's manual-<project> bucket (documented MVP tradeoff).
2271
+ // discarded (local AUTOINCREMENT; export omits related_ids). Session ids are not restored,
2272
+ // only whether a row was an explicit save: see RESTORE_SESSION_ID_PREFIX.
2273
+ // A machine-written row (any exported id but `manual-`) is restored under `restore-<project>`, so
2274
+ // lib/provenance.mjs still reads it as auto-written; before D#157 every row went under
2275
+ // `manual-<project>` and rendered as an explicit save. The exported id is not reused: saveObservation
2276
+ // stores it as both session ids of an active session row, and a bare session uuid
2277
+ // (rows imported from older stores) is the shape sdk_sessions refuses, a `hook-` id could become
2278
+ // browse's current session (the only active hook row once Stop has marked the real one completed)
2279
+ // until a SessionStart sweeps it, and under --project the id names another project. A row exported
2280
+ // without the column restores as an explicit save, as before.
2281
+ const RESTORE_SESSION_ID_PREFIX = 'restore-';
2282
+
2273
2283
  function cmdRestore(db, argv) {
2274
2284
  const { positional, flags } = parseArgs(argv);
2275
2285
  const file = positional[0];
@@ -2396,6 +2406,9 @@ function cmdRestore(db, argv) {
2396
2406
  files,
2397
2407
  lesson_learned: r.lesson_learned || null,
2398
2408
  now: new Date(createdEpoch),
2409
+ sessionId: isAutoWritten(r.memory_session_id)
2410
+ ? writerSessionId(RESTORE_SESSION_ID_PREFIX, project)
2411
+ : undefined,
2399
2412
  });
2400
2413
  if (res.kind !== 'saved') {
2401
2414
  skipped++;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.19.2",
3
+ "version": "6.19.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "6.19.2",
9
+ "version": "6.19.4",
10
10
  "os": [
11
11
  "darwin",
12
12
  "linux",
@@ -2625,9 +2625,9 @@
2625
2625
  "license": "ISC"
2626
2626
  },
2627
2627
  "node_modules/ip-address": {
2628
- "version": "10.5.0",
2629
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
2630
- "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
2628
+ "version": "10.7.2",
2629
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz",
2630
+ "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==",
2631
2631
  "license": "MIT",
2632
2632
  "engines": {
2633
2633
  "node": ">= 12"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.19.2",
3
+ "version": "6.19.4",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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",
@@ -230,7 +230,7 @@
230
230
  "@hono/node-server": ">=2.0.11",
231
231
  "body-parser": ">=2.3.0",
232
232
  "fast-uri": ">=3.1.4 <4",
233
- "ip-address": ">=10.1.1",
233
+ "ip-address": ">=10.6.0",
234
234
  "brace-expansion": ">=5.0.9"
235
235
  },
236
236
  "devDependencies": {
package/secret-scrub.mjs CHANGED
@@ -197,12 +197,19 @@ export const SECRET_PATTERNS = [
197
197
  ],
198
198
  // PEM private key blocks. `[A-Z0-9 ]*` covers every armor label — RSA/EC/DSA/
199
199
  // OPENSSH plus ENCRYPTED and PGP (… PRIVATE KEY BLOCK) — that the fixed
200
- // alternation missed; the block delimiters make FP impossible.
200
+ // alternation missed.
201
201
  // The body stops at the next `-----BEGIN ` (D#130): with `[\s\S]*?` every header with no END
202
202
  // scanned to the end of the text, on each of scrubSecrets' passes — quadratic, 8.3 s on 500k
203
203
  // chars. A block whose END is missing is left to the next pattern. Nor does it cross a mark this
204
204
  // scrubber wrote: on a later pass, a BEGIN that a scrubbed key used to block reached a far END
205
205
  // and erased the prose between (v6.19.2 pre-tag defect review F5).
206
+ // Text that names a BEGIN and, later, an END with no key between them loses the text between
207
+ // (D#155, accepted). Keeping such a block was tried for 6.19.3 and withdrawn before the tag: each
208
+ // of the three keep rules measured stored keys this pattern erases, either a key it took for text
209
+ // or a key tail the kept markers hid from scrubKeyTails (docs/audits/20260928-v6.19.3-pretag-*.md).
210
+ // On this machine (2026-09-28) all 967 such blocks in 1,064 transcripts came from this
211
+ // repository's scrubber work; the other projects' transcripts held 6 BEGIN markers and no END.
212
+ // That shows the shape is rare here, not that text about PEM markers elsewhere keeps its words.
206
213
  [
207
214
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----(?:(?!-----BEGIN [A-Z0-9 ]*PRIVATE KEY|\*\*\*PEM_KEY\*\*\*)[\s\S])*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g,
208
215
  '***PEM_KEY***',
@@ -406,6 +413,16 @@ const ARMOR = 4;
406
413
  const CUT = 5;
407
414
  const OTHER = 6;
408
415
 
416
+ // Character-class checks by code, not a regex per character. (What brought the many-BEGINs linearity
417
+ // shape back under its budget after the v6.19.2 CI run was cutOffKeyEnd stopping before another
418
+ // BEGIN line, not these checks: under coverage the shape measured 5.6-6.2x benign with these checks
419
+ // alone, 1.9-2.3x with the stop alone.)
420
+ function isB64Code(c) {
421
+ return (
422
+ (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 43 || c === 47 || c === 61
423
+ );
424
+ }
425
+
409
426
  function backslashesBefore(text, i, floor) {
410
427
  let j = i;
411
428
  while (j > floor && text[j - 1] === '\\') j--;
@@ -508,7 +525,7 @@ function lineCore(text, s, e, shape) {
508
525
  // (`</key>`), and escaped breaks before it. Read from the end: an unanchored
509
526
  // `(?:\\+[rn])*["']…$` retried every start in a backslash run.
510
527
  let q = b;
511
- while (q > a && /[ \t+,;)\]}]/.test(line[q - 1])) q--;
528
+ while (q > a && ' \t+,;)]}'.includes(line[q - 1])) q--;
512
529
  const tag = q > a && line[q - 1] === '>' ? /<\/[A-Za-z][\w:.-]{0,40}>$/.exec(line.slice(a, q)) : null;
513
530
  if (tag) b = q - tag[0].length;
514
531
  else if (q > a && (line[q - 1] === '"' || line[q - 1] === "'" || line[q - 1] === '`')) {
@@ -559,7 +576,11 @@ function cutRun(core) {
559
576
  function cutOffKeyEnd(text, b, be) {
560
577
  // The rest of the BEGIN line: nothing, or base64 chunks, then a break that sets the depth.
561
578
  let i = be;
562
- while (i < text.length && /[A-Za-z0-9+/= \t]/.test(text[i])) i++;
579
+ for (
580
+ let c = text.charCodeAt(i);
581
+ i < text.length && (isB64Code(c) || c === 32 || c === 9);
582
+ c = text.charCodeAt(++i)
583
+ );
563
584
  const rest = text.slice(be, i).trim();
564
585
  let u;
565
586
  let next;
@@ -608,13 +629,15 @@ function cutOffKeyEnd(text, b, be) {
608
629
  // for one, it stripped this key's identical body line and one copy of a repeated cut key went
609
630
  // per pass (v6.19.2 pre-tag round-5 review F2).
610
631
  let run = 0;
611
- while (run < 16 && b - run > ls && /[A-Za-z0-9+/=]/.test(text[b - run - 1])) run++;
632
+ while (run < 16 && b - run > ls && isB64Code(text.charCodeAt(b - run - 1))) run++;
612
633
  const shape = run >= 16 ? null : prefixShape(text.slice(ls, b));
613
634
 
614
635
  let armors = 0;
615
636
  let shorts = 0;
616
637
  let pendingBlank = false;
617
638
  while (next !== -1) {
639
+ // Another BEGIN line is never part of this key; stop before reading it whole.
640
+ if (!shape && text.startsWith('-----BEGIN ', next)) break;
618
641
  const line = nextLine(text, next, u);
619
642
  const { core, start, end: coreEnd } = lineCore(text, next, line.end, shape);
620
643
  const kind = classify(core);