claude-mem-lite 3.74.0 → 3.75.0
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/bash-utils.mjs +49 -18
- package/hook-context.mjs +4 -7
- package/hook-llm.mjs +7 -2
- package/hook-memory.mjs +4 -6
- package/hook.mjs +72 -175
- package/hooks/hooks.json +1 -1
- package/install.mjs +35 -2
- package/lib/citation-tracker.mjs +15 -46
- 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/registry-core.mjs +94 -0
- package/lib/resolve-data-dir.mjs +53 -4
- package/lib/summary-extractor.mjs +2 -8
- package/lib/transcript-scan.mjs +66 -0
- package/mem-cli.mjs +43 -39
- package/nlp.mjs +20 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -1
- package/registry-recommend.mjs +30 -4
- package/scoring-sql.mjs +21 -7
- package/scripts/pre-agent-inject.sh +43 -0
- package/scripts/user-prompt-search.js +17 -3
- package/server.mjs +30 -38
- package/source-files.mjs +12 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.75.0",
|
|
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.0",
|
|
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/bash-utils.mjs
CHANGED
|
@@ -137,7 +137,21 @@ function collectErrorTerms(cmd, response) {
|
|
|
137
137
|
if (!ERROR_STOP_WORDS.has(lw) && !seen.has(lw)) { seen.add(lw); cmdWords.push(lw); }
|
|
138
138
|
}
|
|
139
139
|
const errWords = [];
|
|
140
|
-
|
|
140
|
+
// The line filter is the TRIGGER's pattern list OR'd with the prose one. Anything
|
|
141
|
+
// that made detectBashSignificance call this a hard error is, by construction, also
|
|
142
|
+
// something we will extract terms from — which closes the "trigger fired, extractor
|
|
143
|
+
// found nothing, so we queried the command's own words" class without enumerating
|
|
144
|
+
// failure shapes. ERROR_LINE_RE alone missed `npm ERR! code ENOENT` (no `error`, no
|
|
145
|
+
// `fail`, no `not found` — npm says "no such file") and `panic: assignment to entry
|
|
146
|
+
// in nil map`, while letting `panic: runtime error: …` through purely because that
|
|
147
|
+
// message happens to contain the substring `error`.
|
|
148
|
+
// Note HARD_ERROR_RE's `\n\s+at\s+\S` alternative cannot match a single line (it
|
|
149
|
+
// needs the preceding newline); that is fine — it is a stack-frame anchor, and the
|
|
150
|
+
// frames it guards are accompanied by a line the other alternatives do catch.
|
|
151
|
+
const errLines = String(response || '')
|
|
152
|
+
.split('\n')
|
|
153
|
+
.filter((l) => ERROR_LINE_RE.test(l) || HARD_ERROR_RE.test(l))
|
|
154
|
+
.slice(0, 3);
|
|
141
155
|
for (const line of errLines) {
|
|
142
156
|
const tokens = line.replace(/[^a-zA-Z0-9_.-]/g, ' ').split(/\s+/)
|
|
143
157
|
.filter(w => w.length > 3 && !/^\d+$/.test(w));
|
|
@@ -168,22 +182,25 @@ export function extractErrorKeywords(cmd, response) {
|
|
|
168
182
|
* Two defects this closes, both measured against the live DB on 2026-08-22 (obs
|
|
169
183
|
* #10730 carries the readings):
|
|
170
184
|
*
|
|
171
|
-
* 1.
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
185
|
+
* 1. THE SELECTION FILTER IS A SUPERSET OF THE TRIGGER. This surface fires on
|
|
186
|
+
* detectBashSignificance's isHardError (HARD_ERROR_RE), but term extraction used to
|
|
187
|
+
* keep only lines matching ERROR_LINE_RE — a DIFFERENT list. The two diverge:
|
|
188
|
+
* HARD_ERROR_RE accepts `ERR!`, `enoent`, `panic`, `traceback`; ERROR_LINE_RE takes
|
|
189
|
+
* `error|fail|exception|cannot|not found|undefined|null` as SUBSTRINGS (no word
|
|
190
|
+
* boundaries — `AssertionError` matches on `error`). npm's own output sits in the
|
|
191
|
+
* gap: `npm ERR! code ENOENT / npm ERR! enoent ENOENT: no such file or directory`
|
|
192
|
+
* has no `error`, no `fail`, no `not found` (npm says "no such file"), so it cleared
|
|
193
|
+
* the trigger and then yielded ZERO lines to extract from. The keyword set degraded
|
|
194
|
+
* to pure command words — literally ['npm','run','build'] — and the surface searched
|
|
195
|
+
* the COMMAND'S TOPIC instead of the failure.
|
|
196
|
+
* The sharpest symptom was Go: `panic: assignment to entry in nil map` was silenced
|
|
197
|
+
* while `panic: runtime error: index out of range` was not, purely because the
|
|
198
|
+
* second message happens to contain the substring `error`. Recall depending on the
|
|
199
|
+
* wording of a panic is the same divergence, relocated.
|
|
200
|
+
* OR-ing HARD_ERROR_RE into the line filter closes the class BY CONSTRUCTION rather
|
|
201
|
+
* than by enumerating shapes: whatever convinced the trigger this was a hard error
|
|
202
|
+
* is, by definition, also something we will read terms from. (Widening ERROR_LINE_RE
|
|
203
|
+
* ad hoc WOULD be enumeration; making it a superset of the trigger is not.)
|
|
187
204
|
*
|
|
188
205
|
* 2. COMMAND WORDS STAY IN THE QUERY — a demotion was TRIED AND REJECTED on data.
|
|
189
206
|
* The obvious follow-up is to drop `npm` / `run` / `grep` from the query, since
|
|
@@ -196,7 +213,21 @@ export function extractErrorKeywords(cmd, response) {
|
|
|
196
213
|
* fails locally) for a test failure. Command words are carrying domain anchoring,
|
|
197
214
|
* not just noise. A demote-to-fallback variant measured byte-identical to
|
|
198
215
|
* error-terms-only (12 rows either way): the primary query always filled its
|
|
199
|
-
* LIMIT 3, so the fallback never ran.
|
|
216
|
+
* LIMIT 3, so the fallback never ran.
|
|
217
|
+
*
|
|
218
|
+
* 3. THE RESIDUAL GATE. With (1) in place this fires rarely, but it is not dead: a
|
|
219
|
+
* failure can still yield no usable term — empty output, or a line whose tokens are
|
|
220
|
+
* all stop words (`Error: it failed`). There is then nothing to recall ON, and
|
|
221
|
+
* silence beats querying the command's topic.
|
|
222
|
+
* Read the predicate precisely: `errWords` excludes anything ALREADY taken as a
|
|
223
|
+
* command word, because collectErrorTerms dedups across both classes with the
|
|
224
|
+
* command filled first. So this is "no error term that is not also in the command",
|
|
225
|
+
* not "no error term". `docker compose up -d` and `docker stack deploy` on the SAME
|
|
226
|
+
* output decide differently for exactly that reason — the first has `compose` in the
|
|
227
|
+
* command, the second does not. That asymmetry is inherited from the pre-split
|
|
228
|
+
* single-Set implementation and is preserved deliberately; it is documented here
|
|
229
|
+
* rather than silently "fixed" because changing it would change extractErrorKeywords
|
|
230
|
+
* for every caller, which is a separate decision from this one.
|
|
200
231
|
*
|
|
201
232
|
* @param {string} cmd The command that was executed
|
|
202
233
|
* @param {string} response The error output text
|
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-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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Search past observations for relevant memories to inject as context at user-prompt time.
|
|
3
3
|
|
|
4
4
|
import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords, neutralizeContextDelimiters, basenameAnySep } from './utils.mjs';
|
|
5
|
-
import { citeFactorJs } from './scoring-sql.mjs';
|
|
5
|
+
import { citeFactorJs, TYPE_QUALITY, TYPE_QUALITY_DEFAULT } from './scoring-sql.mjs';
|
|
6
6
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
7
7
|
import { recordMetric } from './lib/metrics.mjs';
|
|
8
8
|
import { DB_DIR } from './schema.mjs';
|
|
@@ -12,11 +12,9 @@ import { formatSubagentContext } from './lib/task-imperative.mjs';
|
|
|
12
12
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
13
13
|
const MAX_MEMORY_INJECTIONS = 3;
|
|
14
14
|
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.
|
|
15
|
+
// Type weights come from scoring-sql.mjs — this was a hand-copy kept equal by an
|
|
16
|
+
// "aligned with (R2)" comment (audit 2026-08-22, P2-10).
|
|
18
17
|
// 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
18
|
// Adaptive BM25 thresholds — scale with corpus size to filter noise.
|
|
21
19
|
// Larger corpora produce more weak matches from common words.
|
|
22
20
|
const BM25_THRESHOLD = { TINY: 0, SMALL: 1.5, MEDIUM: 2.5, LARGE: 3.5 };
|
|
@@ -302,7 +300,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
302
300
|
return {
|
|
303
301
|
...r,
|
|
304
302
|
score: Math.abs(r.relevance)
|
|
305
|
-
* (
|
|
303
|
+
* (TYPE_QUALITY[r.type] || TYPE_QUALITY_DEFAULT)
|
|
306
304
|
* (r.lesson_learned ? 1.5 : 1.0)
|
|
307
305
|
* (r.importance >= 2 ? 1.0 : 0.6)
|
|
308
306
|
* crossProjectPenalty
|