claude-mem-lite 6.22.1 → 6.23.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.22.1",
12
+ "version": "6.23.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.22.1",
3
+ "version": "6.23.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
@@ -194,7 +194,7 @@ Source files stay in the cloned repo. Update via `git pull && node install.mjs i
194
194
  ### What happens during installation
195
195
 
196
196
  1. **Install dependencies** -- `npm install --omit=dev` (compiles native `better-sqlite3`)
197
- 2. **Register MCP server** -- `mem-lite` server with 18 tools (9 core exposed via `tools/list` + 9 hidden-but-callable; see the Usage section for the full table). The pre-v2.78 generic server name `mem` is renamed to `mem-lite` for namespace hygiene; the tool names themselves (`mem_search`, `mem_recall`, ...) are unchanged.
197
+ 2. **Register MCP server** -- `mem-lite` server with 19 tool definitions (10 core + 9 hidden-but-callable; `mem_search_feedback` is registered only when search telemetry is enabled). The pre-v2.78 generic server name `mem` is renamed to `mem-lite` for namespace hygiene; the tool names themselves (`mem_search`, `mem_recall`, ...) are unchanged.
198
198
  3. **Configure hooks** -- all seven lifecycle events: `SessionStart`, `PreCompact`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Stop`, `UserPromptSubmit`
199
199
  4. **Create data directory** -- `~/.claude-mem-lite/` (hidden) for database, runtime, and managed resource files
200
200
  5. **Auto-migrate** -- If `~/.claude-mem/` (original claude-mem) or `~/claude-mem-lite/` (pre-v0.5 unhidden) exists, migrates database and runtime files to `~/.claude-mem-lite/`, preserving the original untouched
@@ -237,6 +237,28 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
237
237
  repos/ # Shallow-cloned source repos
238
238
  ```
239
239
 
240
+ ## Upgrading to 6.23.1
241
+
242
+ **If you set `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY`, background LLM calls now go to that
243
+ gateway.** No schema-version change: 6.22.1 still opens the database after this release has.
244
+ To keep the old behaviour, stay on 6.22.1; a plugin install that already updated can follow the
245
+ rollback recipe under "Trust model per install path".
246
+
247
+ - **Who is affected.** The direct API leg used to post to `api.anthropic.com` with the built-in
248
+ model IDs whatever `ANTHROPIC_BASE_URL` said; only the `claude -p` fallback followed it. Now
249
+ the direct leg uses it too, together with `ANTHROPIC_DEFAULT_HAIKU_MODEL` /
250
+ `ANTHROPIC_DEFAULT_SONNET_MODEL`. Without `ANTHROPIC_BASE_URL` nothing changes, even when the
251
+ two model variables are set.
252
+ - **A value the plugin cannot use** (not a parseable URL, not http or https, plain http to a
253
+ host that is not loopback, credentials in the URL, a query or a fragment) skips the direct
254
+ leg: every background call falls back to `claude -p`, which reads `ANTHROPIC_BASE_URL` itself,
255
+ and `claude-mem-lite doctor` says why. `doctor` now probes the gateway's host and port instead
256
+ of `api.anthropic.com`.
257
+ - **Also in this release:** large stores no longer time out on every prompt. The database now
258
+ gets SQLite's planner statistics, which adds SQLite's own `sqlite_stat1` / `sqlite_stat4`
259
+ tables. And `CLAUDE_MEM_SEARCH_TELEMETRY=1` is a new, off-by-default switch that records MCP
260
+ searches for relevance feedback. Details in CHANGELOG.md.
261
+
240
262
  ## Upgrading to 6.21.0
241
263
 
242
264
  **Projects whose names are not plain ASCII get a new id, and what they stored moves once.** No
@@ -496,8 +518,9 @@ are lost; only the derived vector index is.
496
518
 
497
519
  ### MCP Tools (used automatically by Claude)
498
520
 
499
- As of v2.70.0, the server registers 18 tools in total but only the 9 **core**
500
- tools appear in `tools/list`. The 9 **hidden** tools remain callable at the
521
+ The server defines 19 tools but registers `mem_search_feedback` only when
522
+ `CLAUDE_MEM_SEARCH_TELEMETRY=1`. The other 9 **core** tools appear in `tools/list`
523
+ by default. The 9 **hidden** tools remain callable at the
501
524
  protocol layer (`tools/call` by exact name still routes normally); they're
502
525
  omitted from the list response so Claude Code sessions don't load 9 extra
503
526
  tool schemas at startup. (It read 20 / 11 until v5.0.0 removed the two skill-registry
@@ -506,11 +529,12 @@ tools — `tool-schemas.mjs` is the source of truth, and
506
529
  `README.zh-CN.md`, `llms.txt` and `docs/ARCHITECTURE.md` to it.) Hidden tools are the maintenance / admin / browser
507
530
  surface — reach them through the CLI column in the second table.
508
531
 
509
- **Core (9, exposed to Claude Code)**
532
+ **Core (10 definitions; `mem_search_feedback` requires search telemetry)**
510
533
 
511
534
  | Tool | Description |
512
535
  |------|-------------|
513
536
  | `mem_search` | FTS5 full-text search with BM25 ranking. Filters by type, project, date range, importance level. |
537
+ | `mem_search_feedback` | Record sparse relevance labels for results from an existing `mem_search` Search ID. |
514
538
  | `mem_recent` | Show most recent observations, ordered by time. Quick snapshot of latest activity. |
515
539
  | `mem_recall` | Recall observations related to a file. Use before editing to surface past bugfixes and context. |
516
540
  | `mem_timeline` | Browse observations chronologically around an anchor point. |
@@ -1064,7 +1088,10 @@ claude-mem-lite.
1064
1088
  |----------|-------------|---------|
1065
1089
  | `CLAUDE_MEM_DIR` | Custom data directory. All databases, runtime files, and managed resources are stored here. | `~/.claude-mem-lite/` |
1066
1090
  | `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
