claude-mem-lite 6.26.0 → 6.26.1

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.26.0",
12
+ "version": "6.26.1",
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.26.0",
3
+ "version": "6.26.1",
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/haiku-client.mjs CHANGED
@@ -69,16 +69,37 @@ const MODEL_MAP = {
69
69
 
70
70
  // Models that get the STRICT body. Claude Haiku 5.5 answers any non-default `temperature`
71
71
  // with a 400 and thinks unless told not to, so a reply can open with a thinking block, and
72
- // thinking spends the max_tokens these short fixed-schema extractions are sized for. Every
73
- // leg runs it with thinking off: the CLI sets MAX_THINKING_TOKENS=0, OpenRouter gets
74
- // `reasoning: {enabled: false}` (STRICT_BODY_SLUGS), and the direct leg sends no sampling
75
- // field and thinking disabled. With thinking off it can answer a bare one-word query with a
72
+ // thinking spends the max_tokens these short fixed-schema extractions are sized for. The
73
+ // keyed legs run it with thinking off: OpenRouter gets `reasoning: {enabled: false}`
74
+ // (STRICT_BODY_SLUGS), and the direct leg sends no sampling field and thinking disabled.
75
+ // The CLI leg does NOT: Claude Code 2.1.301 sends `thinking: {type: "adaptive"}` and effort
76
+ // medium for `--model haiku` whether or not MAX_THINKING_TOKENS=0 is set (request bodies
77
+ // captured at a stub base URL, 2026-10-15; D#334). Its max_tokens is Claude Code's own, so
78
+ // nothing is cut off there. With thinking off it can answer a bare one-word query with a
76
79
  // prose question instead of JSON, so a caller's `jsonSchema` becomes a structured output on
77
80
  // these models (rewrite replays 2026-10-15: 7/72 below two variants without it, 0/72 with
78
- // it; docs/audits/20261015-v6.26.0-replays.md). Keyed by the ID actually sent, so an
79
- // override naming another model keeps the old body.
81
+ // it; docs/audits/20261015-v6.26.0-replays.md). Keyed by the ID actually sent, read through
82
+ // strictBodyKey, so an override naming another model keeps the old body.
80
83
  const STRICT_BODY_MODELS = new Set(['claude-haiku-5-5']);
81
84
 
85
+ /**
86
+ * The model's own name inside the routing decoration around it, for the STRICT_BODY_*
87
+ * lookups: an OpenRouter variant suffix (`anthropic/claude-haiku-5.5:nitro`, `:floor`) and
88
+ * the Bedrock form a gateway may take (`anthropic.claude-haiku-5-5`, with or without a
89
+ * cross-region prefix such as `us.`, `global.` or GovCloud's `us-gov.`). Haiku 5.5 named
90
+ * either way was sent the old body: thinking on, no structured output and, on the direct
91
+ * leg, temperature 0, which it answers with a 400 (OpenRouter drops the field in transit).
92
+ * A gateway deployment name says nothing about the model behind it and is not guessed at.
93
+ * (D#321)
94
+ * @param {string} id model ID or OpenRouter slug as sent
95
+ * @returns {string}
96
+ */
97
+ function strictBodyKey(id) {
98
+ return String(id)
99
+ .replace(/:[a-z0-9-]+$/, '')
100
+ .replace(/^(?:[a-z]{2,6}(?:-[a-z]+)?\.)?anthropic\./, '');
101
+ }
102
+
82
103
  // Claude Code's tier-alias override vars, honored on the direct-API leg too:
83
104
  // gateway deployments route on the DEPLOYMENT name (Azure Foundry rejects the
84
105
  // Anthropic model ID when it differs), and one env set then covers both the
@@ -582,7 +603,7 @@ async function callModelAPI(
582
603
  // "`temperature` is deprecated for this model."). Keep sending it until a
583
604
  // model refuses, then drop it for that model only - the retry at the bottom
584
605
  // pays for itself once. A model known to refuse it never gets it.
585
- if (STRICT_BODY_MODELS.has(modelId)) {
606
+ if (STRICT_BODY_MODELS.has(strictBodyKey(modelId))) {
586
607
  body.thinking = { type: 'disabled' };
587
608
  if (jsonSchema) body.output_config = { format: { type: 'json_schema', schema: jsonSchema.schema } };
588
609
  } else if (!_temperatureDeprecated.has(modelId)) body.temperature = temperature;
@@ -680,10 +701,22 @@ async function callModelAPI(
680
701
  debugLog('WARN', `${model}-api`, `refusal (${data.stop_details?.category ?? 'no category'})`);
681
702
  return null;
682
703
  }
683
- // By content, not position: a thinking block (no `text`) can come first.
704
+ // The callers parse a fixed-schema JSON answer out of this text (the opt-in lesson bridge
705
+ // keeps its first line), so a reply cut off at max_tokens is a failed call too: the CLI
706
+ // fallback has no such cap. A whole JSON answer followed by cut-off prose is discarded with
707
+ // it, although parseJsonFromLLM would have recovered it (D#322; D#338).
708
+ if (data.stop_reason === 'max_tokens') {
709
+ debugLog('WARN', `${model}-api`, 'reply cut off at max_tokens');
710
+ return null;
711
+ }
712
+ // By content, not position: a thinking block (no `text`) can come first. Every text
713
+ // block, joined, so an empty one cannot hide the answer after it (D#324).
684
714
  const text = Array.isArray(data.content)
685
- ? data.content.find((b) => typeof b?.text === 'string')?.text
686
- : undefined;
715
+ ? data.content
716
+ .filter((b) => typeof b?.text === 'string')
717
+ .map((b) => b.text)
718
+ .join('')
719
+ : '';
687
720
  return text ? { text } : null;
688
721
  } finally {
689
722
  clearTimeout(timer);
@@ -757,7 +790,10 @@ function claudeArgs(modelName) {
757
790
  /**
758
791
  * The environment every headless spawn runs with. MAX_THINKING_TOKENS=0: these are
759
792
  * structured extraction calls, and the thinking the CLI enables by default was most of
760
- * their output tokens and latency (measured above).
793
+ * their output tokens and latency (measured above, 2026-09-29; the model the CLI ran then was
794
+ * not recorded). Claude Code 2.1.301 does not change its request for Claude Haiku 5.5 on this
795
+ * variable: the body carries adaptive thinking either way (D#334). Earlier versions were not
796
+ * checked.
761
797
  */
762
798
  function cliSpawnEnv() {
763
799
  return {
@@ -1095,8 +1131,15 @@ async function callOpenRouterAPI(
1095
1131
  // Optional OpenRouter attribution headers (ignored by the API if absent).
1096
1132
  'X-Title': 'claude-mem-lite',
1097
1133
  };
1098
- const payload = { model, max_tokens: maxTokens, temperature, messages };
1099
- if (STRICT_BODY_SLUGS.has(model)) {
1134
+ // A strict slug gets no temperature, as on the direct leg. No OpenRouter endpoint serving
1135
+ // Claude Haiku 5.5 lists it (endpoints API, 2026-10-15), so it was dropped in transit,
1136
+ // and it made `provider.require_parameters` match no endpoint at all (72/72 HTTP 404 on
1137
+ // the rewrite prompts, 0/72 without it; docs/audits/20261015-d330-openrouter-routing.md).
1138
+ const strict = STRICT_BODY_SLUGS.has(strictBodyKey(model));
1139
+ const payload = strict
1140
+ ? { model, max_tokens: maxTokens, messages }
1141
+ : { model, max_tokens: maxTokens, temperature, messages };
1142
+ if (strict) {
1100
1143
  payload.reasoning = { enabled: false };
1101
1144
  if (jsonSchema) {
1102
1145
  payload.response_format = {
@@ -1119,7 +1162,27 @@ async function callOpenRouterAPI(
1119
1162
  }
1120
1163
 
1121
1164
  const data = await res.json();
1122
- const text = data.choices?.[0]?.message?.content;
1165
+ const choice = data.choices?.[0];
1166
+ // OpenRouter normalises finish_reason to stop / length / content_filter / tool_calls /
1167
+ // error and passes the provider's own value as native_finish_reason. A cut-off, filtered
1168
+ // or refused reply can still carry part of an answer; none is one. (D#322, D#323)
1169
+ const finish = choice?.finish_reason;
1170
+ const native = choice?.native_finish_reason;
1171
+ if (
1172
+ finish === 'length' ||
1173
+ finish === 'content_filter' ||
1174
+ finish === 'error' ||
1175
+ native === 'refusal' ||
1176
+ choice?.error
1177
+ ) {
1178
+ debugLog(
1179
+ 'WARN',
1180
+ `${tier}-openrouter`,
1181
+ `no answer (finish_reason ${finish}, native ${native ?? 'none'})`,
1182
+ );
1183
+ return null;
1184
+ }
1185
+ const text = choice?.message?.content;
1123
1186
  return text ? { text } : null;
1124
1187
  } finally {
1125
1188
  clearTimeout(timer);
package/hook.mjs CHANGED
@@ -128,8 +128,7 @@ import {
128
128
  import { writePlainHookText, resetPlainHookText, idsShownWhole } from './lib/hook-text-cap.mjs';
129
129
  import { shouldRecallOnFailure } from './lib/tool-refusal.mjs';
130
130
  import {
131
- entryInputTags,
132
- extractDiagnosis,
131
+ workerEntryFields,
133
132
  filterSummaryInput,
134
133
  episodeInputFilterEnabled,
135
134
  } from './lib/episode-input-filter.mjs';
@@ -781,28 +780,16 @@ async function handlePostToolUse() {
781
780
  // Tier 1 B: Detect significant Bash commands
782
781
  const bashSig = tool_name === 'Bash' ? detectBashSignificance(toolInput, resp) : null;
783
782
 
784
- // D#69: tags and diagnosis lines need the FULL command and output, which the entry
785
- // does not keep, so they are computed here once. The output is windowed head + tail:
786
- // a runner prints its failures at the END, and a multi-megabyte stdout should not be
787
- // regex-scanned on every PostToolUse.
788
- const respWindow = resp.length > 65536 ? resp.slice(0, 32768) + '\n' + resp.slice(-32768) : resp;
789
-
790
- // `diagOut` = the diagnosis lines read from tool OUTPUT: a lesson quoting one stays
791
- // under every injection floor (D#100(3)). Always present on a Bash entry, [] included, so
792
- // the worker can tell a new entry with no output lines from one buffered before the field.
793
- const diagnosis = extractDiagnosis(tool_name, toolInput, respWindow, {
794
- isError: bashSig?.isError || false,
795
- writesFiles: tool_name === 'Bash' && bashWrites.length > 0,
796
- scrub: scrubSecrets,
797
- });
798
-
799
- // Build episode entry
783
+ // Build episode entry. The worker fields (input tags, DIAGNOSIS lines, Bash's diagOut)
784
+ // need the full command and output, which the entry does not keep (D#69).
800
785
  const entry = {
801
786
  tool: tool_name,
802
787
  desc: scrubSecrets(makeEntryDesc(tool_name, toolInput, resp, bashSig)),
803
- inputTags: entryInputTags(tool_name, toolInput, respWindow),
804
- diag: diagnosis.lines,
805
- ...(tool_name === 'Bash' ? { diagOut: diagnosis.output } : {}),
788
+ ...workerEntryFields(tool_name, toolInput, resp, {
789
+ isError: bashSig?.isError || false,
790
+ writesFiles: tool_name === 'Bash' && bashWrites.length > 0,
791
+ scrub: scrubSecrets,
792
+ }),
806
793
  files,
807
794
  ...(tool_name === 'Bash' && bashWrites.length ? { bashWrites } : {}),
808
795
  ts: Date.now(),
@@ -386,6 +386,33 @@ export function extractDiagnosis(
386
386
  return { lines: [...clean(lines), ...out], output: out };
387
387
  }
388
388
 
389
+ /**
390
+ * The fields an episode entry carries for the llm-episode worker: the input filter's tags,
391
+ * the prompt's DIAGNOSIS lines and, on Bash, `diagOut`, the lines read from tool OUTPUT (a
392
+ * lesson quoting one stays under every injection floor, D#100(3); always present on a Bash
393
+ * entry, [] included, so the worker can tell a new entry with no output lines from one
394
+ * buffered before the field). They need the FULL command and output, which the entry does
395
+ * not keep, so hook.mjs computes them once per PostToolUse. The output is windowed head +
396
+ * tail: a runner prints its failures at the END, and a multi-megabyte stdout should not be
397
+ * regex-scanned on every call (D#69). benchmark/episode-flush-replay.mjs calls this too, so
398
+ * the model A/B it feeds sees the entries production builds (D#326).
399
+ * @param {string} tool
400
+ * @param {object} input tool_input
401
+ * @param {string} resp normalized tool response
402
+ * @param {{isError?: boolean, writesFiles?: boolean, scrub?: (s: string) => string}} [opts]
403
+ * as extractDiagnosis
404
+ * @returns {{inputTags: string[], diag: string[], diagOut?: string[]}}
405
+ */
406
+ export function workerEntryFields(tool, input, resp, opts) {
407
+ const respWindow = resp.length > 65536 ? resp.slice(0, 32768) + '\n' + resp.slice(-32768) : resp;
408
+ const diagnosis = extractDiagnosis(tool, input, respWindow, opts);
409
+ return {
410
+ inputTags: entryInputTags(tool, input, respWindow),
411
+ diag: diagnosis.lines,
412
+ ...(tool === 'Bash' ? { diagOut: diagnosis.output } : {}),
413
+ };
414
+ }
415
+
389
416
  // ─── Grounding check ────────────────────────────────────────────────────────
390
417
 
391
418
  const GROUND_NGRAM = 4;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.26.0",
3
+ "version": "6.26.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "6.26.0",
9
+ "version": "6.26.1",
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": "6.26.0",
3
+ "version": "6.26.1",
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",