claude-mem-lite 3.74.1 → 3.75.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 +122 -4
- package/README.zh-CN.md +18 -10
- package/haiku-client.mjs +7 -58
- package/hook-context.mjs +4 -7
- package/hook-episode.mjs +44 -20
- package/hook-llm.mjs +7 -2
- package/hook-memory.mjs +25 -10
- package/hook.mjs +132 -178
- package/hooks/hooks.json +1 -1
- package/install.mjs +35 -2
- package/lib/citation-tracker.mjs +67 -51
- package/lib/cite-back-hint.mjs +4 -18
- package/lib/doctor-drift.mjs +8 -1
- package/lib/fast-summary.mjs +87 -0
- package/lib/get-core.mjs +35 -0
- package/lib/maintain-core.mjs +95 -1
- package/lib/persist-reminder.mjs +8 -8
- package/lib/registry-core.mjs +94 -0
- package/lib/resolve-data-dir.mjs +53 -4
- package/lib/summary-extractor.mjs +2 -8
- package/lib/task-imperative.mjs +8 -1
- package/lib/transcript-scan.mjs +66 -0
- package/lib/ups-query.mjs +26 -0
- package/mem-cli.mjs +53 -44
- package/memdir.mjs +5 -1
- package/nlp.mjs +20 -2
- package/npm-shrinkwrap.json +3 -2
- package/package.json +7 -1
- package/registry-recommend.mjs +30 -4
- package/schema.mjs +3 -1
- package/scoring-sql.mjs +21 -7
- package/scripts/pre-agent-inject.sh +43 -0
- package/scripts/user-prompt-search.js +8 -3
- package/server.mjs +30 -38
- package/source-files.mjs +15 -0
- package/utils.mjs +13 -5
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.75.1",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.75.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`claude-mem-lite` is a **persistent memory** (also called *long-term memory* or *cross-session context*) system for **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — Anthropic's CLI coding agent. It runs as an **[MCP](https://modelcontextprotocol.io/) server** plus a set of Claude Code hooks, automatically capturing coding observations, decisions, and bug fixes during sessions, then providing hybrid full-text + semantic search to recall them later.
|
|
6
6
|
|
|
7
|
-
Compared to general-purpose LLM memory frameworks like [`mem0`](https://github.com/mem0ai/mem0) or the MCP reference [`memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) server, claude-mem-lite is purpose-built for Claude Code's hook lifecycle: episode batching cuts LLM calls 7–10× vs the original [claude-mem](https://github.com/thedotmack/claude-mem) (an estimated ~600× lower total cost — see the cost model below; this is an architecture estimate, not a measured benchmark), while the hybrid FTS5 + TF-IDF retriever benchmarks at 0.
|
|
7
|
+
Compared to general-purpose LLM memory frameworks like [`mem0`](https://github.com/mem0ai/mem0) or the MCP reference [`memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) server, claude-mem-lite is purpose-built for Claude Code's hook lifecycle: episode batching cuts LLM calls 7–10× vs the original [claude-mem](https://github.com/thedotmack/claude-mem) (an estimated ~600× lower total cost — see the cost model below; this is an architecture estimate, not a measured benchmark), while the hybrid FTS5 + TF-IDF retriever benchmarks at 0.90 Recall@10 / 0.85 Precision@10
|
|
8
|
+
(see [Search Quality](#search-quality) for the reproduction command).
|
|
8
9
|
|
|
9
10
|
> 中文简介:claude-mem-lite 是 Claude Code 的轻量级**持久化记忆 / 长期记忆 / 跨会话上下文**插件,基于 MCP 协议 + 钩子机制,自动捕获编码会话中的决策、修复和上下文,并通过 FTS5 + TF-IDF 混合检索召回。详见 [中文 README](README.zh-CN.md)。
|
|
10
11
|
|
|
@@ -681,10 +682,16 @@ path and fails on regression.
|
|
|
681
682
|
| Metric | Score (production-hybrid) |
|
|
682
683
|
|--------|---------------------------|
|
|
683
684
|
| Recall@10 | 0.90 |
|
|
684
|
-
| Precision@10 | 0.
|
|
685
|
+
| Precision@10 | 0.85 |
|
|
685
686
|
| nDCG@10 | 0.97 |
|
|
686
|
-
| MRR@10 | 0.
|
|
687
|
-
| P95 search latency | ~
|
|
687
|
+
| MRR@10 | 0.96 |
|
|
688
|
+
| P95 search latency | ~1.8ms |
|
|
689
|
+
|
|
690
|
+
> **Where these numbers come from.** Reproduce with
|
|
691
|
+
> `node benchmark/benchmark.mjs --production-hybrid` (deterministic — same fixture corpus,
|
|
692
|
+
> same query set, no sampling). The CI reference capture is `benchmark/baseline.json`, and
|
|
693
|
+
> `npm run benchmark:gate` fails the build when a run drifts more than 5% from it. This is
|
|
694
|
+
> the single source for every retrieval figure quoted in this README.
|
|
688
695
|
|
|
689
696
|
> **Note on the path measured.** Earlier versions of this table reported the *lexical*
|
|
690
697
|
> FTS-only path (Precision@10 0.96, P95 0.15ms). The hybrid vector arm trades raw
|
|
@@ -761,6 +768,12 @@ npm run benchmark:gate # CI gate: fails if metrics regress beyond 5% toleranc
|
|
|
761
768
|
|
|
762
769
|
## Environment Variables
|
|
763
770
|
|
|
771
|
+
Every environment variable the shipped code reads is listed below, grouped by what it
|
|
772
|
+
controls. Booleans accept `1` unless noted. Anything not listed here is not read by
|
|
773
|
+
claude-mem-lite.
|
|
774
|
+
|
|
775
|
+
### Core
|
|
776
|
+
|
|
764
777
|
| Variable | Description | Default |
|
|
765
778
|
|----------|-------------|---------|
|
|
766
779
|
| `CLAUDE_MEM_DIR` | Custom data directory. All databases, runtime files, and managed resources are stored here. | `~/.claude-mem-lite/` |
|
|
@@ -773,6 +786,111 @@ npm run benchmark:gate # CI gate: fails if metrics regress beyond 5% toleranc
|
|
|
773
786
|
| `MEM_NO_AUTO_ADOPT` | Global opt-out for auto-adopt (v2.82.0+). `1` prevents the first-SessionStart auto-write of the invited-memory sentinel across **all** projects. For per-project opt-out use `claude-mem-lite adopt --disable` instead (writes a durable `<memdir>/.mem-no-auto-adopt` sentinel that survives marker deletion). | _(disabled)_ |
|
|
774
787
|
| `MEM_NO_ADOPT_HINT` | Silences the one-line "Invited-memory 未启用:`claude-mem-lite adopt`…" hint that SessionStart appends when the current project hasn't been adopted. Since v2.82.1 auto-adopt fires on first SessionStart for any install path, so this hint typically surfaces only when you've explicitly opted out (`MEM_NO_AUTO_ADOPT=1` or `claude-mem-lite adopt --disable`). | _(disabled)_ |
|
|
775
788
|
|
|
789
|
+
### What gets injected into your context
|
|
790
|
+
|
|
791
|
+
| Variable | Description | Default |
|
|
792
|
+
|----------|-------------|---------|
|
|
793
|
+
| `CLAUDE_MEM_ALL_TOOLS` | `1` exposes all 20 MCP tools in `tools/list` instead of the 9 core ones (pre-v2.34.0 behavior). The 11 hidden tools stay callable by exact name either way. | _(9 core)_ |
|
|
794
|
+
| `CLAUDE_MEM_FILE_INTEL` | `0` disables the file-intel block injected before `Read` (past observations about the file you are about to open). | _(on)_ |
|
|
795
|
+
| `CLAUDE_MEM_FILE_INTEL_MIN_TOKENS` | Files smaller than this stay silent — file-intel only pays for itself on large files. | `800` |
|
|
796
|
+
| `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)_ |
|
|
797
|
+
| `CLAUDE_MEM_REREAD_MIN_TOKENS` | Token floor below which the re-read guard stays silent. | `600` |
|
|
798
|
+
| `CLAUDE_MEM_PRETOOL_NUDGE` | `1` extends the pre-tool recall nudge from `Read` to other tools. | _(Read only)_ |
|
|
799
|
+
| `CLAUDE_MEM_KEEP_LOW_SIGNAL` | `1` keeps low-signal observations that the deterministic filter would otherwise drop before dedup/vector work. | _(filtered)_ |
|
|
800
|
+
| `CLAUDE_MEM_NO_TEMPLATE_REFRESH` | `1` stops SessionStart from refreshing the adopted `CLAUDE.md` managed block when the shipped template changes. | _(refreshes)_ |
|
|
801
|
+
| `MEM_QUIET_HOOKS` | See Core above — the broadest injection-volume switch. | _(disabled)_ |
|
|
802
|
+
|
|
803
|
+
### Retrieval tuning
|
|
804
|
+
|
|
805
|
+
Prompt-time search (`UPS_*` = the UserPromptSubmit surface). Defaults are the values the
|
|
806
|
+
benchmark and A/B harness are calibrated against — changing them invalidates the numbers in
|
|
807
|
+
[Search Quality](#search-quality).
|
|
808
|
+
|
|
809
|
+
| Variable | Description | Default |
|
|
810
|
+
|----------|-------------|---------|
|
|
811
|
+
| `CLAUDE_MEM_UPS_MAX_RESULTS` | Max memories injected per prompt. | `3` |
|
|
812
|
+
| `CLAUDE_MEM_UPS_REQUIRE_SIGNAL` | `0` restores always-search; by default the prompt must carry an explicit retrieval signal. | _(signal required)_ |
|
|
813
|
+
| `CLAUDE_MEM_UPS_BM25_MIN` | BM25 relevance floor for a result to be injected. | `1e-5` |
|
|
814
|
+
| `CLAUDE_MEM_UPS_BM25_MIN_FOLLOWUP` | Looser floor for follow-up prompts inside an already-injected session. | `5e-6` |
|
|
815
|
+
| `CLAUDE_MEM_UPS_OR_BM25_MIN` | Floor applied to the OR-fallback arm (looser query, needs a stricter floor). | `30` |
|
|
816
|
+
| `CLAUDE_MEM_UPS_TOP_MIN` | Minimum score for the top hit; `0` disables (useful on tiny test corpora). | `50` |
|
|
817
|
+
| `CLAUDE_MEM_UPS_FLOOR_REF_CORPUS` | Reference corpus size the score floors are normalized against, so a fresh install with few rows is not silently gated to zero injections. | `584` |
|
|
818
|
+
| `CLAUDE_MEM_UPS_IDENTIFIER_BYPASS` | `0` disables the bypass that lets an exact identifier match skip the score floors. | _(on)_ |
|
|
819
|
+
| `CLAUDE_MEM_UPS_PROMPT_FALLBACK_LIMIT` | How many past-prompt rows the fallback arm may return. | `1` |
|
|
820
|
+
| `MEM_COVERAGE_THRESHOLD` | Fraction of query terms a memory must cover to qualify (∈ [0,1]). | `0.4` |
|
|
821
|
+
| `MEM_CROSS_PROJECT_BOOST` | Multiplier for matches from other projects (∈ [0,1]); raise it for installs that want more cross-project sharing. | `0.4` |
|
|
822
|
+
| `MEM_OR_FALLBACK_MAX_TOKENS` | Max query tokens allowed into the OR fallback (∈ [0,50]). | `8` |
|
|
823
|
+
| `CLAUDE_MEM_CJK_PREC_MIN` | Precision floor for CJK segmentation candidates. | `0.2` |
|
|
824
|
+
| `CLAUDE_MEM_AUTO_DEEP` | `0` disables automatic deep-search escalation (one Haiku call rewriting a weak query into keyword/concept/HyDE variants). Explicit `deep: true` still works. | _(auto)_ |
|
|
825
|
+
| `CLAUDE_MEM_AUTO_DEEP_CLI` | `0` disables the same auto-escalation on the CLI path only. | _(auto)_ |
|
|
826
|
+
| `CLAUDE_MEM_VECTORS` | `1` re-enables the persisted TF-IDF vector arm (off by default; also needs a vector rebuild via `maintain`). | _(off)_ |
|
|
827
|
+
| `CLAUDE_MEM_SCOPE_FILTER` | `1` stops environment-scoped observations from firing on file-triggered recall. They stay reachable via search. | _(off)_ |
|
|
828
|
+
|
|
829
|
+
### Citation tracking and feedback
|
|
830
|
+
|
|
831
|
+
| Variable | Description | Default |
|
|
832
|
+
|----------|-------------|---------|
|
|
833
|
+
| `CLAUDE_MEM_NO_CITATION_TRACK` | `1` disables both the access-count bump and the decay loop — no citation bookkeeping at all. | _(enabled)_ |
|
|
834
|
+
| `MEM_DISABLE_CITATION_DECAY` | `1` disables only the decay writes, keeping access-count bumps. | _(enabled)_ |
|
|
835
|
+
| `CLAUDE_MEM_CITATION_ADOPTION_THRESHOLD` | Session cite-rate below which demotion is suppressed (promotion always proceeds). | `0.02` |
|
|
836
|
+
| `CLAUDE_MEM_NO_CITE_NUDGE` | `1` fully silences the cite-back nudge. | _(enabled)_ |
|
|
837
|
+
| `CLAUDE_MEM_CITE_NUDGE_THRESHOLD` | Cite-rate below which the nudge fires. | `0.6` |
|
|
838
|
+
| `CLAUDE_MEM_CITE_NUDGE_MIN_INJECTED` | Minimum injection volume before the ratio gate is judged at all. | `5` |
|
|
839
|
+
| `CLAUDE_MEM_CITE_NUDGE_SILENCE_AFTER` | Consecutive low-cite sessions before the nudge goes quiet; `0` = never silence. | `3` |
|
|
840
|
+
| `CLAUDE_MEM_METRICS` | `1` records feature-injection counters surfaced by `claude-mem-lite stats`. | _(off)_ |
|
|
841
|
+
|
|
842
|
+
### Background work
|
|
843
|
+
|
|
844
|
+
All of these turn *off* work that normally happens in the background. Nothing here changes
|
|
845
|
+
what is already stored — only whether new work runs.
|
|
846
|
+
|
|
847
|
+
| Variable | Description | Default |
|
|
848
|
+
|----------|-------------|---------|
|
|
849
|
+
| `CLAUDE_MEM_SKIP_SUMMARY` | Skip the LLM session summary at Stop. | _(runs)_ |
|
|
850
|
+
| `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush — observations are still batched, just not summarized. | _(runs)_ |
|
|
851
|
+
| `CLAUDE_MEM_SKIP_SAVE_ENRICH` | Skip the background Haiku call that backfills `lesson_learned` / search aliases after a save. | _(runs)_ |
|
|
852
|
+
| `CLAUDE_MEM_SKIP_COMPRESS` | Skip auto-compression of old observations. | _(runs)_ |
|
|
853
|
+
| `CLAUDE_MEM_SKIP_MAINTAIN` | Skip the 24h auto-maintain pass (decay, purge, backup). | _(runs)_ |
|
|
854
|
+
| `CLAUDE_MEM_SKIP_OPTIMIZE` | Skip the LLM optimization pass (re-enrich, normalize, cluster-merge). | _(runs)_ |
|
|
855
|
+
| `CLAUDE_MEM_SKIP_AUTO_DEDUP_FUZZY` | Skip the MinHash near-duplicate pass, keeping exact dedup. | _(runs)_ |
|
|
856
|
+
| `CLAUDE_MEM_SKIP_MARKER_GC` | Skip the runtime-marker sweep. | _(runs)_ |
|
|
857
|
+
| `CLAUDE_MEM_SKIP_UPDATE` | Skip the 24h auto-update check against GitHub Releases. | _(runs)_ |
|
|
858
|
+
| `CLAUDE_MEM_SKIP_SIG_VERIFY` | Skip Ed25519 signature verification of a downloaded update. **Escape hatch — leaves updates unauthenticated.** | _(verifies)_ |
|
|
859
|
+
| `CLAUDE_MEM_SKIP_REPOS` | Skip skill/agent registry seeding during install. | _(seeds)_ |
|
|
860
|
+
| `CLAUDE_MEM_NO_LESSON_RETRY` | `1` disables the one-shot retry that re-asks for a missing `lesson_learned`. | _(retries)_ |
|
|
861
|
+
| `CLAUDE_MEM_FLUSH_TIMEOUT` | Seconds the Stop hook waits for pending episode flushes. | `15` |
|
|
862
|
+
| `CLAUDE_MEM_BACKUP_BUDGET_MB` | Disk budget for backup snapshots; the next maintain/save evicts oldest snapshots past the 7-day undo grace. | `256` |
|
|
863
|
+
|
|
864
|
+
### Experimental
|
|
865
|
+
|
|
866
|
+
Off or shadow-mode by default. These are measurement arms, not finished features — behavior
|
|
867
|
+
and names can change between releases.
|
|
868
|
+
|
|
869
|
+
| Variable | Description | Default |
|
|
870
|
+
|----------|-------------|---------|
|
|
871
|
+
| `CLAUDE_MEM_RECOMMEND_MODE` | Skill-recommendation engine: `shadow` (log would-be recommendations, inject nothing) or `off`. **`live` is parsed but not implemented** — live injection is Phase 2. Setting it runs shadow and prints one warning to stderr per process; `claude-mem-lite doctor` also reports it as an inert flag. | `shadow` |
|
|
872
|
+
| `CLAUDE_MEM_TASK_IMPERATIVE` | `on`/`1` injects the single most relevant lesson at prompt position under an imperative template. | _(off)_ |
|
|
873
|
+
| `CLAUDE_MEM_SUBAGENT_INJECT` | Dispatch-time memory injection for subagents. | _(off)_ |
|
|
874
|
+
| `CLAUDE_MEM_SALIENCE` | Selects a comprehension-bridge arm (`bridge`, `bind`); unset = current default behavior. | _(unset)_ |
|
|
875
|
+
| `CLAUDE_MEM_EDGE_DECAY` | Enables decay of file↔observation edges. | _(off)_ |
|
|
876
|
+
| `CLAUDE_MEM_EDGE_DECAY_K` | Edge-decay threshold when the flag above is on (clamped to ≥1). | `3` |
|
|
877
|
+
|
|
878
|
+
### Internal and test-only
|
|
879
|
+
|
|
880
|
+
Set by the tool or by the test harness. Setting these by hand is not supported:
|
|
881
|
+
`CLAUDE_MEM_HOOK_RUNNING`, `CLAUDE_MEM_BINDING_HEALED`, `CLAUDE_MEM_BRIDGE_FAKE`,
|
|
882
|
+
`CLAUDE_MEM_NO_DELAY`, `CLAUDE_MEM_CATCH_SAMPLE`, `CLAUDE_MEM_QUIET_TRACE`,
|
|
883
|
+
`CLAUDE_MEM_DB_PATH`, `CLAUDE_MEM_RUNTIME_DIR`, `MEM_DISABLE_SPAWN_LOG`.
|
|
884
|
+
`CLAUDE_PLUGIN_ROOT` is set by Claude Code itself.
|
|
885
|
+
|
|
886
|
+
Three more are set by `vitest.config.mjs` / `tests/global-setup.mjs` and exist only to
|
|
887
|
+
keep a test run off the live database: `CLAUDE_MEM_TEST_GUARD` (`1` arms the guard, `off`
|
|
888
|
+
opts a test out), `CLAUDE_MEM_TEST_REALDIR` (the live data dir, captured before the suite
|
|
889
|
+
relocates anything) and `CLAUDE_MEM_TEST_SANDBOX` (this run's throwaway dir). With the
|
|
890
|
+
guard armed, any resolution that lands on the live data dir is redirected to the sandbox
|
|
891
|
+
instead — including from a subprocess that inherited the ambient environment. Unset in
|
|
892
|
+
normal use, and inert when unset.
|
|
893
|
+
|
|
776
894
|
## FAQ
|
|
777
895
|
|
|
778
896
|
### What is a memory system for Claude Code?
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`claude-mem-lite` 是 **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)**(Anthropic 官方 CLI 编程代理)的 **持久化记忆系统**(也称 **长期记忆 / 跨会话上下文 / Claude Code 记忆插件**)。它以 **[MCP](https://modelcontextprotocol.io/) 服务器** + Claude Code 钩子(hooks)的形式运行,在编码会话中自动捕获观察记录、决策、bug 修复,并通过 FTS5 全文检索 + TF-IDF 向量的混合检索召回历史上下文。
|
|
6
6
|
|
|
7
|
-
与 [`mem0`](https://github.com/mem0ai/mem0)、MCP 官方参考实现的 [`memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) 服务器等通用 LLM 记忆框架相比,claude-mem-lite 专为 Claude Code 的钩子生命周期定制:episode 批处理把 LLM 调用量相比原版 [claude-mem](https://github.com/thedotmack/claude-mem) 减少 7-10 倍(综合成本估算下降约 600 倍 —— 见下方成本模型,属架构估算而非实测基准);FTS5 + TF-IDF 混合检索在 30 个查询的基准上达到 **Recall@10 = 0.
|
|
7
|
+
与 [`mem0`](https://github.com/mem0ai/mem0)、MCP 官方参考实现的 [`memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) 服务器等通用 LLM 记忆框架相比,claude-mem-lite 专为 Claude Code 的钩子生命周期定制:episode 批处理把 LLM 调用量相比原版 [claude-mem](https://github.com/thedotmack/claude-mem) 减少 7-10 倍(综合成本估算下降约 600 倍 —— 见下方成本模型,属架构估算而非实测基准);FTS5 + TF-IDF 混合检索在 30 个查询的基准上达到 **Recall@10 = 0.90 / Precision@10 = 0.85**(复现命令见[搜索质量](#搜索质量)一节)。
|
|
8
8
|
|
|
9
9
|
无需外部服务。单一 SQLite 数据库。开销极低。
|
|
10
10
|
|
|
@@ -572,17 +572,25 @@ claude-mem-lite/
|
|
|
572
572
|
|
|
573
573
|
## 搜索质量
|
|
574
574
|
|
|
575
|
-
基于 200 条观察和 30 个查询(标准 +
|
|
575
|
+
基于 200 条观察和 30 个查询(标准 + 困难负样本类别)的基准测试结果,测量的是
|
|
576
|
+
**production-hybrid** 检索路径(FTS5 BM25 + TF-IDF 向量 + RRF)——也就是 `mem_search` /
|
|
577
|
+
`recall` 实际走的那条路径:
|
|
576
578
|
|
|
577
|
-
| 指标 |
|
|
579
|
+
| 指标 | 得分(production-hybrid) |
|
|
578
580
|
|------|------|
|
|
579
|
-
| Recall@10 | 0.
|
|
580
|
-
| Precision@10 | 0.
|
|
581
|
-
| nDCG@10 | 0.
|
|
582
|
-
| MRR@10 | 0.
|
|
583
|
-
| P95 搜索延迟 |
|
|
584
|
-
|
|
585
|
-
|
|
581
|
+
| Recall@10 | 0.90 |
|
|
582
|
+
| Precision@10 | 0.85 |
|
|
583
|
+
| nDCG@10 | 0.97 |
|
|
584
|
+
| MRR@10 | 0.96 |
|
|
585
|
+
| P95 搜索延迟 | ~1.8ms |
|
|
586
|
+
|
|
587
|
+
> **数据来源。** 复现命令:`node benchmark/benchmark.mjs --production-hybrid`(确定性输出——
|
|
588
|
+
> 固定语料、固定查询集、无采样)。CI 参考快照是 `benchmark/baseline.json`,
|
|
589
|
+
> `npm run benchmark:gate` 在偏离超过 5% 时让构建失败。本 README 中所有检索指标都以此为唯一来源。
|
|
590
|
+
|
|
591
|
+
> **关于测量路径。** 本表早期版本报告的是 *lexical* 纯 FTS 路径(Precision@10 0.96、
|
|
592
|
+
> P95 0.15ms)。混合向量臂用 precision@10 换取更高的 recall / nDCG / MRR——它会召回超出字面
|
|
593
|
+
> 匹配的语义相关候选;门控现在测量混合路径,所以这些数字反映的是 `mem_search` 的真实行为。
|
|
586
594
|
|
|
587
595
|
## 开发
|
|
588
596
|
|
package/haiku-client.mjs
CHANGED
|
@@ -190,8 +190,14 @@ export async function callHaiku(prompt, { timeout = 10000, maxTokens = 500, temp
|
|
|
190
190
|
// out-of-credit key must not silently drop background summaries.
|
|
191
191
|
let primary = null;
|
|
192
192
|
try {
|
|
193
|
+
// callModelAPI, not a second copy of it: the two were byte-identical apart from
|
|
194
|
+
// where the model id came from (MODEL_MAP[model] vs resolveModel().api — the same
|
|
195
|
+
// value, since resolveModel().cli is a MODEL_MAP key) and a hardcoded 'haiku-api'
|
|
196
|
+
// log label that lied under CLAUDE_MEM_MODEL=sonnet. Two copies of an HTTP client
|
|
197
|
+
// means every proxy fix has to land twice, on the path where missing the proxy is
|
|
198
|
+
// the difference between 1.4s and 13.5s.
|
|
193
199
|
primary = mode === 'api'
|
|
194
|
-
? await
|
|
200
|
+
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature })
|
|
195
201
|
: await callOpenRouterAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature });
|
|
196
202
|
} catch (e) {
|
|
197
203
|
debugCatch(e, `callHaiku:${mode}`);
|
|
@@ -683,63 +689,6 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
683
689
|
return second.result;
|
|
684
690
|
}
|
|
685
691
|
|
|
686
|
-
// ─── API Mode ────────────────────────────────────────────────────────────────
|
|
687
|
-
|
|
688
|
-
async function callHaikuAPI(prompt, { timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE }) {
|
|
689
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
690
|
-
if (!apiKey) return null;
|
|
691
|
-
|
|
692
|
-
const { api: modelId } = resolveModel();
|
|
693
|
-
const controller = new AbortController();
|
|
694
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
695
|
-
|
|
696
|
-
try {
|
|
697
|
-
const { system, user } = splitPrompt(prompt);
|
|
698
|
-
const body = {
|
|
699
|
-
model: modelId,
|
|
700
|
-
max_tokens: maxTokens,
|
|
701
|
-
temperature,
|
|
702
|
-
messages: [{ role: 'user', content: user }],
|
|
703
|
-
};
|
|
704
|
-
// See callModelAPI: cache_control on the constant system slot.
|
|
705
|
-
if (system) {
|
|
706
|
-
body.system = [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }];
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Proxy-aware, same as the OpenRouter site below. Missing it here meant the
|
|
710
|
-
// ANTHROPIC_API_KEY paths were the one keyed provider still doing a bare
|
|
711
|
-
// fetch — a silent outage behind a proxy, and one the new doctor check would
|
|
712
|
-
// have certified as healthy because it probes the hop this code was ASSUMED
|
|
713
|
-
// to use. (pre-tag review SHOULD-FIX 3)
|
|
714
|
-
const apiUrl = 'https://api.anthropic.com/v1/messages';
|
|
715
|
-
const apiHeaders = {
|
|
716
|
-
'Content-Type': 'application/json',
|
|
717
|
-
'x-api-key': apiKey,
|
|
718
|
-
'anthropic-version': '2023-06-01',
|
|
719
|
-
};
|
|
720
|
-
const apiProxy = httpConnectProxyFor(apiUrl);
|
|
721
|
-
const res = apiProxy
|
|
722
|
-
? await postViaConnectProxy(apiProxy, apiUrl, { headers: apiHeaders, body: JSON.stringify(body), timeout })
|
|
723
|
-
: await fetch(apiUrl, {
|
|
724
|
-
method: 'POST',
|
|
725
|
-
headers: apiHeaders,
|
|
726
|
-
body: JSON.stringify(body),
|
|
727
|
-
signal: controller.signal,
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
if (!res.ok) {
|
|
731
|
-
debugLog('WARN', 'haiku-api', `HTTP ${res.status}`);
|
|
732
|
-
return null;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const data = await res.json();
|
|
736
|
-
const text = data.content?.[0]?.text;
|
|
737
|
-
return text ? { text } : null;
|
|
738
|
-
} finally {
|
|
739
|
-
clearTimeout(timer);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
692
|
// ─── OpenRouter Mode ─────────────────────────────────────────────────────────
|
|
744
693
|
|
|
745
694
|
// OpenRouter exposes an OpenAI-compatible chat-completions API (NOT the
|
package/hook-context.mjs
CHANGED
|
@@ -16,6 +16,9 @@ import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet,
|
|
|
16
16
|
import { extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
17
17
|
import { recentInjectableEvents, renderInjectableEvent } from './lib/events-injection.mjs';
|
|
18
18
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
19
|
+
// Single source for the type-quality weights (audit 2026-08-22 P2-10) — this table used
|
|
20
|
+
// to be hand-copied here and in hook-memory.mjs, kept equal only by comment convention.
|
|
21
|
+
import { TYPE_QUALITY, TYPE_QUALITY_DEFAULT } from './scoring-sql.mjs';
|
|
19
22
|
|
|
20
23
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
21
24
|
/**
|
|
@@ -108,18 +111,12 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
|
|
|
108
111
|
const selectedSess = [];
|
|
109
112
|
let totalTokens = 0;
|
|
110
113
|
|
|
111
|
-
// Type quality multipliers — aligned with scoring-sql.mjs TYPE_QUALITY_CASE (R2).
|
|
112
|
-
// Weights calibrated from empirical avg access_count per type:
|
|
113
|
-
// decision 6.05, discovery 3.32, bugfix 2.24, feature 2.04, change 0.93, refactor 0.54.
|
|
114
|
-
// Pre-R2 had bugfix=0.35 (inverted vs reality — bugfixes are 2.4× more used than changes).
|
|
115
|
-
const TYPE_QUALITY = { decision: 1.5, discovery: 1.3, bugfix: 1.1, feature: 1.0, refactor: 0.6, change: 0.5 };
|
|
116
|
-
|
|
117
114
|
// Score each candidate: value = recency * type_quality * importance, cost = tokens
|
|
118
115
|
// Recency uses exponential half-life (consistent with server.mjs BM25 scoring)
|
|
119
116
|
const scoredObs = obsPool.map(o => {
|
|
120
117
|
const halfLifeMs = DECAY_HALF_LIFE_BY_TYPE[o.type] || DEFAULT_DECAY_HALF_LIFE_MS;
|
|
121
118
|
const recency = 1.0 + Math.exp(-0.693 * (now_ms - o.created_at_epoch) / halfLifeMs);
|
|
122
|
-
const typeQuality = TYPE_QUALITY[o.type] ||
|
|
119
|
+
const typeQuality = TYPE_QUALITY[o.type] || TYPE_QUALITY_DEFAULT;
|
|
123
120
|
const impBoost = 0.5 + 0.5 * (o.importance || 1);
|
|
124
121
|
const lessonBoost = o.lesson_learned ? 1.3 : 1.0;
|
|
125
122
|
const value = recency * typeQuality * impBoost * lessonBoost;
|
package/hook-episode.mjs
CHANGED
|
@@ -246,37 +246,61 @@ export function mergePendingEntries(episode) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/** Rule 4's threshold — 8+ Read/Grep entries read as investigation. */
|
|
250
|
+
const RESEARCH_ENTRY_THRESHOLD = 8;
|
|
251
|
+
|
|
249
252
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
253
|
+
* The significance decision WITH its reasoning, for instrumentation.
|
|
254
|
+
* `episodeHasSignificantContent` is the boolean face of this same body, so the meter
|
|
255
|
+
* and the decision cannot drift (audit 2026-08-22 P2-14).
|
|
256
|
+
*
|
|
257
|
+
* `grepDecisive` answers the one question the "move Grep into the bash skip list"
|
|
258
|
+
* decision is blocked on: would this episode still have been kept without its Grep
|
|
259
|
+
* entries? It is true ONLY when rule 4 decided AND the non-Grep entries alone fall
|
|
260
|
+
* short — an edit-driven episode that happens to contain Greps is not evidence that
|
|
261
|
+
* Grep carries research episodes.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} episode
|
|
264
|
+
* @returns {{significant: boolean, rule: 1|2|3|4|null, readCount: number,
|
|
265
|
+
* grepCount: number, grepDecisive: boolean}}
|
|
255
266
|
*/
|
|
256
|
-
export function
|
|
267
|
+
export function explainSignificance(episode) {
|
|
268
|
+
const entries = episode?.entries || [];
|
|
269
|
+
const grepCount = entries.filter(e => e.tool === 'Grep').length;
|
|
270
|
+
const readCount = entries.filter(e => e.tool === 'Read' || e.tool === 'Grep').length;
|
|
271
|
+
const base = { readCount, grepCount, grepDecisive: false };
|
|
272
|
+
|
|
257
273
|
// 1. File edits → always significant (code changes matter)
|
|
258
|
-
|
|
259
|
-
if (hasEdits) return true;
|
|
274
|
+
if (entries.some(e => EDIT_TOOLS.has(e.tool))) return { ...base, significant: true, rule: 1 };
|
|
260
275
|
|
|
261
276
|
// 2. Test/build errors → significant (actionable failures)
|
|
262
277
|
// Plain bash errors without edits are noise (e.g. typos, exploration errors)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (hasTestOrBuildError) return true;
|
|
278
|
+
if (entries.some(e => e.tool === 'Bash' && e.isError && (e.bashSig?.isTest || e.bashSig?.isBuild))) {
|
|
279
|
+
return { ...base, significant: true, rule: 2 };
|
|
280
|
+
}
|
|
267
281
|
|
|
268
282
|
// 3. Important files touched (config, schema, security, migration)
|
|
269
283
|
// Checks episode.files (all touched files, including reads) — catches important-file investigation
|
|
270
|
-
const allFiles = episode
|
|
271
|
-
|
|
284
|
+
const allFiles = episode?.files || [];
|
|
285
|
+
if (allFiles.some(f =>
|
|
272
286
|
/\.(env|yml|yaml|toml|lock|sql|prisma|proto)$/.test(f) ||
|
|
273
287
|
/(config|schema|migration|auth|security)/i.test(f)
|
|
274
|
-
);
|
|
275
|
-
if (hasImportantFile) return true;
|
|
288
|
+
)) return { ...base, significant: true, rule: 3 };
|
|
276
289
|
|
|
277
290
|
// 4. Research pattern: reading many files indicates investigation
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return
|
|
291
|
+
if (readCount >= RESEARCH_ENTRY_THRESHOLD) {
|
|
292
|
+
return { ...base, significant: true, rule: 4, grepDecisive: readCount - grepCount < RESEARCH_ENTRY_THRESHOLD };
|
|
293
|
+
}
|
|
294
|
+
return { ...base, significant: false, rule: null };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if an episode has significant content worth processing with LLM.
|
|
299
|
+
* Significant = contains file edits, Bash errors, or a review/research pattern
|
|
300
|
+
* (8+ Read/Grep entries indicate investigation worth recording).
|
|
301
|
+
* @param {object} episode The episode to check
|
|
302
|
+
* @returns {boolean} true if the episode has significant content
|
|
303
|
+
*/
|
|
304
|
+
export function episodeHasSignificantContent(episode) {
|
|
305
|
+
return explainSignificance(episode).significant;
|
|
282
306
|
}
|
package/hook-llm.mjs
CHANGED
|
@@ -511,14 +511,19 @@ export function buildDegradedTitle(episode) {
|
|
|
511
511
|
// processed), so without this the in-flight episode is silently lost on abnormal
|
|
512
512
|
// termination — and spawning a detached child from a dying process is unreliable, so
|
|
513
513
|
// the save must be synchronous (audit #6). Never throws; returns the obs id or null.
|
|
514
|
-
|
|
514
|
+
// `scope` names the CALLER in hook-error telemetry. Audit 2026-08-22 P2-9 folded
|
|
515
|
+
// flushEpisodeGroup's hand-copied version of this block into this function; without the
|
|
516
|
+
// parameter all three paths would report failures under one label, and "the immediate
|
|
517
|
+
// save threw" means different things on the normal flush, the lock-contended Stop
|
|
518
|
+
// fallback, and the shutdown salvage.
|
|
519
|
+
export function saveEpisodeImmediate(episode, externalDb, scope = 'saveEpisodeImmediate') {
|
|
515
520
|
try {
|
|
516
521
|
if (!episode || !Array.isArray(episode.entries) || episode.entries.length === 0) return null;
|
|
517
522
|
if (!episodeHasSignificantContent(episode)) return null;
|
|
518
523
|
const obs = buildImmediateObservation(episode);
|
|
519
524
|
return saveObservation(obs, episode.project, episode.sessionId, externalDb) || null;
|
|
520
525
|
} catch (e) {
|
|
521
|
-
debugCatch(e,
|
|
526
|
+
debugCatch(e, scope);
|
|
522
527
|
return null;
|
|
523
528
|
}
|
|
524
529
|
}
|
package/hook-memory.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// claude-mem-lite — Semantic Memory Injection
|
|
2
2
|
// Search past observations for relevant memories to inject as context at user-prompt time.
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords, neutralizeContextDelimiters, basenameAnySep } from './utils.mjs';
|
|
5
|
+
import { upsFtsQuery } from './lib/ups-query.mjs';
|
|
6
|
+
import { citeFactorJs, TYPE_QUALITY, TYPE_QUALITY_DEFAULT } from './scoring-sql.mjs';
|
|
6
7
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
7
8
|
import { recordMetric } from './lib/metrics.mjs';
|
|
8
9
|
import { DB_DIR } from './schema.mjs';
|
|
@@ -12,11 +13,9 @@ import { formatSubagentContext } from './lib/task-imperative.mjs';
|
|
|
12
13
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
13
14
|
const MAX_MEMORY_INJECTIONS = 3;
|
|
14
15
|
const MEMORY_LOOKBACK_MS = 60 * DAY_MS; // 60 days
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// decision 6.05, discovery 3.32, bugfix 2.24, feature 2.04, change 0.93, refactor 0.54.
|
|
16
|
+
// Type weights come from scoring-sql.mjs — this was a hand-copy kept equal by an
|
|
17
|
+
// "aligned with (R2)" comment (audit 2026-08-22, P2-10).
|
|
18
18
|
// lesson_learned boost (1.5×) stacks for entries with a real takeaway.
|
|
19
|
-
const MEMORY_TYPE_BOOST = { decision: 1.5, discovery: 1.3, bugfix: 1.1, feature: 1.0, refactor: 0.6, change: 0.5 };
|
|
20
19
|
// Adaptive BM25 thresholds — scale with corpus size to filter noise.
|
|
21
20
|
// Larger corpora produce more weak matches from common words.
|
|
22
21
|
const BM25_THRESHOLD = { TINY: 0, SMALL: 1.5, MEDIUM: 2.5, LARGE: 3.5 };
|
|
@@ -194,7 +193,12 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
194
193
|
};
|
|
195
194
|
|
|
196
195
|
try {
|
|
197
|
-
|
|
196
|
+
// upsFtsQuery, not bare sanitizeFtsQuery: this is the SECOND hook UserPromptSubmit
|
|
197
|
+
// fires, and v3.75.0 capped only the first. This one is the worse half — its stdin
|
|
198
|
+
// ceiling is MAX_HOOK_STDIN_BYTES (256KB) against path A's 64KB, and nothing
|
|
199
|
+
// truncates between stdin and here. The caps are shared, not copied, so the two
|
|
200
|
+
// faces of one event cannot drift apart again.
|
|
201
|
+
const ftsQuery = upsFtsQuery(userPrompt);
|
|
198
202
|
if (!ftsQuery) return [];
|
|
199
203
|
|
|
200
204
|
const cutoff = Date.now() - MEMORY_LOOKBACK_MS;
|
|
@@ -245,7 +249,15 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
245
249
|
if (rows.length === 0) {
|
|
246
250
|
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
247
251
|
if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
|
|
248
|
-
|
|
252
|
+
// debugCatch, not a bare swallow: this is the injection chain's LAST query, and
|
|
253
|
+
// an FTS5 fault here (corrupt index, malformed relaxed query) degrades to an
|
|
254
|
+
// EMPTY injection that reads exactly like "nothing matched" — invisible to
|
|
255
|
+
// stats and doctor alike. Still non-fatal; the prompt must go through.
|
|
256
|
+
// (The two bare catches further down, around the per-row access bumps, are
|
|
257
|
+
// deliberately left bare: they are write-path and per-row, so logging them would
|
|
258
|
+
// flood the debug stream on the same corruption this one reports once.)
|
|
259
|
+
try { rows = selectStmt.all(orQuery, project, cutoff); usedOrFallback = true; }
|
|
260
|
+
catch (e) { debugCatch(e, 'injectMemory:orFallback'); }
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
|
|
@@ -276,7 +288,10 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
276
288
|
if (crossRows.length === 0) {
|
|
277
289
|
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
278
290
|
if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
|
|
279
|
-
|
|
291
|
+
// Same reasoning as the same-project OR fallback above: a fault here silently
|
|
292
|
+
// drops the cross-project half of the injection.
|
|
293
|
+
try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; }
|
|
294
|
+
catch (e) { debugCatch(e, 'injectMemory:crossOrFallback'); }
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
} catch (e) { debugCatch(e, 'crossProjectSearch'); }
|
|
@@ -302,7 +317,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
302
317
|
return {
|
|
303
318
|
...r,
|
|
304
319
|
score: Math.abs(r.relevance)
|
|
305
|
-
* (
|
|
320
|
+
* (TYPE_QUALITY[r.type] || TYPE_QUALITY_DEFAULT)
|
|
306
321
|
* (r.lesson_learned ? 1.5 : 1.0)
|
|
307
322
|
* (r.importance >= 2 ? 1.0 : 0.6)
|
|
308
323
|
* crossProjectPenalty
|