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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.74.0",
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.74.0",
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.88 Recall@10 / 0.96 Precision@10.
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.79 |
685
+ | Precision@10 | 0.85 |
685
686
  | nDCG@10 | 0.97 |
686
- | MRR@10 | 0.97 |
687
- | P95 search latency | ~3ms |
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.88 / Precision@10 = 0.96**。
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.88 |
580
- | Precision@10 | 0.96 |
581
- | nDCG@10 | 0.95 |
582
- | MRR@10 | 0.95 |
583
- | P95 搜索延迟 | 0.15ms |
584
-
585
- 基准测试作为 CI 门控运行(`npm run benchmark:gate`),防止搜索质量回退。
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
- const errLines = String(response || '').split('\n').filter(l => ERROR_LINE_RE.test(l)).slice(0, 3);
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. NO ERROR SIGNAL NO INJECTION. This surface fires on detectBashSignificance's
172
- * isHardError, and that gate's HARD_ERROR_RE is NOT in sync with ERROR_LINE_RE
173
- * here: HARD_ERROR_RE accepts `ERR!`, `enoent` and `traceback`, while ERROR_LINE_RE
174
- * only matches the whole word `error`. So npm's own failure output clears the
175
- * trigger and then yields ZERO error lines — `npm ERR! code ENOENT / npm ERR!
176
- * enoent ENOENT: no such file or directory` contains no `error`, no `fail`, and no
177
- * `not found` (it says "no such file"). The keyword set then degraded to pure
178
- * command words literally ['npm','run','build'] and the surface searched the
179
- * COMMAND'S TOPIC instead of the failure. Same for a Python traceback whose head
180
- * lines carry `Traceback (most recent call last):` and a bare `File "x.py"`.
181
- * Both are among the most common failures a session produces.
182
- * With no error term there is nothing to recall ON, so the honest answer is
183
- * silence rather than a topic match. Widening ERROR_LINE_RE is NOT the fix
184
- * enumeration always misses one more shape, and this gate is correct for every
185
- * shape it misses. (Verified: a `grep` killed by seccomp does NOT reach here at
186
- * all isHardError is false for it so that shape is not evidence for this gate.)
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. Net: gate only, selection unchanged.
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] || 1.0;
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
- export function saveEpisodeImmediate(episode, externalDb) {
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, 'saveEpisodeImmediate');
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
- // Aligned with TYPE_QUALITY_CASE in scoring-sql.mjs (R2 rebalance).
16
- // Weights calibrated to empirical avg access_count:
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
- * (MEMORY_TYPE_BOOST[r.type] || 1.0)
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