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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/haiku-client.mjs +77 -14
- package/hook.mjs +8 -21
- package/lib/episode-input-filter.mjs +27 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.26.
|
|
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.
|
|
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.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
686
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
785
|
-
//
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.26.
|
|
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.
|
|
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.
|
|
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",
|