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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.74.1",
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.74.1",
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.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/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 callHaikuAPI(prompt, { timeout, maxTokens, temperature })
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] || 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-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
- * Check if an episode has significant content worth processing with LLM.
251
- * Significant = contains file edits, Bash errors, or a review/research pattern
252
- * (8+ Read/Grep entries indicate investigation worth recording).
253
- * @param {object} episode The episode to check
254
- * @returns {boolean} true if the episode has significant content
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 episodeHasSignificantContent(episode) {
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
- const hasEdits = episode.entries.some(e => EDIT_TOOLS.has(e.tool));
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
- const hasTestOrBuildError = episode.entries.some(e =>
264
- e.tool === 'Bash' && e.isError && (e.bashSig?.isTest || e.bashSig?.isBuild)
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.files || [];
271
- const hasImportantFile = allFiles.some(f =>
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
- const readCount = episode.entries.filter(e =>
279
- e.tool === 'Read' || e.tool === 'Grep'
280
- ).length;
281
- return readCount >= 8;
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
- 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
@@ -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 { 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';
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
- // 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.
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
- const ftsQuery = sanitizeFtsQuery(userPrompt);
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
- try { rows = selectStmt.all(orQuery, project, cutoff); usedOrFallback = true; } catch {}
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
- try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; } catch {}
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
- * (MEMORY_TYPE_BOOST[r.type] || 1.0)
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