claude-mem-lite 6.25.3 → 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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/deep-search.mjs +18 -1
- package/haiku-client.mjs +161 -29
- package/hook-llm.mjs +59 -44
- 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.
|
|
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.
|
|
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/README.md
CHANGED
|
@@ -1134,10 +1134,10 @@ claude-mem-lite.
|
|
|
1134
1134
|
| `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
|
|
1135
1135
|
| `ANTHROPIC_API_KEY` | Anthropic API key. When set, all background LLM calls go directly to the Anthropic Messages API (with prompt caching) - or to the `ANTHROPIC_BASE_URL` gateway when that is set. Highest priority. | _(unset → CLI)_ |
|
|
1136
1136
|
| `ANTHROPIC_BASE_URL` | Base URL for the direct Messages API when an Anthropic-compatible gateway serves the models (Azure AI Foundry, LiteLLM, Bedrock/Vertex proxies). No `/v1` suffix — the endpoint path is appended. The `claude -p` fallback reads the same variable, so one value covers both transports. Plain `http://` is accepted only for a loopback host; anything else would send the API key unencrypted. That request always goes straight to the loopback host, even when `NODE_USE_ENV_PROXY=1` sends other requests through `HTTP_PROXY`. A value that is set but unusable skips the direct API leg, so background calls fall back to the `claude -p` CLI, and doctor warns. | `https://api.anthropic.com` |
|
|
1137
|
-
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Model ID or gateway deployment name for the `haiku` tier, honored on the direct API **only when `ANTHROPIC_BASE_URL` is set**. Without a base URL it stays CLI-only, so the Bedrock/Vertex form of this alias is never sent to `api.anthropic.com`. Use it when the gateway routes on deployment names rather than Anthropic model IDs (Azure Foundry deployments). The `claude -p` fallback resolves its `--model haiku` alias through it too. | built-in `claude-haiku-
|
|
1137
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Model ID or gateway deployment name for the `haiku` tier, honored on the direct API **only when `ANTHROPIC_BASE_URL` is set**. Without a base URL it stays CLI-only, so the Bedrock/Vertex form of this alias is never sent to `api.anthropic.com`. Use it when the gateway routes on deployment names rather than Anthropic model IDs (Azure Foundry deployments). The `claude -p` fallback resolves its `--model haiku` alias through it too. | built-in `claude-haiku-5-5` |
|
|
1138
1138
|
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | Same as above for the `sonnet` tier. | built-in `claude-sonnet-4-5-…` |
|
|
1139
1139
|
| `OPENROUTER_API_KEY` | OpenRouter API key (OpenAI-compatible). Used for background LLM calls when `ANTHROPIC_API_KEY` is **not** set. If neither key is set, calls fall back to the `claude -p` CLI. | _(unset)_ |
|
|
1140
|
-
| `OPENROUTER_MODEL` | Overrides the OpenRouter model slug for **all** background calls (e.g. `openai/gpt-4o-mini`, `qwen/qwen-2.5-72b-instruct`). When unset, the `CLAUDE_MEM_MODEL` tier maps to `anthropic/claude-haiku-
|
|
1140
|
+
| `OPENROUTER_MODEL` | Overrides the OpenRouter model slug for **all** background calls (e.g. `openai/gpt-4o-mini`, `qwen/qwen-2.5-72b-instruct`). When unset, the `CLAUDE_MEM_MODEL` tier maps to `anthropic/claude-haiku-5.5` (haiku) or `anthropic/claude-sonnet-4.5` (sonnet). | _(tier default)_ |
|
|
1141
1141
|
| `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
|
|
1142
1142
|
| `MEM_QUIET_HOOKS` | Low-noise hooks. `1` drops the `File Lessons` / `Key Context` sections from SessionStart injection, the lesson suffix from `[mem] Related memories`, and the `WHEN TO USE` / `Decision rules` blocks from MCP server instructions. IDs and the `Recent` table still surface so `mem_get(ids=[…])` remains reachable. Intended for users running the invited-memory adopt path or who otherwise want minimal auto-injection. **Since v2.82.0 this env no longer gates auto-adopt — use `MEM_NO_AUTO_ADOPT=1` for that.** | _(disabled)_ |
|
|
1143
1143
|
| `CLAUDE_MEM_SESSION_EVENTS` | `1`/`on` restores the SessionStart `### Key Events` section (recent high-importance rows from the `events` table). **Off by default since v6.13.0**: an audit of 30 events read 2 accurate and 16 wrong. The UserPromptSubmit events block and PreToolUse recall are query-matched and stay on; `mem_search` still reaches every event. | _(off)_ |
|
package/README.zh-CN.md
CHANGED
|
@@ -863,10 +863,10 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
|
|
|
863
863
|
| `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
|
|
864
864
|
| `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching);设了 `ANTHROPIC_BASE_URL` 时改发往那个网关。优先级最高。 | _(未设 → CLI)_ |
|
|
865
865
|
| `ANTHROPIC_BASE_URL` | 由兼容 Anthropic 的网关(Azure AI Foundry、LiteLLM、Bedrock/Vertex 代理)提供模型时,直连 Messages API 使用的基础 URL。不要带 `/v1` 后缀,端点路径会自动拼上。`claude -p` 兜底读取同一个变量,所以一个值同时作用于两条路径。明文 `http://` 只接受回环主机,否则 API key 会以明文发送。这个请求总是直连回环主机,即使 `NODE_USE_ENV_PROXY=1` 让其他请求走 `HTTP_PROXY`。设了但不可用的值会跳过直连这一路,后台调用改走 `claude -p`,doctor 会提示。 | `https://api.anthropic.com` |
|
|
866
|
-
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `haiku` 档的模型 ID 或网关部署名,**仅在设了 `ANTHROPIC_BASE_URL` 时**用于直连 API。没有基础 URL 时只给 CLI 用,所以这个别名的 Bedrock/Vertex 写法不会被发往 `api.anthropic.com`。网关按部署名而不是 Anthropic 模型 ID 路由时(Azure Foundry 部署)使用。`claude -p` 兜底的 `--model haiku` 别名也经由它解析。 | 内置 `claude-haiku-
|
|
866
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `haiku` 档的模型 ID 或网关部署名,**仅在设了 `ANTHROPIC_BASE_URL` 时**用于直连 API。没有基础 URL 时只给 CLI 用,所以这个别名的 Bedrock/Vertex 写法不会被发往 `api.anthropic.com`。网关按部署名而不是 Anthropic 模型 ID 路由时(Azure Foundry 部署)使用。`claude -p` 兜底的 `--model haiku` 别名也经由它解析。 | 内置 `claude-haiku-5-5` |
|
|
867
867
|
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | 同上,用于 `sonnet` 档。 | 内置 `claude-sonnet-4-5-…` |
|
|
868
868
|
| `OPENROUTER_API_KEY` | OpenRouter API key(OpenAI 兼容)。当**未设** `ANTHROPIC_API_KEY` 时用于后台 LLM 调用;两者都未设则回退到 `claude -p` CLI。 | _(未设)_ |
|
|
869
|
-
| `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-
|
|
869
|
+
| `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-5.5`(haiku)或 `anthropic/claude-sonnet-4.5`(sonnet)。 | _(分层默认)_ |
|
|
870
870
|
| `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
|
|
871
871
|
| `MEM_QUIET_HOOKS` | 低噪声 hook。设为 `1` 时,SessionStart 注入去掉 `File Lessons` / `Key Context` 两节,`[mem] Related memories` 去掉 lesson 后缀,MCP server instructions 去掉 `WHEN TO USE` / `Decision rules` 两段。ID 与 `Recent` 表仍保留,`mem_get(ids=[…])` 可继续展开细节。适用于启用了 invited-memory adopt 流程或偏好最小化自动注入的用户。**v2.82.0 起此 env 不再阻挡 auto-adopt——如需关闭 auto-adopt 用 `MEM_NO_AUTO_ADOPT=1`。** | _(禁用)_ |
|
|
872
872
|
| `CLAUDE_MEM_SESSION_EVENTS` | 设为 `1`/`on` 时恢复 SessionStart 的 `### Key Events` 一节(`events` 表中最近的高重要度条目)。**v6.13.0 起默认关闭**:对 30 条 event 的核验只有 2 条属实、16 条错误。UserPromptSubmit 的 events 块与 PreToolUse 召回按查询匹配,保持开启;`mem_search` 仍可检索全部 event。 | _(关闭)_ |
|
package/deep-search.mjs
CHANGED
|
@@ -269,6 +269,23 @@ export const REWRITE_SYSTEM =
|
|
|
269
269
|
'Emit exactly 3 non-empty variants. If unsure, still emit at least the keyword form as v1.\n' +
|
|
270
270
|
INJECTION_GUARD;
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The rewrite's output as a structured-output schema. Sent only to models that get the
|
|
274
|
+
* strict body (haiku-client's STRICT_BODY_MODELS / _SLUGS): with thinking off, Claude
|
|
275
|
+
* Haiku 5.5's answer gave fewer than two variants on 7 of 72 replays (the ones printed were
|
|
276
|
+
* prose questions about a bare one-word query, "React"), and on 0 of 72 with this schema
|
|
277
|
+
* (2026-10-15, docs/audits/20261015-v6.26.0-replays.md).
|
|
278
|
+
*/
|
|
279
|
+
export const REWRITE_JSON_SCHEMA = {
|
|
280
|
+
name: 'rewrite_variants',
|
|
281
|
+
schema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: { variants: { type: 'array', items: { type: 'string' } } },
|
|
284
|
+
required: ['variants'],
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
272
289
|
/**
|
|
273
290
|
* Build the split-form rewrite prompt. The constant instructions live in the
|
|
274
291
|
* system slot; the untrusted query goes verbatim into the user/data slot so an
|
|
@@ -372,7 +389,7 @@ export function makeThrottled(llm, { intervalMs = AUTO_DEEP_THROTTLE_MS } = {})
|
|
|
372
389
|
// never load the LLM client.
|
|
373
390
|
async function callRewriteLLM(prompt, { timeout }) {
|
|
374
391
|
const { callModelJSONAsync } = await import('./haiku-client.mjs');
|
|
375
|
-
return callModelJSONAsync(prompt, 'haiku', { timeout, maxTokens: 400 });
|
|
392
|
+
return callModelJSONAsync(prompt, 'haiku', { timeout, maxTokens: 400, jsonSchema: REWRITE_JSON_SCHEMA });
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
// Default (explicit deep=true) provider: patient timeout, no throttle/cache.
|
package/haiku-client.mjs
CHANGED
|
@@ -55,11 +55,51 @@ function cliSpawnCwd() {
|
|
|
55
55
|
// ─── Model Resolution ────────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
57
|
// CLI name → API model ID mapping
|
|
58
|
+
// haiku moved from claude-haiku-4-5-20251001 on 2026-10-15, after
|
|
59
|
+
// benchmark/haiku-model-ab.mjs read usability parity on the three haiku-tier sites it
|
|
60
|
+
// samples (episode, rewrite, rerank) and a blind read of the kept lessons put 5.5 ahead
|
|
61
|
+
// (docs/measurement/rulers.md, "haiku-tier model A/B"). It is also what
|
|
62
|
+
// `claude -p --model haiku` resolves to on the Anthropic API since Claude Code 2.1.293
|
|
63
|
+
// (Bedrock, Vertex, Foundry and Claude Platform on AWS still resolve it to 4.5), so there
|
|
64
|
+
// all three legs run the same model.
|
|
58
65
|
const MODEL_MAP = {
|
|
59
|
-
haiku: 'claude-haiku-
|
|
66
|
+
haiku: 'claude-haiku-5-5',
|
|
60
67
|
sonnet: 'claude-sonnet-4-5-20250929',
|
|
61
68
|
};
|
|
62
69
|
|
|
70
|
+
// Models that get the STRICT body. Claude Haiku 5.5 answers any non-default `temperature`
|
|
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. 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
|
|
79
|
+
// prose question instead of JSON, so a caller's `jsonSchema` becomes a structured output on
|
|
80
|
+
// these models (rewrite replays 2026-10-15: 7/72 below two variants without it, 0/72 with
|
|
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.
|
|
83
|
+
const STRICT_BODY_MODELS = new Set(['claude-haiku-5-5']);
|
|
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
|
+
|
|
63
103
|
// Claude Code's tier-alias override vars, honored on the direct-API leg too:
|
|
64
104
|
// gateway deployments route on the DEPLOYMENT name (Azure Foundry rejects the
|
|
65
105
|
// Anthropic model ID when it differs), and one env set then covers both the
|
|
@@ -94,6 +134,9 @@ function apiModelId(tier, base = resolveAnthropicBaseUrl()) {
|
|
|
94
134
|
// to 0 so the provider default (~1.0) doesn't inject wording variance that breaks
|
|
95
135
|
// JSON parsing or defeats the wording-sensitive MinHash near-duplicate detector.
|
|
96
136
|
// A call that genuinely needs sampling can pass opts.temperature to override.
|
|
137
|
+
// The pin holds only where the model takes it: Claude Haiku 5.5 accepts no temperature
|
|
138
|
+
// but its default (the direct leg omits the field, OpenRouter drops it, the CLI never sent
|
|
139
|
+
// one), so on that model the extraction runs at its default sampling.
|
|
97
140
|
const DEFAULT_LLM_TEMPERATURE = 0;
|
|
98
141
|
|
|
99
142
|
/**
|
|
@@ -134,13 +177,21 @@ export function resolveModel() {
|
|
|
134
177
|
// OpenRouter uses its own slug namespace (OpenAI-compatible API). Map the
|
|
135
178
|
// project's haiku/sonnet tiers to the matching anthropic/* slugs so the quality
|
|
136
179
|
// tiering is preserved when routing through OpenRouter. Slugs verified against
|
|
137
|
-
// openrouter.ai (2026-06)
|
|
138
|
-
// native MODEL_MAP IDs above.
|
|
180
|
+
// openrouter.ai: claude-sonnet-4.5 (2026-06) and claude-haiku-5.5 (2026-10-15) mirror
|
|
181
|
+
// the native MODEL_MAP IDs above.
|
|
139
182
|
const OPENROUTER_MODEL_MAP = {
|
|
140
|
-
haiku: 'anthropic/claude-haiku-
|
|
183
|
+
haiku: 'anthropic/claude-haiku-5.5',
|
|
141
184
|
sonnet: 'anthropic/claude-sonnet-4.5',
|
|
142
185
|
};
|
|
143
186
|
|
|
187
|
+
// STRICT_BODY_MODELS on OpenRouter's slug namespace. Measured 2026-10-15 on six rerank
|
|
188
|
+
// prompts, three of them picked because they had failed, three calls each (300-token
|
|
189
|
+
// budget): the JSON came back cut off (finish_reason "length"; the two printed spent 259 and
|
|
190
|
+
// 275 tokens thinking) on 8 of 18 calls, 0 of 18 with reasoning off
|
|
191
|
+
// (docs/audits/20261015-v6.26.0-replays.md). A one-line extraction did not think at all, so
|
|
192
|
+
// a single easy probe reads this as fine.
|
|
193
|
+
const STRICT_BODY_SLUGS = new Set(['anthropic/claude-haiku-5.5']);
|
|
194
|
+
|
|
144
195
|
/**
|
|
145
196
|
* Resolve the OpenRouter model slug for a given tier.
|
|
146
197
|
* OPENROUTER_MODEL (if set, non-blank) overrides every tier with an explicit
|
|
@@ -242,11 +293,13 @@ export function flattenForCLI(input) {
|
|
|
242
293
|
* @param {object} [opts] Options
|
|
243
294
|
* @param {number} [opts.timeout=10000] Timeout in milliseconds
|
|
244
295
|
* @param {number} [opts.maxTokens=500] Max tokens in response
|
|
296
|
+
* @param {{name: string, schema: object}|null} [opts.jsonSchema] structured output for the
|
|
297
|
+
* models that get the strict body (STRICT_BODY_MODELS / _SLUGS); ignored elsewhere and on the CLI
|
|
245
298
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
246
299
|
*/
|
|
247
300
|
export async function callHaiku(
|
|
248
301
|
prompt,
|
|
249
|
-
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
302
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
250
303
|
) {
|
|
251
304
|
if (!prompt) return null;
|
|
252
305
|
|
|
@@ -275,8 +328,13 @@ export async function callHaiku(
|
|
|
275
328
|
// the difference between 1.4s and 13.5s.
|
|
276
329
|
primary =
|
|
277
330
|
mode === 'api'
|
|
278
|
-
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature })
|
|
279
|
-
: await callOpenRouterAPI(prompt, resolveModel().cli, {
|
|
331
|
+
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature, jsonSchema })
|
|
332
|
+
: await callOpenRouterAPI(prompt, resolveModel().cli, {
|
|
333
|
+
timeout,
|
|
334
|
+
maxTokens,
|
|
335
|
+
temperature,
|
|
336
|
+
jsonSchema,
|
|
337
|
+
});
|
|
280
338
|
} catch (e) {
|
|
281
339
|
debugCatch(e, `callHaiku:${mode}`);
|
|
282
340
|
}
|
|
@@ -323,14 +381,15 @@ export async function callHaikuJSON(prompt, opts) {
|
|
|
323
381
|
* Defaults also mirror callHaiku (10s / 500 tokens), not callModelJSONAsync's
|
|
324
382
|
* 15s / 1000: a caller that omits opts must get the sync twin's budget.
|
|
325
383
|
* @param {string|{system?:string,user:string}} prompt
|
|
326
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
384
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
385
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
327
386
|
* @returns {Promise<object|null>} Parsed JSON or null
|
|
328
387
|
*/
|
|
329
388
|
export async function callHaikuJSONAsync(
|
|
330
389
|
prompt,
|
|
331
|
-
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
390
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
332
391
|
) {
|
|
333
|
-
return callModelJSONAsync(prompt, resolveModel().cli, { timeout, maxTokens, temperature });
|
|
392
|
+
return callModelJSONAsync(prompt, resolveModel().cli, { timeout, maxTokens, temperature, jsonSchema });
|
|
334
393
|
}
|
|
335
394
|
|
|
336
395
|
// ─── Model-Selectable API ────────────────────────────────────────────────────
|
|
@@ -345,12 +404,14 @@ export async function callHaikuJSONAsync(
|
|
|
345
404
|
* @param {object} [opts] Options
|
|
346
405
|
* @param {number} [opts.timeout=15000] Timeout in milliseconds
|
|
347
406
|
* @param {number} [opts.maxTokens=1000] Max tokens in response
|
|
407
|
+
* @param {{name: string, schema: object}|null} [opts.jsonSchema] structured output for the
|
|
408
|
+
* models that get the strict body (STRICT_BODY_MODELS / _SLUGS); ignored elsewhere and on the CLI
|
|
348
409
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
349
410
|
*/
|
|
350
411
|
export async function callLLMWithModel(
|
|
351
412
|
prompt,
|
|
352
413
|
model = 'haiku',
|
|
353
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
414
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
354
415
|
) {
|
|
355
416
|
if (!prompt) return null;
|
|
356
417
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -372,8 +433,8 @@ export async function callLLMWithModel(
|
|
|
372
433
|
try {
|
|
373
434
|
primary =
|
|
374
435
|
mode === 'api'
|
|
375
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
376
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
436
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
437
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
377
438
|
} catch (e) {
|
|
378
439
|
debugCatch(e, `callLLMWithModel:${mode}:${resolvedModel}`);
|
|
379
440
|
}
|
|
@@ -401,13 +462,14 @@ export async function callLLMWithModel(
|
|
|
401
462
|
* salvage. Only the CLI transport differs.
|
|
402
463
|
* @param {string|{system?:string,user:string}} prompt
|
|
403
464
|
* @param {'haiku'|'sonnet'} model
|
|
404
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
465
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
466
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
405
467
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
406
468
|
*/
|
|
407
469
|
export async function callLLMWithModelAsync(
|
|
408
470
|
prompt,
|
|
409
471
|
model = 'haiku',
|
|
410
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
472
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
411
473
|
) {
|
|
412
474
|
if (!prompt) return null;
|
|
413
475
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -420,8 +482,8 @@ export async function callLLMWithModelAsync(
|
|
|
420
482
|
try {
|
|
421
483
|
primary =
|
|
422
484
|
mode === 'api'
|
|
423
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
424
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
485
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
486
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
425
487
|
} catch (e) {
|
|
426
488
|
debugCatch(e, `callLLMWithModelAsync:${mode}:${resolvedModel}`);
|
|
427
489
|
}
|
|
@@ -457,13 +519,14 @@ export async function callModelJSON(prompt, model = 'haiku', opts) {
|
|
|
457
519
|
* throws; returns parsed JSON or null.
|
|
458
520
|
* @param {string|{system?:string,user:string}} prompt
|
|
459
521
|
* @param {'haiku'|'sonnet'} model
|
|
460
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
522
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
523
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
461
524
|
* @returns {Promise<object|null>}
|
|
462
525
|
*/
|
|
463
526
|
export async function callModelJSONAsync(
|
|
464
527
|
prompt,
|
|
465
528
|
model = 'haiku',
|
|
466
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
529
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
467
530
|
) {
|
|
468
531
|
if (!prompt) return null;
|
|
469
532
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -480,8 +543,8 @@ export async function callModelJSONAsync(
|
|
|
480
543
|
try {
|
|
481
544
|
primary =
|
|
482
545
|
mode === 'api'
|
|
483
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
484
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
546
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
547
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
485
548
|
} catch (e) {
|
|
486
549
|
debugCatch(e, `callModelJSONAsync:${mode}:${resolvedModel}`);
|
|
487
550
|
}
|
|
@@ -501,7 +564,11 @@ export function _resetTemperatureCompat() {
|
|
|
501
564
|
_temperatureDeprecated.clear();
|
|
502
565
|
}
|
|
503
566
|
|
|
504
|
-
async function callModelAPI(
|
|
567
|
+
async function callModelAPI(
|
|
568
|
+
prompt,
|
|
569
|
+
model,
|
|
570
|
+
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null },
|
|
571
|
+
) {
|
|
505
572
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
506
573
|
if (!apiKey) return null;
|
|
507
574
|
|
|
@@ -535,8 +602,11 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
535
602
|
// field outright (Azure Foundry's claude-sonnet-5 answers 400
|
|
536
603
|
// "`temperature` is deprecated for this model."). Keep sending it until a
|
|
537
604
|
// model refuses, then drop it for that model only - the retry at the bottom
|
|
538
|
-
// pays for itself once.
|
|
539
|
-
if (
|
|
605
|
+
// pays for itself once. A model known to refuse it never gets it.
|
|
606
|
+
if (STRICT_BODY_MODELS.has(strictBodyKey(modelId))) {
|
|
607
|
+
body.thinking = { type: 'disabled' };
|
|
608
|
+
if (jsonSchema) body.output_config = { format: { type: 'json_schema', schema: jsonSchema.schema } };
|
|
609
|
+
} else if (!_temperatureDeprecated.has(modelId)) body.temperature = temperature;
|
|
540
610
|
// System slot is constant per call type (instructions, schema, type taxonomy)
|
|
541
611
|
// — mark it cache_control:ephemeral so repeated calls within the 5-min cache
|
|
542
612
|
// window pay the cached-input rate (~0.10× base). Sub-1024-token systems still
|
|
@@ -624,7 +694,29 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
624
694
|
}
|
|
625
695
|
|
|
626
696
|
const data = await res.json();
|
|
627
|
-
|
|
697
|
+
// A safety classifier's decline is a 200 that may carry no text or part of an answer.
|
|
698
|
+
// null hands it to the CLI fallback like any other failure; the category, logged only
|
|
699
|
+
// under CLAUDE_MEM_DEBUG, is the only trace of why.
|
|
700
|
+
if (data.stop_reason === 'refusal') {
|
|
701
|
+
debugLog('WARN', `${model}-api`, `refusal (${data.stop_details?.category ?? 'no category'})`);
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
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).
|
|
714
|
+
const text = Array.isArray(data.content)
|
|
715
|
+
? data.content
|
|
716
|
+
.filter((b) => typeof b?.text === 'string')
|
|
717
|
+
.map((b) => b.text)
|
|
718
|
+
.join('')
|
|
719
|
+
: '';
|
|
628
720
|
return text ? { text } : null;
|
|
629
721
|
} finally {
|
|
630
722
|
clearTimeout(timer);
|
|
@@ -698,7 +790,10 @@ function claudeArgs(modelName) {
|
|
|
698
790
|
/**
|
|
699
791
|
* The environment every headless spawn runs with. MAX_THINKING_TOKENS=0: these are
|
|
700
792
|
* structured extraction calls, and the thinking the CLI enables by default was most of
|
|
701
|
-
* 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.
|
|
702
797
|
*/
|
|
703
798
|
function cliSpawnEnv() {
|
|
704
799
|
return {
|
|
@@ -1014,7 +1109,7 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
1014
1109
|
async function callOpenRouterAPI(
|
|
1015
1110
|
prompt,
|
|
1016
1111
|
tier,
|
|
1017
|
-
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE },
|
|
1112
|
+
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null },
|
|
1018
1113
|
) {
|
|
1019
1114
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
1020
1115
|
if (!apiKey) return null;
|
|
@@ -1036,7 +1131,24 @@ async function callOpenRouterAPI(
|
|
|
1036
1131
|
// Optional OpenRouter attribution headers (ignored by the API if absent).
|
|
1037
1132
|
'X-Title': 'claude-mem-lite',
|
|
1038
1133
|
};
|
|
1039
|
-
|
|
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) {
|
|
1143
|
+
payload.reasoning = { enabled: false };
|
|
1144
|
+
if (jsonSchema) {
|
|
1145
|
+
payload.response_format = {
|
|
1146
|
+
type: 'json_schema',
|
|
1147
|
+
json_schema: { name: jsonSchema.name, strict: true, schema: jsonSchema.schema },
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const reqBody = JSON.stringify(payload);
|
|
1040
1152
|
// Native fetch ignores HTTP(S)_PROXY; when a proxy is configured, tunnel the
|
|
1041
1153
|
// request through it — a direct fetch to openrouter.ai times out behind one.
|
|
1042
1154
|
const proxy = httpConnectProxyFor(url);
|
|
@@ -1050,7 +1162,27 @@ async function callOpenRouterAPI(
|
|
|
1050
1162
|
}
|
|
1051
1163
|
|
|
1052
1164
|
const data = await res.json();
|
|
1053
|
-
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;
|
|
1054
1186
|
return text ? { text } : null;
|
|
1055
1187
|
} finally {
|
|
1056
1188
|
clearTimeout(timer);
|
package/hook-llm.mjs
CHANGED
|
@@ -933,6 +933,64 @@ ${diagnosisBlock(diag)}`;
|
|
|
933
933
|
|
|
934
934
|
// ─── Background: LLM Episode Extraction (Tier 2 F) ──────────────────────────
|
|
935
935
|
|
|
936
|
+
/**
|
|
937
|
+
* The episode-extraction prompt, `{ system, user }`. Exported so
|
|
938
|
+
* benchmark/haiku-model-ab.mjs sends the SHIPPED prompt rather than a retyped copy
|
|
939
|
+
* (buildCompressPrompt was extracted for the same reason).
|
|
940
|
+
* @param {object} episode
|
|
941
|
+
* @param {string[]} [diag] episodeDiagnosis(episode); the handler passes the one it keeps
|
|
942
|
+
* @returns {{system: string, user: string}}
|
|
943
|
+
*/
|
|
944
|
+
export function buildEpisodePrompt(episode, diag = episodeDiagnosis(episode)) {
|
|
945
|
+
// Same guard as the handler: an older-format tmp file can omit `files`.
|
|
946
|
+
const episodeFiles = Array.isArray(episode.files) ? episode.files : [];
|
|
947
|
+
const fileList = episodeFiles.map((f) => basename(f)).join(', ') || '(multiple)';
|
|
948
|
+
|
|
949
|
+
// Defense-in-depth (cso F#4): split static instructions (system) from
|
|
950
|
+
// per-call data (user). Episode descriptions and file paths come from tool
|
|
951
|
+
// events; treating them as a separate role + boundary marker reduces the
|
|
952
|
+
// attack surface for memory poisoning via crafted file content.
|
|
953
|
+
const SHARED_OBS_SCHEMA_TAIL = `${MEMORY_INPUT_GUARD}
|
|
954
|
+
Grounding: state only outcomes the user message shows. An action description may be cut short ("…"); if its result is not visible, say what was run, not how it turned out. Never write that something passed, failed, was verified or confirmed unless that result appears in the user message.
|
|
955
|
+
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause; a test that failed only until this episode's implementation landed (written first, then made to pass) is TDD, not a bugfix — type that feature or refactor. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
956
|
+
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
957
|
+
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
958
|
+
lesson_learned: The non-obvious insight a future session would benefit from, resting ONLY on the DIAGNOSIS lines: copy at least 4 consecutive words verbatim, in double quotes, from one DIAGNOSIS line, and claim no mechanism that line does not state. Example: FTS5's "default tokenizer doesn't split CJK" — index bigrams instead. No DIAGNOSIS line, or none that states a cause → output JSON null; a lesson without such a quote is discarded. Do NOT invent a lesson. A mutation/probe run (a file changed on purpose, a checksum, a restore) and the agent's own script errors are not product bugs. Do NOT write the strings "none"/"n/a"/"todo"/"tbd"/"-" — those will be discarded as noise.
|
|
959
|
+
scope: ${SCOPE_PROMPT_LEGEND}
|
|
960
|
+
search_aliases: 2-6 alternative search terms someone might use to find this memory later (include CJK if project uses Chinese)`;
|
|
961
|
+
|
|
962
|
+
if (episode.entries.length === 1) {
|
|
963
|
+
const e = episode.entries[0];
|
|
964
|
+
const system = `Extract a structured observation from this code change. Return ONLY valid JSON, no markdown fences.
|
|
965
|
+
|
|
966
|
+
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"concise ≤80 char description","narrative":"what changed, why, and outcome (2-3 sentences)","concepts":["kw1","kw2"],"facts":["fact1","fact2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
967
|
+
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
968
|
+
const user = `Tool: ${e.tool}
|
|
969
|
+
File: ${episodeFiles.join(', ') || 'unknown'}
|
|
970
|
+
Action: ${e.desc}
|
|
971
|
+
Error: ${e.isError ? 'yes' : 'no'}
|
|
972
|
+
|
|
973
|
+
${diagnosisBlock(diag)}`;
|
|
974
|
+
return { system, user };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const actionList = episode.entries
|
|
978
|
+
.map((e, i) => `${i + 1}. [${e.tool}] ${e.desc}${e.isError ? ' (ERROR)' : ''}`)
|
|
979
|
+
.join('\n');
|
|
980
|
+
|
|
981
|
+
const system = `Summarize this coding episode as ONE coherent observation. Return ONLY valid JSON, no markdown fences.
|
|
982
|
+
|
|
983
|
+
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"coherent ≤80 char summary","narrative":"what was done, why, and outcome (3-5 sentences)","concepts":["keyword1","keyword2"],"facts":["specific fact 1","specific fact 2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
984
|
+
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
985
|
+
const user = `Project: ${episode.project}
|
|
986
|
+
Files: ${fileList}
|
|
987
|
+
Actions (${episode.entries.length} total):
|
|
988
|
+
${actionList}
|
|
989
|
+
|
|
990
|
+
${diagnosisBlock(diag)}`;
|
|
991
|
+
return { system, user };
|
|
992
|
+
}
|
|
993
|
+
|
|
936
994
|
export async function handleLLMEpisode() {
|
|
937
995
|
const tmpFile = process.argv[3];
|
|
938
996
|
if (!tmpFile) return;
|
|
@@ -974,51 +1032,8 @@ export async function handleLLMEpisode() {
|
|
|
974
1032
|
const fileList = episodeFiles.map((f) => basename(f)).join(', ') || '(multiple)';
|
|
975
1033
|
const edgeFiles = splitEpisodeFiles(episode);
|
|
976
1034
|
|
|
977
|
-
// Defense-in-depth (cso F#4): split static instructions (system) from
|
|
978
|
-
// per-call data (user). Episode descriptions and file paths come from tool
|
|
979
|
-
// events; treating them as a separate role + boundary marker reduces the
|
|
980
|
-
// attack surface for memory poisoning via crafted file content.
|
|
981
|
-
const SHARED_OBS_SCHEMA_TAIL = `${MEMORY_INPUT_GUARD}
|
|
982
|
-
Grounding: state only outcomes the user message shows. An action description may be cut short ("…"); if its result is not visible, say what was run, not how it turned out. Never write that something passed, failed, was verified or confirmed unless that result appears in the user message.
|
|
983
|
-
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause; a test that failed only until this episode's implementation landed (written first, then made to pass) is TDD, not a bugfix — type that feature or refactor. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
984
|
-
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
985
|
-
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
986
|
-
lesson_learned: The non-obvious insight a future session would benefit from, resting ONLY on the DIAGNOSIS lines: copy at least 4 consecutive words verbatim, in double quotes, from one DIAGNOSIS line, and claim no mechanism that line does not state. Example: FTS5's "default tokenizer doesn't split CJK" — index bigrams instead. No DIAGNOSIS line, or none that states a cause → output JSON null; a lesson without such a quote is discarded. Do NOT invent a lesson. A mutation/probe run (a file changed on purpose, a checksum, a restore) and the agent's own script errors are not product bugs. Do NOT write the strings "none"/"n/a"/"todo"/"tbd"/"-" — those will be discarded as noise.
|
|
987
|
-
scope: ${SCOPE_PROMPT_LEGEND}
|
|
988
|
-
search_aliases: 2-6 alternative search terms someone might use to find this memory later (include CJK if project uses Chinese)`;
|
|
989
|
-
|
|
990
1035
|
const diag = episodeDiagnosis(episode);
|
|
991
|
-
|
|
992
|
-
if (episode.entries.length === 1) {
|
|
993
|
-
const e = episode.entries[0];
|
|
994
|
-
const system = `Extract a structured observation from this code change. Return ONLY valid JSON, no markdown fences.
|
|
995
|
-
|
|
996
|
-
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"concise ≤80 char description","narrative":"what changed, why, and outcome (2-3 sentences)","concepts":["kw1","kw2"],"facts":["fact1","fact2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
997
|
-
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
998
|
-
const user = `Tool: ${e.tool}
|
|
999
|
-
File: ${episodeFiles.join(', ') || 'unknown'}
|
|
1000
|
-
Action: ${e.desc}
|
|
1001
|
-
Error: ${e.isError ? 'yes' : 'no'}
|
|
1002
|
-
|
|
1003
|
-
${diagnosisBlock(diag)}`;
|
|
1004
|
-
prompt = { system, user };
|
|
1005
|
-
} else {
|
|
1006
|
-
const actionList = episode.entries
|
|
1007
|
-
.map((e, i) => `${i + 1}. [${e.tool}] ${e.desc}${e.isError ? ' (ERROR)' : ''}`)
|
|
1008
|
-
.join('\n');
|
|
1009
|
-
|
|
1010
|
-
const system = `Summarize this coding episode as ONE coherent observation. Return ONLY valid JSON, no markdown fences.
|
|
1011
|
-
|
|
1012
|
-
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"coherent ≤80 char summary","narrative":"what was done, why, and outcome (3-5 sentences)","concepts":["keyword1","keyword2"],"facts":["specific fact 1","specific fact 2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
1013
|
-
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
1014
|
-
const user = `Project: ${episode.project}
|
|
1015
|
-
Files: ${fileList}
|
|
1016
|
-
Actions (${episode.entries.length} total):
|
|
1017
|
-
${actionList}
|
|
1018
|
-
|
|
1019
|
-
${diagnosisBlock(diag)}`;
|
|
1020
|
-
prompt = { system, user };
|
|
1021
|
-
}
|
|
1036
|
+
const prompt = buildEpisodePrompt(episode, diag);
|
|
1022
1037
|
|
|
1023
1038
|
const ruleImportance = computeRuleImportance(episode);
|
|
1024
1039
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|