1067
- | `ANTHROPIC_API_KEY` | Anthropic API key. When set, all background LLM calls go directly to the Anthropic Messages API (with prompt caching). Highest priority. | _(unset → CLI)_ |
1091
+ | `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)_ |
1092
+ | `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. 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` |
1093
+ | `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-4-5-…` |
1094
+ | `ANTHROPIC_DEFAULT_SONNET_MODEL` | Same as above for the `sonnet` tier. | built-in `claude-sonnet-4-5-…` |
1068
1095
  | `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)_ |
1069
1096
  | `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-4.5` (haiku) or `anthropic/claude-sonnet-4.5` (sonnet). | _(tier default)_ |
1070
1097
  | `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
@@ -1082,7 +1109,7 @@ claude-mem-lite.
1082
1109
 
1083
1110
  | Variable | Description | Default |
1084
1111
  |----------|-------------|---------|
1085
- | `CLAUDE_MEM_ALL_TOOLS` | `1` exposes all 18 MCP tools in `tools/list` instead of the 9 core ones (pre-v2.34.0 behavior). The 9 hidden tools stay callable by exact name either way. | _(9 core)_ |
1112
+ | `CLAUDE_MEM_ALL_TOOLS` | `1` exposes every registered MCP tool in `tools/list` instead of only the core ones (pre-v2.34.0 behavior). The 9 hidden tools stay callable by exact name either way; `mem_search_feedback` is registered only when search telemetry is enabled. | _(9 core)_ |
1086
1113
  | `CLAUDE_MEM_FILE_INTEL` | `0` disables the file-intel block injected before `Read` (past observations about the file you are about to open). | _(on)_ |
1087
1114
  | `CLAUDE_MEM_FILE_INTEL_MIN_TOKENS` | Files smaller than this stay silent — file-intel only pays for itself on large files. | `800` |
1088
1115
  | `CLAUDE_MEM_REREAD_GUARD` | `0` disables the warning when the same file is read twice in a session. Never fires on `offset`/`limit` paging. | _(on)_ |
@@ -1141,6 +1168,13 @@ benchmark and A/B harness are calibrated against — changing them invalidates t
1141
1168
  | `CLAUDE_MEM_CITATION_RELEVANCE_GATE` | Stop credits an `access_count` to a memory the session cited only when something made that memory relevant to the session — it was injected, or you typed its `#NN` yourself. `off` restores the pre-v3.84.0 behaviour of crediting every `#NN` the assistant wrote, which over-counts sessions that discuss memories in prose (release notes, audit reports): measured on real transcripts, 267 of 859 credited (id, session) pairs — 31.1% — were mentions nothing had put in front of the model. Superseded citations are redirected to their keeper on both settings. | _(on)_ |
1142
1169
  | `CLAUDE_MEM_SUBAGENT_DECAY` | The `subagent` injection face feeds the decay loop: memories handed to a dispatched agent enter the denominator, and the citation that agent makes in its own transcript counts as the numerator. `0` returns the face to metered-but-never-decaying (v3.77–v3.82). | _(on)_ |
1143
1170
  | `CLAUDE_MEM_METRICS` | `1` records feature-injection counters surfaced by `claude-mem-lite stats`. | _(off)_ |
1171
+ | `CLAUDE_MEM_SEARCH_TELEMETRY` | `1` records MCP `mem_search` queries and results, prints Search IDs for optional `mem_search_feedback`, and stamps each search with the running claude-mem-lite version. | _(off)_ |
1172
+
1173
+ Search telemetry rows are retained until you delete them. Inspect their volume with
1174
+ `claude-mem-lite stats --search-telemetry`; clear them from the configured SQLite
1175
+ database (normally `~/.claude-mem-lite/claude-mem-lite.db`) with
1176
+ `DELETE FROM search_results; DELETE FROM search_runs; VACUUM;`. Pre-delete snapshots
1177
+ beside that database retain their earlier contents until those `.bak` files are removed.
1144
1178
 
1145
1179
  ### Background work
1146
1180
 
package/README.zh-CN.md CHANGED
@@ -152,7 +152,7 @@ node install.mjs install
152
152
  ### 安装过程
153
153
 
154
154
  1. **安装依赖** -- `npm install --omit=dev`(编译原生 `better-sqlite3`)
155
- 2. **注册 MCP 服务器** -- `mem-lite` 服务器,包含 18 个工具(9 个核心通过 `tools/list` 暴露 + 9 个隐藏但可调;完整表见 Usage 段)。v2.78 前服务器名为通用的 `mem`,现已改名为 `mem-lite` 避免与用户其它 `.mcp.json` 冲突;工具名(`mem_search`/`mem_recall` 等)保持不变。
155
+ 2. **注册 MCP 服务器** -- `mem-lite` 服务器定义 19 个工具(10 个核心 + 9 个隐藏但可调;`mem_search_feedback` 仅在启用搜索遥测时注册)。v2.78 前服务器名为通用的 `mem`,现已改名为 `mem-lite` 避免与用户其它 `.mcp.json` 冲突;工具名(`mem_search`/`mem_recall` 等)保持不变。
156
156
 
