claude-mem-lite 3.66.1 → 3.66.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.66.1",
13
+ "version": "3.66.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.66.1",
3
+ "version": "3.66.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/haiku-client.mjs CHANGED
@@ -428,26 +428,122 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
428
428
  }
429
429
  }
430
430
 
431
+ // ─── Headless CLI flag compatibility ─────────────────────────────────────────
432
+ //
433
+ // --no-session-persistence + DISABLE_CLAUDEMD_HOOKS (2026-08-16): these headless
434
+ // calls were paying the full interactive-session tax — 1,004 transcripts piled up
435
+ // in ~/.claude/projects/-tmp/, and every spawn ran the claudemd plugin's whole
436
+ // hook fan-out (its SessionStart banner alone logged 682 rows in 3 days, drowning
437
+ // that project's telemetry). The persistence flag is OAuth-safe (probed);
438
+ // `--bare`/CLAUDE_CODE_SIMPLE are NOT (they hard-require ANTHROPIC_API_KEY —
439
+ // "Not logged in" on OAuth machines). The user's global CLAUDE.md injection has
440
+ // no OAuth-safe opt-out; accepted (haiku + prompt caching keeps it cheap).
441
+ //
442
+ // The flag is an unguarded dependency on a recent Claude Code CLI: package.json
443
+ // declares only node>=20, no Claude Code floor. On an older binary the spawn dies
444
+ // in argument parsing, the catch swallows it, and every CLI-leg LLM call returns
445
+ // null — no retry, no telemetry. That leg is what the keyed providers degrade to,
446
+ // so such a user loses enrichment, summarization and optimize all at once. So:
447
+ // detect the arg-parse rejection, retry once without the flag, and cache the
448
+ // negative only AFTER the retry actually succeeded. Caching on the failure
449
+ // instead would let one transient non-zero exit that happens to mention the flag
450
+ // push a healthy CLI back onto the session tax for the rest of the process.
451
+ // Hooks are short-lived, so an old binary pays one extra fail-fast spawn per
452
+ // process; the long-lived MCP server pays it once per run.
453
+ const HEADLESS_FLAG = '--no-session-persistence';
454
+ let _headlessFlagOk = true;
455
+
456
+ /** @internal test hook — module-level compat state must not leak across cases. */
457
+ export function _resetHeadlessFlag() { _headlessFlagOk = true; }
458
+
459
+ function claudeArgs(modelName) {
460
+ return _headlessFlagOk
461
+ ? ['-p', '--model', modelName, HEADLESS_FLAG]
462
+ : ['-p', '--model', modelName];
463
+ }
464
+
465
+ // A retry is only ever worth it when the diagnostic NAMES the token it rejected —
466
+ // every argv parser does, and requiring it is what keeps this from firing on
467
+ // Claude Code's own config diagnostics. The installed CLI carries strings like
468
+ // `Skill X has invalid effort 'y'. Valid options: …` and `Input validation error:
469
+ // Invalid arguments for tool`, which an unanchored unknown-word/option-word regex
470
+ // matches outright. Those are emitted for a malformed agent/skill file — a
471
+ // *persistent* condition — so an unanchored match would fire on the next transient
472
+ // 529, permanently revert v3.66.0's session-tax fix on a perfectly healthy CLI,
473
+ // and log a WARN blaming a flag that was never the problem (pre-tag review, HIGH).
474
+ // Deliberately NOT keyed on exit code alone either: a non-zero exit is also the
475
+ // normal shape of an overload/auth failure.
476
+ const FLAG_TOKEN = /no-session-persistence/;
477
+ const PARSE_REJECTION = /(unknown|unrecognized|unsupported|invalid|unexpected)[^\n]{0,40}(option|argument|flag|switch)/i;
478
+
479
+ // Below this many ms left, a retry can only spawn a process and immediately kill
480
+ // it — worse than returning the original failure.
481
+ const RETRY_MIN_BUDGET_MS = 500;
482
+
483
+ export function _isUnknownFlagError(diagnostic) {
484
+ if (!diagnostic) return false;
485
+ return FLAG_TOKEN.test(diagnostic) && (PARSE_REJECTION.test(diagnostic) || /usage:/i.test(diagnostic));
486
+ }
487
+
488
+ // stdout as well as stderr: a parser that prints its rejection (or usage banner)
489
+ // on stdout is otherwise invisible here, and FLAG_TOKEN keeps the widened input
490
+ // from loosening the match.
491
+ function cliDiagnostic(e) {
492
+ const err = e?.stderr?.toString?.() || e?.output?.[2]?.toString?.() || '';
493
+ const out = e?.stdout?.toString?.() || e?.output?.[1]?.toString?.() || '';
494
+ return `${err}\n${out}`;
495
+ }
496
+
497
+ /**
498
+ * Shared blocking `claude -p` runner for every sync CLI leg (callModelCLI,
499
+ * callHaikuCLI, hook-shared#callLLM). Throws exactly what execFileSync throws so
500
+ * each caller keeps its own partial-output salvage; the only added behaviour is
501
+ * the one-shot flag-compat retry described above.
502
+ * @param {string} modelName CLI model name ('haiku'|'sonnet')
503
+ * @param {{input:string, timeout:number}} opts
504
+ * @returns {string} raw stdout
505
+ */
506
+ export function execClaudeCliSync(modelName, { input, timeout }) {
507
+ const opts = {
508
+ input,
509
+ timeout,
510
+ encoding: 'utf8',
511
+ env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
512
+ stdio: ['pipe', 'pipe', 'pipe'],
513
+ cwd: '/tmp', // Prevent ghost sessions in the user's /resume list
514
+ };
515
+ const args = claudeArgs(modelName);
516
+ const started = Date.now();
517
+ try {
518
+ return execFileSync(getClaudePath(), args, opts);
519
+ } catch (e) {
520
+ // A timeout is NOT a parse rejection. execFileSync kills the child and throws
521
+ // with its partial buffers attached (callModelCLI's salvage depends on exactly
522
+ // that), so without this guard a slow call whose output merely looked
523
+ // parse-shaped would be retried on the FULL original budget — doubling a
524
+ // latency-bound ceiling. lesson-bridge runs this leg at 2500ms on PreToolUse,
525
+ // where the CLI is measured at 8–13s and therefore times out routinely.
526
+ if (e?.killed || e?.signal) throw e;
527
+ // `args`, not the live flag: what matters is whether THIS attempt carried it.
528
+ // Symmetry with the async leg, where the distinction is load-bearing (a
529
+ // sibling can flip the flag across an await). Here execFileSync blocks the
530
+ // event loop for the whole child, so no other JS can interleave and the two
531
+ // readings are behaviourally identical — the substitution is deliberately
532
+ // mutation-silent, kept so the two legs cannot drift apart in meaning.
533
+ if (!args.includes(HEADLESS_FLAG) || !_isUnknownFlagError(cliDiagnostic(e))) throw e;
534
+ const remaining = timeout - (Date.now() - started);
535
+ if (remaining < RETRY_MIN_BUDGET_MS) throw e;
536
+ const out = execFileSync(getClaudePath(), ['-p', '--model', modelName], { ...opts, timeout: remaining });
537
+ _headlessFlagOk = false;
538
+ debugLog('WARN', 'cli-compat', `claude CLI rejected ${HEADLESS_FLAG}; dropped for this process (the headless session tax returns — upgrade Claude Code to avoid it)`);
539
+ return out;
540
+ }
541
+ }
542
+
431
543
  function callModelCLI(prompt, model, { timeout }) {
432
544
  const modelName = MODEL_MAP[model] ? model : 'haiku';
433
545
  try {
434
- // --no-session-persistence + DISABLE_CLAUDEMD_HOOKS (2026-08-16): these
435
- // headless calls were paying the full interactive-session tax — 1,004
436
- // transcripts piled up in ~/.claude/projects/-tmp/, and every spawn ran
437
- // the claudemd plugin's whole hook fan-out (its SessionStart banner alone
438
- // logged 682 rows in 3 days, drowning that project's telemetry). The
439
- // persistence flag is OAuth-safe (probed); `--bare`/CLAUDE_CODE_SIMPLE are
440
- // NOT (they hard-require ANTHROPIC_API_KEY — "Not logged in" on OAuth
441
- // machines). The user's global CLAUDE.md injection has no OAuth-safe
442
- // opt-out; accepted (haiku + prompt caching keeps it cheap).
443
- const result = execFileSync(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
444
- input: flattenForCLI(prompt),
445
- timeout,
446
- encoding: 'utf8',
447
- env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
448
- stdio: ['pipe', 'pipe', 'pipe'],
449
- cwd: '/tmp',
450
- });
546
+ const result = execClaudeCliSync(modelName, { input: flattenForCLI(prompt), timeout });
451
547
  const text = result.trim();
452
548
  return text ? { text } : null;
453
549
  } catch (e) {
@@ -479,23 +575,31 @@ function callModelCLI(prompt, model, { timeout }) {
479
575
  * @param {{timeout:number}} opts SIGKILL after `timeout` ms; no retry.
480
576
  * @returns {Promise<{text:string}|null>}
481
577
  */
482
- export function callModelCLIAsync(prompt, model, { timeout }) {
483
- return new Promise((resolve) => {
484
- const modelName = MODEL_MAP[model] ? model : 'haiku';
578
+ export async function callModelCLIAsync(prompt, model, { timeout }) {
579
+ const modelName = MODEL_MAP[model] ? model : 'haiku';
580
+ const payload = flattenForCLI(prompt);
581
+ const started = Date.now();
582
+
583
+ // One spawn. Resolves {result, stderr, stdout, code}, never rejects. `code` is
584
+ // a number ONLY when the child exited on its own; a timeout/SIGKILL or a spawn
585
+ // error reports null, which is what keeps either from being mistaken for an
586
+ // argument-parse rejection and costing a second full-budget spawn.
587
+ const attempt = (args, budget) => new Promise((resolve) => {
485
588
  let child;
486
589
  try {
487
- // Same headless-tax flags as callModelCLI (rationale there).
488
- child = spawn(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
590
+ // Same headless-tax flags + flag-compat retry as callModelCLI (rationale there).
591
+ child = spawn(getClaudePath(), args, {
489
592
  env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
490
593
  cwd: '/tmp',
491
594
  stdio: ['pipe', 'pipe', 'pipe'],
492
595
  });
493
596
  } catch (e) {
494
597
  debugCatch(e, `${model}-cli-async`);
495
- resolve(null);
598
+ resolve({ result: null, stderr: '', stdout: '', code: null });
496
599
  return;
497
600
  }
498
601
  let stdout = '';
602
+ let stderr = '';
499
603
  let settled = false;
500
604
  const done = (val) => {
501
605
  if (settled) return;
@@ -510,26 +614,59 @@ export function callModelCLIAsync(prompt, model, { timeout }) {
510
614
  // brace check would discard a complete-but-```json-fenced payload (#8605);
511
615
  // parseJsonFromLLM strips fences before validating, and the caller re-parses
512
616
  // the returned text the same way.
513
- if (t && parseJsonFromLLM(t) !== null) { done({ text: t }); return; }
514
- done(null);
515
- }, timeout);
617
+ if (t && parseJsonFromLLM(t) !== null) { done({ result: { text: t }, stderr, stdout, code: null }); return; }
618
+ done({ result: null, stderr, stdout, code: null });
619
+ }, budget);
516
620
  child.stdout?.setEncoding('utf8'); // decode multi-byte UTF-8 (CJK) across chunk boundaries
517
621
  child.stdout?.on('data', (d) => { stdout += d; });
518
- child.stderr?.on('data', () => {}); // drain stderr so a chatty child can't block on a full pipe
519
- child.on('error', (e) => { debugCatch(e, `${model}-cli-async`); done(null); });
520
- child.on('close', () => {
622
+ // Keep draining stderr so a chatty child can't block on a full pipe, but keep
623
+ // a bounded head of it — the flag-compat probe needs the parser's complaint.
624
+ // Slice AFTER appending: checking the length first lets one arbitrarily large
625
+ // chunk through whole, which is the shape a single big stderr write takes.
626
+ child.stderr?.setEncoding?.('utf8');
627
+ child.stderr?.on('data', (d) => { stderr = (stderr + d).slice(0, 4096); });
628
+ child.on('error', (e) => { debugCatch(e, `${model}-cli-async`); done({ result: null, stderr: '', stdout: '', code: null }); });
629
+ child.on('close', (code) => {
521
630
  const t = stdout.trim();
522
- done(t ? { text: t } : null);
631
+ done({ result: t ? { text: t } : null, stderr, stdout, code });
523
632
  });
524
633
  // EPIPE guard: the child may exit before we finish writing stdin.
525
634
  child.stdin?.on('error', () => {});
526
635
  try {
527
- child.stdin?.write(flattenForCLI(prompt));
636
+ child.stdin?.write(payload);
528
637
  child.stdin?.end();
529
638
  } catch (e) {
530
639
  debugCatch(e, `${model}-cli-async:stdin`);
531
640
  }
532
641
  });
642
+
643
+ const firstArgs = claudeArgs(modelName);
644
+ const first = await attempt(firstArgs, timeout);
645
+ // Judged on `firstArgs`, not the live flag: a concurrent sibling may have
646
+ // flipped it between our spawn and our resume, and reading the global there
647
+ // would silently deny THIS call the retry it earned (MCP server, concurrent
648
+ // deep-search escalations). Gating on the exit code before `first.result` also
649
+ // covers a CLI that prints its usage banner to stdout and exits non-zero —
650
+ // otherwise that banner is returned as the model's answer and nothing retries.
651
+ const rejected = firstArgs.includes(HEADLESS_FLAG)
652
+ && typeof first.code === 'number' && first.code !== 0
653
+ && _isUnknownFlagError(`${first.stderr}\n${first.stdout.slice(0, 4096)}`);
654
+ if (!rejected) return first.result;
655
+ // The rejection is instantaneous (the child dies in argv parsing), so the retry
656
+ // normally gets nearly the whole budget; spend only what is left of it.
657
+ const remaining = timeout - (Date.now() - started);
658
+ if (remaining < RETRY_MIN_BUDGET_MS) return first.result;
659
+ const second = await attempt(['-p', '--model', modelName], remaining);
660
+ // Cache on the retry's EXIT, not its payload. Empty output is a designed
661
+ // outcome here (emit-nothing prompts, an `N/A` that trims away), so keying on
662
+ // text left the long-lived MCP server re-probing — two spawns per call, for the
663
+ // life of the process — on exactly the old CLI this exists to rescue. The sync
664
+ // twin caches on any non-throwing run; this now means the same thing.
665
+ if (second.code === 0) {
666
+ _headlessFlagOk = false;
667
+ debugLog('WARN', `${model}-cli-async`, `claude CLI rejected ${HEADLESS_FLAG}; dropped for this process (the headless session tax returns — upgrade Claude Code to avoid it)`);
668
+ }
669
+ return second.result;
533
670
  }
534
671
 
535
672
  // ─── API Mode ────────────────────────────────────────────────────────────────
@@ -635,15 +772,8 @@ async function callOpenRouterAPI(prompt, tier, { timeout, maxTokens, temperature
635
772
  function callHaikuCLI(prompt, { timeout }) {
636
773
  const { cli: modelName } = resolveModel();
637
774
  try {
638
- // Same headless-tax flags as callModelCLI (rationale there).
639
- const result = execFileSync(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
640
- input: flattenForCLI(prompt),
641
- timeout,
642
- encoding: 'utf8',
643
- env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
644
- stdio: ['pipe', 'pipe', 'pipe'],
645
- cwd: '/tmp', // Prevent ghost sessions in user's /resume list
646
- });
775
+ // Same headless-tax flags + flag-compat retry as callModelCLI (rationale there).
776
+ const result = execClaudeCliSync(modelName, { input: flattenForCLI(prompt), timeout });
647
777
  const text = result.trim();
648
778
  return text ? { text } : null;
649
779
  } catch (e) {
package/hook-shared.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // claude-mem-lite: Shared infrastructure for hook.mjs and hook-llm.mjs
2
2
  // Constants, session management, DB access, LLM calls, process utilities
3
3
 
4
- import { execFileSync, spawn } from 'child_process';
4
+ import { spawn } from 'child_process';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { join } from 'path';
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync, statSync, unlinkSync, chmodSync } from 'fs';
@@ -10,7 +10,7 @@ import { ensureDbWithWalRecovery, DB_DIR } from './schema.mjs';
10
10
  // Pure-`node:`/local module (it imports only binding-probe + native-binding-hint, and
11
11
  // neither imports this file) — no cycle.
12
12
  import { recordHookError } from './lib/hook-telemetry.mjs';
13
- import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI, detectMode as detectLLMMode, callHaiku, BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
13
+ import { execClaudeCliSync, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI, detectMode as detectLLMMode, callHaiku, BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
14
14
  // Phase D: invited-memory sentinel detection. memdir.mjs/claudemd.mjs only pull in
15
15
  // fs/path/os/crypto; adopt-content.mjs is pure strings. No circular deps —
16
16
  // neither imports hook-shared.
@@ -318,16 +318,10 @@ export async function callLLM(prompt, timeoutMs = BG_LLM_TIMEOUT_MS) {
318
318
 
319
319
  const { cli: modelName } = resolveModelShared();
320
320
  try {
321
- // Same headless-tax flags as haiku-client.mjs#callModelCLI (rationale
322
- // there): no transcript persistence, no claudemd hook fan-out.
323
- const result = execFileSync(getClaudePathShared(), ['-p', '--model', modelName, '--no-session-persistence'], {
324
- input: _flattenForCLI(prompt),
325
- timeout: timeoutMs,
326
- encoding: 'utf8',
327
- env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
328
- stdio: ['pipe', 'pipe', 'pipe'],
329
- cwd: '/tmp', // Prevent ghost sessions in user's /resume list
330
- });
321
+ // Shared runner with haiku-client.mjs#callModelCLI (rationale there): no
322
+ // transcript persistence, no claudemd hook fan-out, and the one-shot
323
+ // retry-without-flag that keeps this leg alive on an older Claude Code CLI.
324
+ const result = execClaudeCliSync(modelName, { input: _flattenForCLI(prompt), timeout: timeoutMs });
331
325
  return result.trim();
332
326
  } catch (e) {
333
327
  const out = _extractResponseFromError(e);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.66.1",
3
+ "version": "3.66.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.66.1",
9
+ "version": "3.66.2",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.26.0",
12
12
  "better-sqlite3": "^12.6.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.66.1",
3
+ "version": "3.66.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",