157
157
  > **自动 adopt 不再把托管块加进你项目的 `CLAUDE.md`(6.19.4 之后的下一个版本起)。** 每次 SessionStart,插件都会送达引导文本(提升 Claude 主动调用 `mem_recall` / `mem_save` 的触发表)。在还没有托管块的 git 仓库里,它把托管块写进仓库根目录的 `CLAUDE.local.md`(Claude Code 像加载 `CLAUDE.md` 一样加载它,加载项目指令的子代理也能看到;内置的 Explore 和 Plan 子代理不加载这类文件),并把这个文件加进仓库的 `.git/info/exclude`(你的 ignore 规则已覆盖时不加),所以 git 不列出、也不提交它;第一次写入时你会看到一条**一次性提示**。Claude Code 在插件的启动钩子运行之前就读取了指令文件,所以创建这个文件的那个会话改为在上下文里注入一次引导(该会话的子代理看不到)。Claude Code 把某个 `AGENTS.md` 当作项目指令读取时,会话目录或上层只要有 `CLAUDE.md` 或 `CLAUDE.local.md` 它就不再读 `AGENTS.md`,所以在这种仓库里什么都不写,改为**注入**引导(子代理看不到),并给一条点名那个 `AGENTS.md` 的一次性提示。设了 `CLAUDE_MEM_RULES_STEERING=1` 时,托管块改写进仓库根目录的 `.claude/rules/claude-mem-lite.md`:同样经 `.git/info/exclude` 排除在 git 之外,Claude Code 把它当作项目指令加载(加载项目指令的子代理也能看到),而且不会让 `AGENTS.md` 失效;第一次写入时有单独的一次性提示。它不是默认做法,因为在预先登记的 A/B 里它没有达到事先定下的、胜过注入的标准(`docs/audits/20261006-d212-ab.md`)。不在 git 仓库里、仓库根是 `$HOME`、`CLAUDE.local.md` 已被 git 跟踪或是符号链接,或者仓库根是一个 `npm publish` 会把它带上的 npm 包(没有 `"private": true`,没有把它排除在外的 `files` 列表,没有 `files` 列表时也没有列出它的 `.npmignore`)时,什么都不写,改为每个会话把引导**注入**上下文,并一次性提示可以运行 `/adopt`(在 `$HOME` 下不提示)。设了这个变量但 rules 文件写不了时(路径上有符号链接、同名文件被跟踪或是你自己的文件、npm 包会把它带上)同样改为注入。`AGENTS.md` 旁边的提示会说明是哪个 `AGENTS.md`、为什么没有文件,并只给出在这里行得通的、保留 `AGENTS.md` 又能写文件的做法:在 `/config` 里把 Project instructions 设为 `claude-md-and-agents-md`,之后就会写 `CLAUDE.local.md`(`CLAUDE.local.md` 也会被拒写时不提,比如 npm 包根,或你删过的文件);运行 `/adopt`,它写的 `CLAUDE.md` 会导入同目录的 `AGENTS.md`(上层目录或子目录里的 `AGENTS.md` 会因此失效时不提)。托管块指向的详情文件放在插件自己的数据目录,以 `~/` 路径书写。`.git/info/exclude` 只对 git 生效:docker 构建上下文、归档和其他打包工具都可能把 `CLAUDE.local.md` 打进去(rules 文件同样如此;可发布的 npm 包根目录不会保留 `CLAUDE.local.md`:之前写入的托管块会在下一次会话启动时移除;在这个文件会让 `AGENTS.md` 失效的项目里,托管块会被移除、改为注入;设了 `CLAUDE_MEM_RULES_STEERING=1` 时,在写得了的情况下移到 rules 文件)。旧版本会在你打开的每个项目里,未经询问就向项目自己的 `<cwd>/CLAUDE.md`(通常会进 git)写入托管块,外加 `<cwd>/.claude/plugin_claude_mem_lite.md` 详情文件;这些项目保留托管块,本版本的第一个会话会因为文本变化刷新它——想把某个项目迁到本地文件,在那里运行 `claude-mem-lite unadopt` 并提交这次删除。为什么不全部改成注入:沙箱实测中(一个项目,Claude Opus 5.5),8 个会话里模型主动记录的次数,注入时平均 1.5 次,写在 `CLAUDE.local.md` 或 `CLAUDE.md` 时 5.25 次;而且子代理完全看不到注入的文本。**任何安装路径都生效**(npm、npx、`/plugin`、手动);这些提示都可以用 `MEM_NO_ADOPT_HINT=1` 关闭。
158
158
  >
@@ -199,6 +199,24 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
199
199
  repos/ # 浅克隆的源代码仓库
200
200
  ```
201
201
 
202
+ ## 升级到 6.23.1
203
+
204
+ **如果你设置了 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_API_KEY`,后台 LLM 调用现在会发往那个网关。**
205
+ 没有 schema 版本变更:本版本打开过的数据库,6.22.1 仍能打开。想保持旧行为,请停留在 6.22.1;已经更新的插件安装可以按英文
206
+ README "Trust model per install path" 一节的回退步骤操作。
207
+
208
+ - **受影响的是谁。** 以前直连 API 这一路不管 `ANTHROPIC_BASE_URL` 是什么,都用内置模型 ID 发往
209
+ `api.anthropic.com`,只有 `claude -p` 兜底会跟随它。现在直连这一路也用它,同时采用
210
+ `ANTHROPIC_DEFAULT_HAIKU_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL`。没设 `ANTHROPIC_BASE_URL`
211
+ 时一切不变,即使设了这两个模型变量也一样。
212
+ - **插件用不了的值**(无法解析成 URL、不是 http 或 https、对非回环主机用明文 http、URL 里带凭据、
213
+ 带查询串或片段)会跳过直连这一路:所有后台调用改走 `claude -p`(它自己也会读取
214
+ `ANTHROPIC_BASE_URL`),`claude-mem-lite doctor` 会说明原因。`doctor` 现在探测的是网关的主机和端口,
215
+ 而不是 `api.anthropic.com`。
216
+ - **本版本还有:** 数据量大的存储不再每次提示都超时。数据库现在会有 SQLite 的查询规划统计,因此会多出
217
+ SQLite 自己的 `sqlite_stat1` / `sqlite_stat4` 表。另外新增默认关闭的开关
218
+ `CLAUDE_MEM_SEARCH_TELEMETRY=1`,用来记录 MCP 搜索以便给相关性打分。详见 CHANGELOG.md。
219
+
202
220
  ## 升级到 6.21.0
203
221
 
204
222
  **名字不是纯 ASCII 的项目会换一个新标识,它们存下的内容会搬一次。** 没有 schema 版本变更:
@@ -402,8 +420,8 @@ v48"*。想继续用向量臂,请在**升级之前**锁定 `claude-mem-lite@5.
402
420
 
403
421
  ### MCP 工具
404
422
 
405
- v2.34.0 起服务端只把一部分工具暴露给 `tools/list`。当前是 18 个工具,其中 9 个
406
- **核心** 工具出现在列表里,另外 9 个 **隐藏** 工具仍然注册在 MCP 层(按名
423
+ v2.34.0 起服务端只把一部分工具暴露给 `tools/list`。服务端定义 19 个工具,其中 9 个
424
+ **核心** 工具默认出现在列表里,`mem_search_feedback` 仅在启用搜索遥测时加入;另外 9 个 **隐藏** 工具仍然注册在 MCP 层(按名
407
425
  `tools/call` 仍命中),只是不出现在列表响应里,以避免 Claude Code 会话启动时
408
426
  多加载 9 份工具 schema。隐藏工具走下面表格的 CLI 入口。
409
427
 
@@ -411,11 +429,12 @@ v2.34.0 起服务端只把一部分工具暴露给 `tools/list`。当前是 18
411
429
  `tool-schemas.mjs` 是唯一事实来源,`tests/tool-count-docs.test.mjs` 现在把两份
412
430
  README 和 `docs/ARCHITECTURE.md` 都钉在它上面。)
413
431
 
414
- **核心(9 个,暴露给 Claude Code)**
432
+ **核心(10 个定义;`mem_search_feedback` 需要启用搜索遥测)**
415
433
 
416
434
  | 工具 | 描述 |
417
435
  |------|------|
418
436
  | `mem_search` | 基于 BM25 排名的 FTS5 全文搜索。支持按类型、项目、日期范围、重要度过滤。 |
437
+ | `mem_search_feedback` | 为已有 `mem_search` Search ID 的结果记录稀疏相关性标签。 |
419
438
  | `mem_recent` | 显示最近的观察,按时间排序。快速查看最新活动。 |
420
439
  | `mem_recall` | 召回与文件相关的观察。编辑文件前使用,回顾过去的修复和上下文。 |
421
440
  | `mem_timeline` | 围绕锚点按时间顺序浏览观察。 |
@@ -811,7 +830,10 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
811
830
  |------|------|--------|
812
831
  | `CLAUDE_MEM_DIR` | 自定义数据目录。所有数据库与运行时文件均存储在此。 | `~/.claude-mem-lite/` |
813
832
  | `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
814
- | `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching),优先级最高。 | _(未设 → CLI)_ |
833
+ | `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching);设了 `ANTHROPIC_BASE_URL` 时改发往那个网关。优先级最高。 | _(未设 → CLI)_ |
834
+ | `ANTHROPIC_BASE_URL` | 由兼容 Anthropic 的网关(Azure AI Foundry、LiteLLM、Bedrock/Vertex 代理)提供模型时,直连 Messages API 使用的基础 URL。不要带 `/v1` 后缀,端点路径会自动拼上。`claude -p` 兜底读取同一个变量,所以一个值同时作用于两条路径。明文 `http://` 只接受回环主机,否则 API key 会以明文发送。设了但不可用的值会跳过直连这一路,后台调用改走 `claude -p`,doctor 会提示。 | `https://api.anthropic.com` |
835
+ | `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-4-5-…` |
836
+ | `ANTHROPIC_DEFAULT_SONNET_MODEL` | 同上,用于 `sonnet` 档。 | 内置 `claude-sonnet-4-5-…` |
815
837
  | `OPENROUTER_API_KEY` | OpenRouter API key(OpenAI 兼容)。当**未设** `ANTHROPIC_API_KEY` 时用于后台 LLM 调用;两者都未设则回退到 `claude -p` CLI。 | _(未设)_ |
816
838
  | `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-4.5`(haiku)或 `anthropic/claude-sonnet-4.5`(sonnet)。 | _(分层默认)_ |
817
839
  | `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
@@ -824,6 +846,12 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
824
846
  | `MEM_NO_AUTO_ADOPT` | auto-adopt 全局关闭开关(v2.82.0+)。设为 `1` 在**所有**项目停止自动 adopt——不注入引导文本、不新建本地文件(`CLAUDE.local.md` 或 `.claude/rules/claude-mem-lite.md`),也不同步已有的 `CLAUDE.md` 或本地托管块(这些托管块会保留并继续加载,直到 `claude-mem-lite unadopt`)。项目级关闭走 `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除)。 | _(禁用)_ |
825
847
  | `MEM_NO_ADOPT_HINT` | 静音项目第一次得到 `CLAUDE.local.md`、或第一次以注入方式被引导时给你的一次性提示(后者建议运行 `/adopt`,在 `$HOME` 下除外;在这个文件会让 `AGENTS.md` 失效的项目里,还会说明 `AGENTS.md` 的情况和相应设置)、引导写进 `.claude/rules/claude-mem-lite.md` 时的一次性提示,SessionStart 给只有托管块的 `CLAUDE.md` 补上 `AGENTS.md` 导入时的一次性提示,以及当前项目未 adopt 时 SessionStart 追加的那一行 "Invited-memory 未启用…" 提示。v2.82.1 起任何安装路径每次 SessionStart 都自动 adopt,所以该提示一般只在你显式 opt out(`MEM_NO_AUTO_ADOPT=1` 或 `claude-mem-lite adopt --disable`)的项目才会出现。 | _(禁用)_ |
826
848
  | `CLAUDE_MEM_RULES_STEERING` | 设为 `1` 时,在 `CLAUDE.local.md` 会让 Claude Code 不再读项目 `AGENTS.md` 的仓库里,自动 adopt 把托管块写进 `.claude/rules/claude-mem-lite.md`(排除在 git 之外)而不是注入;子代理能看到它,`AGENTS.md` 也继续加载。开着时写下的文件之后一直是引导渠道。默认关闭:预先登记的 A/B 没有显示出达到标准的、胜过注入的效果(`docs/audits/20261006-d212-ab.md`)。 | _(注入)_ |
849
+ | `CLAUDE_MEM_SEARCH_TELEMETRY` | 设为 `1` 时记录 MCP `mem_search` 的查询和结果,输出 Search ID 供可选的 `mem_search_feedback` 打分,并给每次搜索标上当时运行的 claude-mem-lite 版本。 | _(关闭)_ |
850
+
851
+ 搜索遥测记录会一直保留,直到你删除。用 `claude-mem-lite stats --search-telemetry` 查看数量;在所配置的
852
+ SQLite 数据库(通常是 `~/.claude-mem-lite/claude-mem-lite.db`)里执行
853
+ `DELETE FROM search_results; DELETE FROM search_runs; VACUUM;` 清除。数据库旁边删除前留下的 `.bak`
854
+ 快照,在被删掉之前仍保有之前的内容。
827
855
 
828
856
  ## 许可证
829
857
 
package/cli/common.mjs CHANGED
@@ -374,6 +374,7 @@ export const KNOWN_CLI_FLAGS = new Set([
374
374
  'run',
375
375
  'run-all',
376
376
  'scope',
377
+ 'search-telemetry',
377
378
  'session-audit',
378
379
  'sidechain',
379
380
  'since',
@@ -431,6 +432,7 @@ export const COMMAND_SCOPED_FLAGS = new Map([
431
432
  ['chars', 'context'],
432
433
  ['digest', 'verify-apply'],
433
434
  ['print-project', 'verify-apply'],
435
+ ['search-telemetry', 'stats'],
434
436
  ['undo', 'verify-apply'],
435
437
  ]);
436
438
 
package/haiku-client.mjs CHANGED
@@ -3,7 +3,14 @@
3
3
  // Provider priority: ANTHROPIC_API_KEY (direct Anthropic API) →
4
4
  // OPENROUTER_API_KEY (OpenRouter, OpenAI-compatible) → claude CLI fallback
5
5
  // Model configurable via CLAUDE_MEM_MODEL (haiku|sonnet); OpenRouter slug
6
- // overridable via OPENROUTER_MODEL
6
+ // overridable via OPENROUTER_MODEL. The direct-API leg honours
7
+ // ANTHROPIC_BASE_URL (no /v1 suffix — the path is appended) and, when that base
8
+ // URL is set and usable, per-tier deployment names via
9
+ // ANTHROPIC_DEFAULT_{HAIKU,SONNET}_MODEL — the same vars the `claude` CLI leg
10
+ // resolves its --model aliases through, so one env set points both transports at
11
+ // a gateway (Azure AI Foundry, LiteLLM, Bedrock proxies). Without a base URL the
12
+ // tier vars stay CLI-only: they are also the Bedrock/Vertex aliases and must not
13
+ // leak to api.anthropic.com. Unset base URL → public Anthropic API, unchanged.
7
14
 
8
15
  import { execFileSync, spawn } from 'child_process';
9
16
  import { mkdirSync } from 'fs';
@@ -14,6 +21,7 @@ import { debugLog, debugCatch, parseJsonFromLLM } from './utils.mjs';
14
21
  import { DB_DIR } from './schema.mjs';
15
22
  import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
16
23
  import { httpConnectProxyFor, postViaConnectProxy } from './lib/proxy-fetch.mjs';
24
+ import { resolveAnthropicBaseUrl } from './lib/anthropic-base-url.mjs';
17
25
 
18
26
  /**
19
27
  * cwd for every `claude -p` spawn. R10 P2-13.
@@ -48,6 +56,34 @@ const MODEL_MAP = {
48
56
  sonnet: 'claude-sonnet-4-5-20250929',
49
57
  };
50
58
 
59
+ // Claude Code's tier-alias override vars, honored on the direct-API leg too:
60
+ // gateway deployments route on the DEPLOYMENT name (Azure Foundry rejects the
61
+ // Anthropic model ID when it differs), and one env set then covers both the
62
+ // keyed API call and the `claude -p` fallback.
63
+ const TIER_MODEL_ENV = {
64
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
65
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
66
+ };
67
+
68
+ /**
69
+ * Model ID the direct Messages API should send for a tier.
70
+ *
71
+ * Scoped to a usable ANTHROPIC_BASE_URL on purpose: these vars are ALSO the
72
+ * Claude Code aliases on Bedrock/Vertex, where the value is a provider-specific
73
+ * ID. Someone who set them for the CLI and has an ANTHROPIC_API_KEY would
74
+ * otherwise send that ID straight to api.anthropic.com (a 404 + CLI fallback on
75
+ * every call), and pointing the haiku alias at a bigger model would silently
76
+ * change model and cost. No base URL → the built-in Anthropic ID, as before.
77
+ * @param {'haiku'|'sonnet'} tier
78
+ * @param {ReturnType<typeof resolveAnthropicBaseUrl>} [base] the resolved base URL;
79
+ * callModelAPI passes the one it also builds the request URL from
80
+ * @returns {string}
81
+ */
82
+ function apiModelId(tier, base = resolveAnthropicBaseUrl()) {
83
+ if (!base.configured || base.error) return MODEL_MAP[tier];
84
+ return (process.env[TIER_MODEL_ENV[tier]] || '').trim() || MODEL_MAP[tier];
85
+ }
86
+
51
87
  // Every background LLM call here is fixed-schema extraction / classification
52
88
  // (episode→JSON, type/merge classification, synonym + metadata extraction) whose
53
89
  // output is consumed deterministically (JSON.parse, MinHash dedup). Pin temperature
@@ -81,12 +117,13 @@ export const BG_LLM_TIMEOUT_MS = 45000;
81
117
  /**
82
118
  * Resolve the LLM model to use for background calls.
83
119
  * Reads CLAUDE_MEM_MODEL env var, defaults to 'haiku'.
84
- * @returns {{ cli: string, api: string }} CLI name and API model ID
120
+ * @returns {{ cli: string, api: string }} CLI name and API model ID (tier
121
+ * deployment-name override when set, else the built-in Anthropic ID)
85
122
  */
86
123
  export function resolveModel() {
87
124
  const raw = (process.env.CLAUDE_MEM_MODEL || 'haiku').toLowerCase().trim();
88
125
  const cli = MODEL_MAP[raw] ? raw : 'haiku';
89
- const api = MODEL_MAP[cli];
126
+ const api = apiModelId(cli);
90
127
  return { cli, api };
91
128
  }
92
129
 
@@ -450,22 +487,52 @@ export async function callModelJSONAsync(
450
487
  return res?.text ? parseJsonFromLLM(res.text) : null;
451
488
  }
452
489
 
490
+ // Models that refused `temperature` with a 400 (see the retry in callModelAPI).
491
+ // One name after the first rejection, so the cost is one failed request per
492
+ // model per process rather than one per call.
493
+ const _temperatureDeprecated = new Set();
494
+
495
+ /** @internal test hook — module-level compat state must not leak across cases. */
496
+ export function _resetTemperatureCompat() {
497
+ _temperatureDeprecated.clear();
498
+ }
499
+
453
500
  async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE }) {
454
501
  const apiKey = process.env.ANTHROPIC_API_KEY;
455
502
  if (!apiKey) return null;
456
503
 
457
- const modelId = MODEL_MAP[model];
504
+ // Read the base URL once: the request URL and the tier model must come from the
505
+ // same verdict, and a set-but-unusable value must not quietly move the key to
506
+ // the public endpoint (the cross-origin hop the redirect guard exists to stop).
507
+ // Skip the direct leg; the CLI fallback reads ANTHROPIC_BASE_URL itself.
508
+ const base = resolveAnthropicBaseUrl();
509
+ if (base.configured && base.error) {
510
+ debugLog('WARN', `${model}-api`, `ANTHROPIC_BASE_URL unusable (${base.error}); skipping direct API call`);
511
+ return null;
512
+ }
513
+
514
+ const modelId = apiModelId(model, base);
458
515
  const controller = new AbortController();
459
516
  const timer = setTimeout(() => controller.abort(), timeout);
517
+ // One deadline for the whole call. The AbortController bounds the fetch path
518
+ // across the temperature retry; the proxy tunnel takes what is LEFT of the
519
+ // deadline per send, so a retry cannot buy a second full timeout (a 1000ms
520
+ // budget that took 1633ms on the tunnel path). (pre-tag review on #33)
521
+ const deadline = Date.now() + timeout;
460
522
 
461
523
  try {
462
524
  const { system, user } = splitPrompt(prompt);
463
525
  const body = {
464
526
  model: modelId,
465
527
  max_tokens: maxTokens,
466
- temperature,
467
528
  messages: [{ role: 'user', content: user }],
468
529
  };
530
+ // The 0-pin below is for JSON determinism, but newer models deprecate the
531
+ // field outright (Azure Foundry's claude-sonnet-5 answers 400
532
+ // "`temperature` is deprecated for this model."). Keep sending it until a
533
+ // model refuses, then drop it for that model only - the retry at the bottom
534
+ // pays for itself once.
535
+ if (!_temperatureDeprecated.has(modelId)) body.temperature = temperature;
469
536
  // System slot is constant per call type (instructions, schema, type taxonomy)
470
537
  // — mark it cache_control:ephemeral so repeated calls within the 5-min cache
471
538
  // window pay the cached-input rate (~0.10× base). Sub-1024-token systems still
@@ -480,25 +547,60 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
480
547
  // fetch — a silent outage behind a proxy, and one the new doctor check would
481
548
  // have certified as healthy because it probes the hop this code was ASSUMED
482
549
  // to use. (pre-tag review SHOULD-FIX 3)
483
- const apiUrl = 'https://api.anthropic.com/v1/messages';
550
+ const apiUrl = `${base.url}/v1/messages`;
484
551
  const apiHeaders = {
485
552
  'Content-Type': 'application/json',
486
553
  'x-api-key': apiKey,
487
554
  'anthropic-version': '2023-06-01',
488
555
  };
489
- const apiProxy = httpConnectProxyFor(apiUrl);
490
- const res = apiProxy
491
- ? await postViaConnectProxy(apiProxy, apiUrl, {
492
- headers: apiHeaders,
493
- body: JSON.stringify(body),
494
- timeout,
495
- })
496
- : await fetch(apiUrl, {
497
- method: 'POST',
498
- headers: apiHeaders,
499
- body: JSON.stringify(body),
500
- signal: controller.signal,
501
- });
556
+ const send = (payload) => {
557
+ const json = JSON.stringify(payload);
558
+ const apiProxy = httpConnectProxyFor(apiUrl);
559
+ return apiProxy
560
+ ? postViaConnectProxy(apiProxy, apiUrl, {
561
+ headers: apiHeaders,
562
+ body: json,
563
+ timeout: Math.max(1, deadline - Date.now()),
564
+ })
565
+ : fetch(apiUrl, {
566
+ method: 'POST',
567
+ headers: apiHeaders,
568
+ body: json,
569
+ signal: controller.signal,
570
+ // A gateway must not be able to walk the x-api-key to another origin
571
+ // via a redirect. The tunnel path already surfaces a 3xx without
572
+ // following it; reject here so the native path behaves the same.
573
+ redirect: 'error',
574
+ });
575
+ };
576
+
577
+ let res = await send(body);
578
+
579
+ // Temperature-deprecation compat. Unlike the claude-CLI flag retry, which
580
+ // caches the negative only after the retry succeeds, this pins the model the
581
+ // moment the 400 matches: the 400 itself proves the model refuses the field,
582
+ // so every later call omits it from the start. The body must name the
583
+ // BACKTICKED field — matching bare "temperature" also catches unrelated 400s
584
+ // that merely mention it, and a false match costs a request plus the 0-pin
585
+ // for that model for the rest of the process. A 400 that does not name the
586
+ // field keeps the old single-attempt path.
587
+ if (res.status === 400 && body.temperature !== undefined) {
588
+ let detail = '';
589
+ try {
590
+ detail = (await res.text?.()) || '';
591
+ } catch {
592
+ /* body already gone - treat as a non-matching 400 */
593
+ }
594
+ if (/`temperature`/i.test(detail) && /deprecat|unsupported|not support/i.test(detail)) {
595
+ _temperatureDeprecated.add(modelId);
596
+ // Only spend the second request if real budget is left; otherwise the
597
+ // gateway bills a POST the client aborts before reading it.
598
+ if (deadline - Date.now() >= RETRY_MIN_BUDGET_MS) {
599
+ delete body.temperature;
600
+ res = await send(body);
601
+ }
602
+ }
603
+ }
502
604
 
503
605
  if (!res.ok) {
504
606
  debugLog('WARN', `${model}-api`, `HTTP ${res.status}`);
@@ -606,8 +708,10 @@ const FLAG_TOKEN = /no-session-persistence/;
606
708
  const PARSE_REJECTION =
607
709
  /(unknown|unrecognized|unsupported|invalid|unexpected)[^\n]{0,40}(option|argument|flag|switch)/i;
608
710
 
609
- // Below this many ms left, a retry can only spawn a process and immediately kill
610
- // it — worse than returning the original failure.
711
+ // Below this many ms left, a retry is worse than returning the original failure:
712
+ // the CLI retry can only spawn a process and immediately kill it, and the direct
713
+ // API's temperature retry would send a request (billed by the gateway) whose
714
+ // reply nobody waits for.
611
715
  const RETRY_MIN_BUDGET_MS = 500;
612
716
 
613
717
  export function _isUnknownFlagError(diagnostic) {
package/hook.mjs CHANGED
@@ -76,7 +76,7 @@ import {
76
76
  } from './hook-episode.mjs';
77
77
  // CODE_DIR, not DB_DIR: the schema-skew notice asks which CODE homes exist, and those are
78
78
  // always homedir-rooted even when CLAUDE_MEM_DIR relocates the data.
79
- import { DB_DIR, DB_PATH, CODE_DIR } from './schema.mjs';
79
+ import { DB_DIR, DB_PATH, CODE_DIR, refreshPlannerStats } from './schema.mjs';
80
80
  import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
81
81
  import { entry as preCompactEntry } from './hook-precompact.mjs';
82
82
  import {
@@ -2237,6 +2237,10 @@ function runSessionStartAutoMaintain(db, project) {
2237
2237
  debugCatch(e, 'auto-maintain-handoff-gc');
2238
2238
  }
2239
2239
 
2240
+ // Planner statistics, the "once a day" half of SQLite's recommendation (#41). Last, so
2241
+ // the purge/dedup above have already moved the row counts it compares. Never throws.
2242
+ refreshPlannerStats(db);
2243
+
2240
2244
  // Mark maintenance as done (24h gate) — even though compression runs in background
2241
2245
  writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
2242
2246
  // Weekly summary grouping runs in background to avoid blocking SessionStart
@@ -0,0 +1,97 @@
1
+ // lib/anthropic-base-url.mjs - one resolver for ANTHROPIC_BASE_URL.
2
+ //
3
+ // Two callers need the same answer and used to derive it differently:
4
+ // haiku-client's callModelAPI builds the request URL from it, and
5
+ // lib/llm-provider-probe.mjs picks the host + port doctor probes. When the two
6
+ // disagree, doctor certifies a hop the product never uses - the false-green
7
+ // shape this seam exists to remove. So the trim, the URL parse, and the
8
+ // validity verdict live here and both sites read the same result.
9
+ //
10
+ // ANTHROPIC_BASE_URL is the Claude Code / Anthropic SDK convention: an origin
11
+ // (+ optional /anthropic path on Azure Foundry) with NO /v1 suffix - callers
12
+ // append the endpoint path. Unset or blank keeps the public API, which is the
13
+ // behaviour before the gateway override existed.
14
+
15
+ import net from 'node:net';
16
+
17
+ const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
18
+
19
+ // Plain http:// is allowed only to loopback. Anywhere else it puts the
20
+ // x-api-key on the wire unencrypted (and prompts in the clear), so that value
21
+ // is refused rather than silently honoured.
22
+ function isLoopbackHost(host) {
23
+ return (
24
+ host === 'localhost' ||
25
+ host.endsWith('.localhost') ||
26
+ host === '::1' ||
27
+ host === '[::1]' ||
28
+ // A real dotted-quad loopback, not a DNS name that merely starts "127."
29
+ // ("127.attacker.example" resolves wherever its owner says).
30
+ (net.isIP(host) === 4 && host.startsWith('127.'))
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Resolve the configured base URL into a usable origin + path.
36
+ *
37
+ * Never throws and always returns a well-formed `url` a caller can append
38
+ * `/v1/messages` to: blank / unset keeps the public default, and a value that is
39
+ * set but unusable (unparseable, non-http(s) scheme, userinfo, a query string or
40
+ * fragment, or plain http to a non-loopback host) also returns the default so
41
+ * callers always get a parseable value. `error` names why; the direct API leg
42
+ * ignores the fallback and skips itself when a configured value is unusable, and
43
+ * doctor surfaces the same reason instead of probing a hop nothing uses.
44
+ *
45
+ * @returns {{url: string, configured: boolean, error: string|null}}
46
+ * url normalized origin+path, no trailing slash
47
+ * configured ANTHROPIC_BASE_URL was set to a non-blank value
48
+ * error why a configured value is unusable, or null when it is fine
49
+ */
50
+ export function resolveAnthropicBaseUrl() {
51
+ const raw = (process.env.ANTHROPIC_BASE_URL || '').trim();
52
+ if (!raw) return { url: DEFAULT_ANTHROPIC_BASE_URL, configured: false, error: null };
53
+
54
+ let parsed;
55
+ try {
56
+ parsed = new URL(raw);
57
+ } catch {
58
+ return {
59
+ url: DEFAULT_ANTHROPIC_BASE_URL,
60
+ configured: true,
61
+ error: 'ANTHROPIC_BASE_URL is not a valid URL',
62
+ };
63
+ }
64
+ if (parsed.username || parsed.password) {
65
+ // fetch() refuses to build a request with credentials in the URL, so every
66
+ // call would fall back to the CLI while doctor reported the host reachable.
67
+ return {
68
+ url: DEFAULT_ANTHROPIC_BASE_URL,
69
+ configured: true,
70
+ error: 'ANTHROPIC_BASE_URL must not include userinfo (user:pass@)',
71
+ };
72
+ }
73
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
74
+ return {
75
+ url: DEFAULT_ANTHROPIC_BASE_URL,
76
+ configured: true,
77
+ error: `ANTHROPIC_BASE_URL must use http or https (got ${parsed.protocol})`,
78
+ };
79
+ }
80
+ // Raw string, not URL.search/hash: a bare trailing "?" or "#" leaves both
81
+ // empty while still swallowing the appended path into the query.
82
+ if (/[?#]/.test(raw)) {
83
+ return {
84
+ url: DEFAULT_ANTHROPIC_BASE_URL,
85
+ configured: true,
86
+ error: 'ANTHROPIC_BASE_URL must not include a query string or fragment',
87
+ };
88
+ }
89
+ if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
90
+ return {
91
+ url: DEFAULT_ANTHROPIC_BASE_URL,
92
+ configured: true,
93
+ error: `ANTHROPIC_BASE_URL uses plain http to a non-loopback host (${parsed.hostname}); the direct API leg will not send the API key there unencrypted, but the claude CLI fallback reads the same variable and still does`,
94
+ };
95
+ }
96
+ return { url: raw.replace(/\/+$/, ''), configured: true, error: null };
97
+ }
@@ -86,6 +86,11 @@ export function deleteObservations(db, ids, { snapshotTag = 'pre-delete' } = {})
86
86
  WHERE superseded_by IN (${placeholders}) AND id NOT IN (${placeholders})`,
87
87
  )
88
88
  .run(...ids, ...ids).changes;
89
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'search_results'").get()) {
90
+ db.prepare(
91
+ `UPDATE search_results SET snapshot_label = NULL WHERE source = 'obs' AND result_id IN (${placeholders})`,
92
+ ).run(...ids);
93
+ }
89
94
  // Execute deletion (FTS5 cleanup handled by the observations_ad trigger).
90
95
  const deleted = db.prepare(`DELETE FROM observations WHERE id IN (${placeholders})`).run(...ids);
91
96
  return { changes: deleted.changes, recovered, restoredSuperseded };