claude-mem-lite 6.15.0 → 6.17.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 +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +34 -3
- package/README.zh-CN.md +29 -2
- package/adopt-content.mjs +5 -4
- package/cli/common.mjs +2 -0
- package/hook-context.mjs +24 -4
- package/hook-handoff.mjs +6 -3
- package/hook-precompact.mjs +5 -4
- package/hook-update.mjs +18 -4
- package/hook.mjs +27 -9
- package/lib/citation-tracker.mjs +36 -3
- package/lib/deferred-work.mjs +20 -2
- package/lib/fast-summary.mjs +12 -0
- package/lib/hook-stdout.mjs +22 -2
- package/lib/hook-text-cap.mjs +220 -0
- package/lib/recall-framing.mjs +86 -0
- package/lib/time-constants.mjs +9 -0
- package/mem-cli.mjs +13 -2
- package/npm-shrinkwrap.json +217 -206
- package/package.json +9 -7
- package/scripts/post-tool-recall.js +2 -1
- package/scripts/pre-tool-recall.js +30 -10
- package/scripts/user-prompt-search.js +61 -28
- package/server.mjs +3 -0
- package/source-files.mjs +2 -0
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.17.0",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
|
-
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions.
|
|
15
|
+
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions.
|
|
3
|
+
"version": "6.17.0",
|
|
4
|
+
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
81
81
|
- **Episode batching** -- Groups related file operations into coherent episodes before LLM encoding
|
|
82
82
|
- **Error-triggered recall** -- Automatically searches memory when Bash errors occur, surfacing relevant past fixes
|
|
83
83
|
- **Proactive file history** -- When editing a file, automatically shows relevant past observations for that file
|
|
84
|
-
- **Session summaries** --
|
|
84
|
+
- **Session summaries** -- written at every Stop (the assistant's final report when it has Done / Not done sections, else the first prompt and recent observation titles), then upgraded by a background model summary when the session has observations
|
|
85
85
|
- **Project-scoped context** -- Injects recent memory into `CLAUDE.md` and session startup for immediate context
|
|
86
86
|
- **Observation types** -- Categorized as `decision`, `bugfix`, `feature`, `refactor`, `discovery`, or `change`
|
|
87
87
|
- **Importance grading** -- LLM assigns 1-3 importance levels (routine / notable / critical) to each observation
|
|
@@ -237,6 +237,35 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
|
|
|
237
237
|
repos/ # Shallow-cloned source repos
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
## Upgrading to 6.17.0
|
|
241
|
+
|
|
242
|
+
**Two defaults change; one has a switch.** No schema change and no migration, so reverting
|
|
243
|
+
everything is pinning `claude-mem-lite@6.16.0`.
|
|
244
|
+
|
|
245
|
+
- **The pre-edit lesson line asks for a lesson's `#NN` only where it changed the edit.** It no
|
|
246
|
+
longer asks for an applied / not-applicable verdict on every lesson in your next reply, which
|
|
247
|
+
put lesson-id lists into replies. Old directive: `CLAUDE_MEM_SALIENCE=verdict`. Adopted
|
|
248
|
+
projects get the matching CLAUDE.md managed row and detail doc on the next SessionStart;
|
|
249
|
+
`CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` keeps the old text.
|
|
250
|
+
- **SessionStart's "Deferred Work" list ends with "+N more open"** when more than its 5 rows
|
|
251
|
+
are open. No switch; pin 6.16.0 to revert.
|
|
252
|
+
|
|
253
|
+
## Upgrading to 6.16.0
|
|
254
|
+
|
|
255
|
+
**Three defaults change; one has an off switch.** No schema change and no migration, so
|
|
256
|
+
reverting everything is pinning `claude-mem-lite@6.15.0`.
|
|
257
|
+
|
|
258
|
+
- **Each session gets one of two first lines on a file-recall block.** Half of sessions keep
|
|
259
|
+
"system-injected context, continue your planned action"; the other half get a plain
|
|
260
|
+
statement of where the notes come from, as Claude Code's hooks guide recommends. The two
|
|
261
|
+
are compared by cite-rate before one becomes the default. Off (old line everywhere):
|
|
262
|
+
`CLAUDE_MEM_RECALL_FRAMING=legacy`.
|
|
263
|
+
- **Memory text longer than the host's 10,000-character hook limit is trimmed by whole
|
|
264
|
+
lines**, with a closing line naming the ids left out, instead of the host replacing it with
|
|
265
|
+
a 2,000-character preview. No switch; pin 6.15.0 to revert.
|
|
266
|
+
- **Lessons shown after a failed Bash command now count in citation decay**, like every other
|
|
267
|
+
surface: ones never cited are ranked down over time. No switch; pin 6.15.0 to revert.
|
|
268
|
+
|
|
240
269
|
## Upgrading to 6.15.0
|
|
241
270
|
|
|
242
271
|
**Two defaults change; one has an off switch.** No schema change and no migration, so
|
|
@@ -559,7 +588,7 @@ lesson_learned, minhash_sig, access_count, compressed_into, search_aliases,
|
|
|
559
588
|
branch, superseded_at, superseded_by, last_accessed_at
|
|
560
589
|
```
|
|
561
590
|
|
|
562
|
-
**session_summaries** --
|
|
591
|
+
**session_summaries** -- per-session summaries (written at Stop, upgraded by the background model summary)
|
|
563
592
|
```
|
|
564
593
|
id, memory_session_id, project, request, investigated,
|
|
565
594
|
learned, completed, next_steps, files_read, files_edited, notes,
|
|
@@ -628,6 +657,7 @@ Stop
|
|
|
628
657
|
-> Flush final episode buffer
|
|
629
658
|
-> Save handoff snapshot (type 'exit')
|
|
630
659
|
-> Mark session completed
|
|
660
|
+
-> Write the session summary row (sync, no model call)
|
|
631
661
|
-> Spawn LLM summary worker (poll-based wait)
|
|
632
662
|
-> Keep the session file <- Stop fires per TURN; deleting it here re-minted a mem
|
|
633
663
|
session every turn and left the SessionStart /clear branch unreachable (v5.4.0)
|
|
@@ -1055,7 +1085,8 @@ and names can change between releases.
|
|
|
1055
1085
|
|----------|-------------|---------|
|
|
1056
1086
|
| `CLAUDE_MEM_TASK_IMPERATIVE` | `on`/`1` injects the single most relevant lesson at prompt position under an imperative template. | _(off)_ |
|
|
1057
1087
|
| `CLAUDE_MEM_SUBAGENT_INJECT` | Dispatch-time memory injection for subagents. | _(off)_ |
|
|
1058
|
-
| `
|
|
1088
|
+
| `CLAUDE_MEM_RECALL_FRAMING` | First line of a PreToolUse / PostToolUse recall block. `ab` gives each session one of two wordings, the older "system-injected context, continue your planned action" or a plain statement of source, so their cite-rates can be compared in one run (`benchmark/citation-live-replay.mjs --by-framing`); `legacy` / `factual` pin one. | `ab` |
|
|
1089
|
+
| `CLAUDE_MEM_SALIENCE` | Selects how the pre-edit lesson line asks for a response: unset = name a lesson's `#NN` only where it changed the edit; `verdict` = the pre-6.17 per-lesson `applied` / `n/a` verdict (the directive only: the adoption row in CLAUDE.md and the detail doc keep the new wording); `bind` / `bridge` = comprehension-bridge arms (`bridge` keeps the pre-6.17 verdict wording as its fallback); `legacy` = no directive. | _(unset)_ |
|
|
1059
1090
|
| `CLAUDE_MEM_EDGE_DECAY` | Enables decay of file↔observation edges. | _(off)_ |
|
|
1060
1091
|
| `CLAUDE_MEM_EDGE_DECAY_K` | Edge-decay threshold when the flag above is on (clamped to ≥1). | `3` |
|
|
1061
1092
|
|
package/README.zh-CN.md
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
- **Episode 批处理** -- 将相关文件操作分组为连贯的 episode,再进行 LLM 编码
|
|
61
61
|
- **错误触发回忆** -- Bash 出错时自动搜索记忆,浮现相关的历史修复方案
|
|
62
62
|
- **主动文件历史** -- 编辑文件时,自动显示该文件相关的历史观察记录
|
|
63
|
-
- **会话摘要** --
|
|
63
|
+
- **会话摘要** -- 每次 Stop 时写入(助手最终回复带 Done / Not done 段落时取其内容,否则取首条提示和最近的 observation 标题),会话有 observation 时再由后台模型摘要升级
|
|
64
64
|
- **项目作用域上下文** -- 将最近的记忆注入 `CLAUDE.md` 和会话启动上下文
|
|
65
65
|
- **观察类型** -- 分类为 `decision`、`bugfix`、`feature`、`refactor`、`discovery` 或 `change`
|
|
66
66
|
- **重要度分级** -- LLM 为每条观察分配 1-3 级重要度(日常/关注/关键)
|
|
@@ -199,6 +199,32 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
199
199
|
repos/ # 浅克隆的源代码仓库
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
## 升级到 6.17.0
|
|
203
|
+
|
|
204
|
+
**两处默认行为改变,其中一处有开关。** 没有 schema 变更、不需要迁移,全部回退只需固定
|
|
205
|
+
`claude-mem-lite@6.16.0`。
|
|
206
|
+
|
|
207
|
+
- **编辑前的 lesson 提示只在某条 lesson 改变了这次改动时,才请模型提一次它的 `#NN`。** 不再要求在
|
|
208
|
+
下一条回复里对每条 lesson 逐一表态“采纳 / 不适用”——那会让回复里出现成串的 lesson 编号。恢复旧提示:
|
|
209
|
+
`CLAUDE_MEM_SALIENCE=verdict`。已接入的项目会在下次 SessionStart 时把 CLAUDE.md 托管行和详细文档
|
|
210
|
+
换成对应的新写法;想保留旧文本设 `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`。
|
|
211
|
+
- **SessionStart 的 "Deferred Work" 列表(5 行)在还有更多未完成事项时,末尾加一行 "+N more open"。**
|
|
212
|
+
没有开关,回退请固定 6.16.0。
|
|
213
|
+
|
|
214
|
+
## 升级到 6.16.0
|
|
215
|
+
|
|
216
|
+
**三处默认行为改变,其中一处有开关。** 没有 schema 变更、不需要迁移,全部回退只需固定
|
|
217
|
+
`claude-mem-lite@6.15.0`。
|
|
218
|
+
|
|
219
|
+
- **文件召回块的第一行,每个会话分到两种写法之一。** 一半会话保留原来的 "system-injected context,
|
|
220
|
+
continue your planned action",另一半改成说明这些笔记来自哪里的事实陈述(Claude Code 的 hooks
|
|
221
|
+
指南建议这样写)。两种写法先比较引用率,再决定默认用哪个。关闭(全部用旧写法):
|
|
222
|
+
`CLAUDE_MEM_RECALL_FRAMING=legacy`。
|
|
223
|
+
- **超过宿主 1 万字符 hook 上限的记忆文本会按整行裁剪**,末尾一行列出被略去的 id;以前宿主会把它换成
|
|
224
|
+
2,000 字符的预览。没有开关,回退请固定 6.15.0。
|
|
225
|
+
- **Bash 命令失败后展示的教训,现在也计入引用衰减**,和其他注入面一致:一直没被引用的会逐渐排到后面。
|
|
226
|
+
没有开关,回退请固定 6.15.0。
|
|
227
|
+
|
|
202
228
|
## 升级到 6.15.0
|
|
203
229
|
|
|
204
230
|
**两处默认行为改变,其中一处有开关。** 没有 schema 变更、不需要迁移,全部回退只需固定
|
|
@@ -456,7 +482,7 @@ text, narrative, concepts, facts, files_read, files_modified,
|
|
|
456
482
|
importance, related_ids, created_at, created_at_epoch
|
|
457
483
|
```
|
|
458
484
|
|
|
459
|
-
**session_summaries** --
|
|
485
|
+
**session_summaries** -- 每个会话的摘要(Stop 时写入,后台模型摘要升级)
|
|
460
486
|
```
|
|
461
487
|
id, memory_session_id, project, request, investigated,
|
|
462
488
|
learned, completed, next_steps, files_read, files_edited, notes
|
|
@@ -515,6 +541,7 @@ Stop
|
|
|
515
541
|
-> 刷新最终 episode 缓冲区
|
|
516
542
|
-> 保存交接快照(/exit 时)
|
|
517
543
|
-> 标记会话为已完成
|
|
544
|
+
-> 写入会话摘要行(同步,不调模型)
|
|
518
545
|
-> 启动 LLM 摘要 worker(轮询等待)
|
|
519
546
|
```
|
|
520
547
|
|
package/adopt-content.mjs
CHANGED
|
@@ -50,7 +50,7 @@ PreToolUse hooks already run \`mem_recall\` for past lessons before Read/Edit/Wr
|
|
|
50
50
|
|
|
51
51
|
| When | Call |
|
|
52
52
|
|------|------|
|
|
53
|
-
| Before Edit/Write | hook already recalled; if
|
|
53
|
+
| Before Edit/Write | hook already recalled; if an injected \`#NN\` lesson changed what you did, name \`#NN\` once where you say so (citing = adopting; uncited lessons decay; skip ones that did not apply) |
|
|
54
54
|
| After fixing a non-trivial bug | \`mem_save(type="bugfix", lesson_learned="<root cause + fix>", importance=2)\` |
|
|
55
55
|
| After a non-obvious architecture decision | \`mem_save(type="decision", lesson_learned="<constraint + tradeoff>")\` |
|
|
56
56
|
| Deferring to a future session | \`mem_defer({title, priority:1|2|3, detail})\`; when fixed, add \`closes_deferred=[N]\` to \`mem_save\` |
|
|
@@ -87,11 +87,12 @@ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该
|
|
|
87
87
|
- **Edit / Write** 路径:decision-support——最多 3 条、240 字符、高重要度 bugfix/decision 即使无
|
|
88
88
|
lesson 也注入。
|
|
89
89
|
- Read→Edit 同文件共享 cooldown(不重复注入正文),但 Read 注入后的首个 Edit 会把 lesson **ID**
|
|
90
|
-
以一行 ack 指令重新浮出。看到 \`#NN [bugfix] …\`
|
|
91
|
-
|
|
90
|
+
以一行 ack 指令重新浮出。看到 \`#NN [bugfix] …\` 这类行时:**某条 lesson 改变了你的做法,就在描述
|
|
91
|
+
那处改动时顺带提一次 \`#NN\`**;没用上的 lesson 不必提,也不要逐条列出。纯工具回合不算;
|
|
92
|
+
把 ID 记在工作记忆里,写回时引用。
|
|
92
93
|
- 系统按会话追踪引用:被引用的 lesson 在召回排序里上浮,被注入却未引用的下沉(有界的排序乘数);
|
|
93
94
|
反复注入却从未被引用的,后台维护会把它的 importance 降到 2(无 lesson 的降到 1)。
|
|
94
|
-
|
|
95
|
+
写成 \`#NN n/a\` 的驳回不算采纳:排序上与未引用相同,同样下沉——所以不必写。
|
|
95
96
|
引用是给系统的反馈,不是合规仪式——注入池据此自调。
|
|
96
97
|
|
|
97
98
|
## 何时主动调用 MCP 工具
|
package/cli/common.mjs
CHANGED
|
@@ -326,6 +326,7 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
326
326
|
'benchmark',
|
|
327
327
|
'body',
|
|
328
328
|
'branch',
|
|
329
|
+
'chars',
|
|
329
330
|
'closes-deferred',
|
|
330
331
|
'concepts',
|
|
331
332
|
'confirm',
|
|
@@ -427,6 +428,7 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
427
428
|
*/
|
|
428
429
|
export const COMMAND_SCOPED_FLAGS = new Map([
|
|
429
430
|
['apply', 'verify-apply'],
|
|
431
|
+
['chars', 'context'],
|
|
430
432
|
['digest', 'verify-apply'],
|
|
431
433
|
['print-project', 'verify-apply'],
|
|
432
434
|
['undo', 'verify-apply'],
|
package/hook-context.mjs
CHANGED
|
@@ -580,7 +580,12 @@ export function buildSessionContextLines(
|
|
|
580
580
|
currentCcSessionId = null,
|
|
581
581
|
collector = null,
|
|
582
582
|
) {
|
|
583
|
-
if (collector)
|
|
583
|
+
if (collector) {
|
|
584
|
+
collector.keyContextIds = [];
|
|
585
|
+
// The same rows as {id, text: rendered line}, so a caller can book only the ones the
|
|
586
|
+
// hook-output cap keeps (D#108; lib/hook-text-cap.mjs idsShownWhole).
|
|
587
|
+
collector.keyContextLines = [];
|
|
588
|
+
}
|
|
584
589
|
// 1. Token-budgeted observation selection
|
|
585
590
|
const selected = selectWithTokenBudget(db, project, 2000);
|
|
586
591
|
const observations = selected.observations;
|
|
@@ -711,14 +716,20 @@ export function buildSessionContextLines(
|
|
|
711
716
|
summaryLines.push('### File Lessons');
|
|
712
717
|
summaryLines.push(...shown.map((e) => e.line));
|
|
713
718
|
summaryLines.push('');
|
|
714
|
-
if (collector)
|
|
719
|
+
if (collector) {
|
|
720
|
+
collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
721
|
+
collector.keyContextLines.push(...shown.map((e) => ({ id: e.id, text: e.line })));
|
|
722
|
+
}
|
|
715
723
|
}
|
|
716
724
|
if (keyContext.length > 0 && !quiet) {
|
|
717
725
|
const shown = keyContext.slice(0, keyContextQuota);
|
|
718
726
|
summaryLines.push('### Key Context');
|
|
719
727
|
summaryLines.push(...shown.map((e) => e.line));
|
|
720
728
|
summaryLines.push('');
|
|
721
|
-
if (collector)
|
|
729
|
+
if (collector) {
|
|
730
|
+
collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
731
|
+
collector.keyContextLines.push(...shown.map((e) => ({ id: e.id, text: e.line })));
|
|
732
|
+
}
|
|
722
733
|
}
|
|
723
734
|
} else if (!latestSummary && !effectiveQuiet()) {
|
|
724
735
|
// Fallback: no summary AND no key observations — show recent activity.
|
|
@@ -851,7 +862,8 @@ export function buildSessionContextLines(
|
|
|
851
862
|
SELECT id, title, priority,
|
|
852
863
|
ROW_NUMBER() OVER (
|
|
853
864
|
ORDER BY priority DESC, created_at_epoch ASC, id ASC
|
|
854
|
-
) AS ordinal
|
|
865
|
+
) AS ordinal,
|
|
866
|
+
COUNT(*) OVER () AS open_total
|
|
855
867
|
FROM deferred_work
|
|
856
868
|
WHERE project = ? AND status = 'open'
|
|
857
869
|
ORDER BY priority DESC, created_at_epoch ASC, id ASC
|
|
@@ -867,6 +879,14 @@ export function buildSessionContextLines(
|
|
|
867
879
|
const pTag = d.priority === 3 ? '🔴' : d.priority === 1 ? '⚪' : '🟡';
|
|
868
880
|
deferredLines.push(`${d.ordinal}. ${pTag} [P${d.priority}] ${truncate(d.title, 120)} (D#${d.id})`);
|
|
869
881
|
}
|
|
882
|
+
// The list is capped at 5 and used to stop there silently, so 10 open items read as a
|
|
883
|
+
// 5-item backlog. The total is the same statement's COUNT(*) OVER (), taken before LIMIT.
|
|
884
|
+
const hidden = Number(deferredItems[0].open_total) - deferredItems.length;
|
|
885
|
+
// Both listing surfaces page at 10, so the line names the knob rather than promising "all".
|
|
886
|
+
if (hidden > 0)
|
|
887
|
+
deferredLines.push(
|
|
888
|
+
`+${hidden} more open — mem_defer_list / \`defer list\` with a larger limit lists them`,
|
|
889
|
+
);
|
|
870
890
|
deferredLines.push('');
|
|
871
891
|
}
|
|
872
892
|
|
package/hook-handoff.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import * as taskReaderModule from './lib/task-reader.mjs';
|
|
|
33
33
|
// so a named import could not be spied on in tests.
|
|
34
34
|
import * as pausedReaderModule from './lib/paused-reader.mjs';
|
|
35
35
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
36
|
+
import { summarySourceLabel } from './lib/fast-summary.mjs';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Build and save a handoff snapshot to session_handoffs table.
|
|
@@ -1035,7 +1036,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
1035
1036
|
let summary = db
|
|
1036
1037
|
.prepare(
|
|
1037
1038
|
`
|
|
1038
|
-
SELECT completed, next_steps, remaining_items FROM session_summaries
|
|
1039
|
+
SELECT completed, next_steps, remaining_items, notes FROM session_summaries
|
|
1039
1040
|
WHERE memory_session_id = ? AND project = ?
|
|
1040
1041
|
ORDER BY created_at_epoch DESC, id DESC LIMIT 1
|
|
1041
1042
|
`,
|
|
@@ -1049,7 +1050,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
1049
1050
|
summary = db
|
|
1050
1051
|
.prepare(
|
|
1051
1052
|
`
|
|
1052
|
-
SELECT completed, next_steps, remaining_items FROM session_summaries
|
|
1053
|
+
SELECT completed, next_steps, remaining_items, notes FROM session_summaries
|
|
1053
1054
|
WHERE project = ?
|
|
1054
1055
|
ORDER BY ABS(created_at_epoch - ?) ASC, id DESC LIMIT 1
|
|
1055
1056
|
`,
|
|
@@ -1058,7 +1059,9 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
1058
1059
|
}
|
|
1059
1060
|
if (summary && (summary.completed || summary.next_steps || summary.remaining_items)) {
|
|
1060
1061
|
lines.push('');
|
|
1061
|
-
|
|
1062
|
+
// Provenance of the Done text from the row's own tag, not a constant: it read "haiku" on
|
|
1063
|
+
// every row, including rows whose Done came from the Stop report or observation titles.
|
|
1064
|
+
lines.push(`<session-summary source="${summarySourceLabel(summary.notes)}">`);
|
|
1062
1065
|
// Defang: these come from session_summaries, populated by Haiku OR by
|
|
1063
1066
|
// extractStructuredSummary over the assistant transcript tail — replayed text that can
|
|
1064
1067
|
// carry tool-XML / forged authority tags, same class as working_on above (audit MED-4).
|
package/hook-precompact.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { buildSessionContextLines } from './hook-context.mjs';
|
|
|
9
9
|
import { inferProject, debugCatch, debugLog } from './utils.mjs';
|
|
10
10
|
import { RUNTIME_DIR } from './hook-shared.mjs';
|
|
11
11
|
import { recordKeyContextInjection } from './lib/keyctx-marker.mjs';
|
|
12
|
+
import { writeCappedHookText, idsShownWhole } from './lib/hook-text-cap.mjs';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Build + emit the memory context block on stdout. Writes the Key Context ids
|
|
@@ -26,9 +27,9 @@ export function handlePreCompact({ db, project, sessionId, runtimeDir = RUNTIME_
|
|
|
26
27
|
const collector = {};
|
|
27
28
|
const body = buildSessionContextLines(db, project, new Date(), sessionId || null, collector);
|
|
28
29
|
const rendered = body && String(body).trim() !== '';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
30
|
+
// What the cap kept, not what was rendered: a Key Context row the cap cut is not in
|
|
31
|
+
// context, so booking it would exclude it from <memory-context> too (D#108).
|
|
32
|
+
const shown = rendered ? writeCappedHookText(`<claude-mem-context>\n${body}\n</claude-mem-context>`) : '';
|
|
32
33
|
// Recorded even when NOTHING was re-rendered, matching handleSessionStart — the two
|
|
33
34
|
// callers must describe the same set (keyctx-marker.mjs header), and the marker is an
|
|
34
35
|
// exclude-set for what is actually in context. The old empty-body early return left the
|
|
@@ -39,7 +40,7 @@ export function handlePreCompact({ db, project, sessionId, runtimeDir = RUNTIME_
|
|
|
39
40
|
runtimeDir,
|
|
40
41
|
project,
|
|
41
42
|
sessionId: sessionId || null,
|
|
42
|
-
ids:
|
|
43
|
+
ids: idsShownWhole(shown, collector.keyContextLines),
|
|
43
44
|
});
|
|
44
45
|
} catch (e) {
|
|
45
46
|
debugCatch(e, 'handlePreCompact');
|
package/hook-update.mjs
CHANGED
|
@@ -76,11 +76,12 @@ export async function checkForUpdate(options = {}) {
|
|
|
76
76
|
const state = readState();
|
|
77
77
|
if (!force && !shouldCheck(state)) {
|
|
78
78
|
// Return cached update info if previously detected
|
|
79
|
-
|
|
79
|
+
const running = pendingCachedUpdate(state);
|
|
80
|
+
if (running) {
|
|
80
81
|
return {
|
|
81
82
|
updateAvailable: true,
|
|
82
83
|
updated: false,
|
|
83
|
-
from:
|
|
84
|
+
from: running,
|
|
84
85
|
to: state.latestVersion,
|
|
85
86
|
installDeferred: pluginMode || !allowInstall,
|
|
86
87
|
pluginMode,
|
|
@@ -160,13 +161,14 @@ export function getCachedUpdateBanner() {
|
|
|
160
161
|
try {
|
|
161
162
|
if (isDevMode() || process.env.CLAUDE_MEM_SKIP_UPDATE) return null;
|
|
162
163
|
const state = readState();
|
|
163
|
-
|
|
164
|
+
const running = pendingCachedUpdate(state);
|
|
165
|
+
if (running) {
|
|
164
166
|
// Cached "available" state only persists for deferred installs (plugin mode
|
|
165
167
|
// / allowInstall=false); a successful auto-install clears updateAvailable.
|
|
166
168
|
const hint = isPluginMode()
|
|
167
169
|
? ' — plugin mode only checks for updates; reinstall/update the plugin to apply it'
|
|
168
170
|
: '';
|
|
169
|
-
return `\n📦 claude-mem-lite: v${state.latestVersion} available (current: v${
|
|
171
|
+
return `\n📦 claude-mem-lite: v${state.latestVersion} available (current: v${running})${hint}\n`;
|
|
170
172
|
}
|
|
171
173
|
return null;
|
|
172
174
|
} catch {
|
|
@@ -174,6 +176,18 @@ export function getCachedUpdateBanner() {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
// Issue #35. A cached `updateAvailable` is a claim about the version that was
|
|
180
|
+
// running when the check ran, and in plugin mode the update itself is applied by
|
|
181
|
+
// Claude Code — downloadAndInstall never runs, so nothing clears the flag. Judge
|
|
182
|
+
// the cache against the version running NOW: returns that version when the cached
|
|
183
|
+
// latest is still ahead of it, else null. Both cached faces (the SessionStart
|
|
184
|
+
// banner and the throttled checkForUpdate) ask this, so neither can nag alone.
|
|
185
|
+
function pendingCachedUpdate(state) {
|
|
186
|
+
if (!state.updateAvailable || !state.latestVersion) return null;
|
|
187
|
+
const running = getCurrentVersion();
|
|
188
|
+
return compareVersions(state.latestVersion, running) > 0 ? running : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
177
191
|
// True when a network refresh is due (24h throttle) and updates aren't disabled.
|
|
178
192
|
// Caller spawns the refresh in the background so this session doesn't wait.
|
|
179
193
|
export function isUpdateCheckDue() {
|
package/hook.mjs
CHANGED
|
@@ -98,7 +98,13 @@ import {
|
|
|
98
98
|
} from './lib/fast-summary.mjs';
|
|
99
99
|
import { formatHookError } from './lib/native-binding-hint.mjs';
|
|
100
100
|
import { recordHookError } from './lib/hook-telemetry.mjs';
|
|
101
|
-
import {
|
|
101
|
+
import {
|
|
102
|
+
queueHookContext,
|
|
103
|
+
queueHookSystemMessage,
|
|
104
|
+
flushHookStdout,
|
|
105
|
+
previewHookContext,
|
|
106
|
+
} from './lib/hook-stdout.mjs';
|
|
107
|
+
import { writePlainHookText, resetPlainHookText, idsShownWhole } from './lib/hook-text-cap.mjs';
|
|
102
108
|
import { shouldRecallOnFailure } from './lib/tool-refusal.mjs';
|
|
103
109
|
import {
|
|
104
110
|
entryInputTags,
|
|
@@ -1564,9 +1570,12 @@ function trackCitationsAtStop(db, { sessionId, project, ccSessionId, transcriptP
|
|
|
1564
1570
|
let gate = { gateInjected: null, gateRecalled: null, gateRatio: null };
|
|
1565
1571
|
try {
|
|
1566
1572
|
const gateInjectedIds = unionSurfaces(extractInjectedBySurface(transcriptPath, { mainOnly: true }));
|
|
1567
|
-
// The nudge asks whether the agent ANSWERED what the hooks showed it,
|
|
1568
|
-
// `#NN n/a — <reason>`
|
|
1569
|
-
//
|
|
1573
|
+
// The nudge asks whether the agent ANSWERED what the hooks showed it, so a
|
|
1574
|
+
// `#NN n/a — <reason>` still counts. Since D#98 the default directive asks for no
|
|
1575
|
+
// reply on a lesson that did not apply, so that silence now reads as a miss here and
|
|
1576
|
+
// the gate fires more (projected 78/95 → up to 87/95 qualifying sessions,
|
|
1577
|
+
// docs/audits/20260927-d98-dismissal-baseline.md). Left as is: the gate already fired
|
|
1578
|
+
// on most sessions and self-silences after 3; D#111's readout re-measures it.
|
|
1570
1579
|
const gateCited = extractCitationsFromTranscript(transcriptPath, {
|
|
1571
1580
|
mainOnly: true,
|
|
1572
1581
|
includeDismissed: true,
|
|
@@ -1744,6 +1753,12 @@ async function handleStop() {
|
|
|
1744
1753
|
// waits on, then recreates the sandbox tree behind the test's cleanup. Any
|
|
1745
1754
|
// grace period for that is a race, not a barrier — the post-tag review timed a
|
|
1746
1755
|
// recreate at 432ms and watched a 300ms grace lose.
|
|
1756
|
+
//
|
|
1757
|
+
// D#95 made this opt-in in 34a65cd and was reverted before release: its premise ("Last
|
|
1758
|
+
// Session already comes from the Stop report") holds only when the assistant's final reply
|
|
1759
|
+
// carries Done / Not done sections (lib/summary-extractor.mjs). Without them the Stop row is
|
|
1760
|
+
// the first prompt + recent observation titles, and this worker's model summary is the only
|
|
1761
|
+
// prose summary such a user gets.
|
|
1747
1762
|
if (!process.env.CLAUDE_MEM_SKIP_SUMMARY)
|
|
1748
1763
|
spawnBackground('llm-summary', sessionId, project, String(stopEpoch));
|
|
1749
1764
|
|
|
@@ -2860,7 +2875,9 @@ async function handleSessionStart() {
|
|
|
2860
2875
|
runtimeDir: RUNTIME_DIR,
|
|
2861
2876
|
project,
|
|
2862
2877
|
sessionId: ccSessionId,
|
|
2863
|
-
|
|
2878
|
+
// Only the rows the cap keeps: flushHookStdout caps additionalContext at the
|
|
2879
|
+
// dispatcher's exit, and nothing is queued for the model after this point (D#108).
|
|
2880
|
+
ids: idsShownWhole(previewHookContext(), contextCollector.keyContextLines),
|
|
2864
2881
|
});
|
|
2865
2882
|
|
|
2866
2883
|
// One-time migration: remove any stale <claude-mem-context> block left in
|
|
@@ -3040,7 +3057,7 @@ function injectHandoffIfEarly(db, { project, promptText, promptNumber, ccSession
|
|
|
3040
3057
|
const picked = pickHandoffToInject(db, project, ccSessionId);
|
|
3041
3058
|
if (picked) {
|
|
3042
3059
|
const injection = renderHandoffInjection(db, project, ccSessionId);
|
|
3043
|
-
if (injection)
|
|
3060
|
+
if (injection) writePlainHookText(injection);
|
|
3044
3061
|
// Consume ONLY the row we just injected — leave other projects' exit
|
|
3045
3062
|
// handoffs intact so future sessions can still resume from them.
|
|
3046
3063
|
// Pre-v2.46 wiped every exit handoff for the project on any continuation
|
|
@@ -3251,7 +3268,7 @@ async function injectSemanticMemory(db, { project, promptText, ccSessionId }) {
|
|
|
3251
3268
|
const lines = ['<memory-context relevance="high">'];
|
|
3252
3269
|
for (const m of memories) lines.push(formatMemoryLine(m));
|
|
3253
3270
|
lines.push('</memory-context>');
|
|
3254
|
-
|
|
3271
|
+
writePlainHookText(lines.join('\n'));
|
|
3255
3272
|
}
|
|
3256
3273
|
// HIGH-1 (full audit 2026-07-16): surface FTS-matched events — the canonical
|
|
3257
3274
|
// store for promoted bugfix/decision/lesson memories that persistHaikuSummary
|
|
@@ -3272,7 +3289,7 @@ async function injectSemanticMemory(db, { project, promptText, ccSessionId }) {
|
|
|
3272
3289
|
const elines = ['<memory-context relevance="events">'];
|
|
3273
3290
|
for (const e of events) elines.push(`- ${renderInjectableEvent(e)}`);
|
|
3274
3291
|
elines.push('</memory-context>');
|
|
3275
|
-
|
|
3292
|
+
writePlainHookText(elines.join('\n'));
|
|
3276
3293
|
}
|
|
3277
3294
|
} catch (e) {
|
|
3278
3295
|
debugCatch(e, 'handleUserPrompt-events');
|
|
@@ -3281,7 +3298,7 @@ async function injectSemanticMemory(db, { project, promptText, ccSessionId }) {
|
|
|
3281
3298
|
// Guard the write on a non-empty return — formatTaskImperative yields '' for a
|
|
3282
3299
|
// lesson that strips to empty (e.g. "."), which would otherwise emit a bare line.
|
|
3283
3300
|
const imperativeLine = formatTaskImperative(imperativePick.lesson_learned, imperativePick.id);
|
|
3284
|
-
if (imperativeLine)
|
|
3301
|
+
if (imperativeLine) writePlainHookText(imperativeLine);
|
|
3285
3302
|
}
|
|
3286
3303
|
|
|
3287
3304
|
// D#214's ruler, second half: arm B was computed above, before anything was
|
|
@@ -3312,6 +3329,7 @@ async function injectSemanticMemory(db, { project, promptText, ccSessionId }) {
|
|
|
3312
3329
|
}
|
|
3313
3330
|
|
|
3314
3331
|
async function handleUserPrompt() {
|
|
3332
|
+
resetPlainHookText();
|
|
3315
3333
|
const input = await readUserPromptInput();
|
|
3316
3334
|
if (!input) return;
|
|
3317
3335
|
const { promptText, hookData } = input;
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { readTranscriptEntries } from './transcript-scan.mjs';
|
|
|
18
18
|
// The emitter's own prefix — see SURFACE_MATCHERS.task_imperative. Importing it rather
|
|
19
19
|
// than re-typing the framing is what keeps emit and extract from becoming two lists.
|
|
20
20
|
import { TASK_IMPERATIVE_PREFIX } from './task-imperative.mjs';
|
|
21
|
+
import { classifyRecallFraming } from './recall-framing.mjs';
|
|
21
22
|
|
|
22
23
|
import { DAY_MS } from './time-constants.mjs';
|
|
23
24
|
/**
|
|
@@ -102,8 +103,9 @@ export function unanchoredInjectedIdRe() {
|
|
|
102
103
|
const CITATION_RE = citationIdRe();
|
|
103
104
|
|
|
104
105
|
// ─── Dismissals ──────────────────────────────────────────────────────────────
|
|
105
|
-
// The adoption doc
|
|
106
|
-
// or `'#NN n/a — <reason>'
|
|
106
|
+
// The adoption doc asked the agent (until D#98; still under CLAUDE_MEM_SALIENCE=verdict) to
|
|
107
|
+
// answer every surfaced lesson with `'#NN applied'` or `'#NN n/a — <reason>'`, and agents
|
|
108
|
+
// still write the second form unprompted. It is the agent saying the lesson did NOT
|
|
107
109
|
// apply, yet `#NN` alone was the whole citation test, so a dismissal promoted the row
|
|
108
110
|
// exactly like an application: cited_count + 1, uncited_streak reset, demoted_at
|
|
109
111
|
// cleared, access_count bumped towards boostAccessed. Measured 2026-09-25T21:40Z over every
|
|
@@ -743,8 +745,13 @@ const SURFACE_MATCHERS = {
|
|
|
743
745
|
// post-tool-use.sh. High-volume surface that NO extractor matched before
|
|
744
746
|
// v3.47 — error-recall'd obs accrued injection_count but never reached
|
|
745
747
|
// applyCitationDecay, so they could neither promote nor demote.
|
|
748
|
+
// TWO deliveries: post-tool-use.sh (PostToolUse) and `hook.mjs post-tool-failure`
|
|
749
|
+
// (PostToolUseFailure — where a host-flagged failure goes, not PostToolUse). Keyed on
|
|
750
|
+
// the first alone until 2026-09-27, which kept every failure-path recall out of decay
|
|
751
|
+
// and every cite-rate ruler (C1 denominator count: 158 attachments carrying 389 ids).
|
|
746
752
|
accepts: ({ command, text }) =>
|
|
747
|
-
command.includes('post-tool-use')
|
|
753
|
+
(command.includes('post-tool-use') || command.includes('post-tool-failure')) &&
|
|
754
|
+
text.includes('Related memories found for this error'),
|
|
748
755
|
collect: (text, add) => {
|
|
749
756
|
// Per-line anchored: match only a row that STARTS with `#NN [type]` (after its
|
|
750
757
|
// indent), NOT every such token in the block. The inlined lesson body (v3.16.x)
|
|
@@ -857,6 +864,32 @@ export function countInjectedBySurface(transcriptPath, opts = {}) {
|
|
|
857
864
|
return out;
|
|
858
865
|
}
|
|
859
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Which recall framing arm(s) this transcript's PreToolUse blocks carried (A1 A/B).
|
|
869
|
+
*
|
|
870
|
+
* Read from the injected text, not recomputed from the session id: a session that ran
|
|
871
|
+
* before the A/B shipped saw the legacy line whatever its id hashes to, and only the text
|
|
872
|
+
* knows. Same walk and same `pretool` matcher as every other face.
|
|
873
|
+
*
|
|
874
|
+
* @param {string|null|undefined} transcriptPath
|
|
875
|
+
* @param {{mainOnly?: boolean}} [opts]
|
|
876
|
+
* @returns {'legacy'|'factual'|'mixed'|null} null when no PreToolUse block carried a framing line.
|
|
877
|
+
*/
|
|
878
|
+
export function pretoolFramingOf(transcriptPath, opts = {}) {
|
|
879
|
+
const seen = new Set();
|
|
880
|
+
eachHookAttachment(
|
|
881
|
+
transcriptPath,
|
|
882
|
+
(ctx) => {
|
|
883
|
+
if (!SURFACE_MATCHERS.pretool.accepts(ctx)) return;
|
|
884
|
+
const arm = classifyRecallFraming(ctx.text);
|
|
885
|
+
if (arm) seen.add(arm);
|
|
886
|
+
},
|
|
887
|
+
opts,
|
|
888
|
+
);
|
|
889
|
+
if (seen.size === 0) return null;
|
|
890
|
+
return seen.size > 1 ? 'mixed' : [...seen][0];
|
|
891
|
+
}
|
|
892
|
+
|
|
860
893
|
// Per-face extractors: thin wrappers over the shared table, kept as named
|
|
861
894
|
// exports because callers and tests address individual faces.
|
|
862
895
|
function extractOneSurface(face, transcriptPath, opts) {
|
package/lib/deferred-work.mjs
CHANGED
|
@@ -67,14 +67,16 @@ export function insertDeferred(db, args) {
|
|
|
67
67
|
* @param {Database} db
|
|
68
68
|
* @param {string} project
|
|
69
69
|
* @param {number} [limit=10]
|
|
70
|
-
* @returns {Array<{id, project, title, detail, priority, status, created_at_epoch, ordinal}>}
|
|
70
|
+
* @returns {Array<{id, project, title, detail, priority, status, created_at_epoch, ordinal, open_total}>}
|
|
71
|
+
* `open_total` is every open row in the project, counted by the same statement before LIMIT.
|
|
71
72
|
*/
|
|
72
73
|
export function listOpenWithOrdinal(db, project, limit = 10) {
|
|
73
74
|
return db
|
|
74
75
|
.prepare(
|
|
75
76
|
`
|
|
76
77
|
SELECT id, project, title, detail, priority, status, created_at_epoch,
|
|
77
|
-
ROW_NUMBER() OVER (ORDER BY priority DESC, created_at_epoch ASC, id ASC) AS ordinal
|
|
78
|
+
ROW_NUMBER() OVER (ORDER BY priority DESC, created_at_epoch ASC, id ASC) AS ordinal,
|
|
79
|
+
COUNT(*) OVER () AS open_total
|
|
78
80
|
FROM deferred_work
|
|
79
81
|
WHERE project = ? AND status = 'open'
|
|
80
82
|
ORDER BY priority DESC, created_at_epoch ASC, id ASC
|
|
@@ -84,6 +86,22 @@ export function listOpenWithOrdinal(db, project, limit = 10) {
|
|
|
84
86
|
.all(project, limit);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/**
|
|
90
|
+
* How many open rows `defer list` / `mem_defer_list` left off their page, as a line to
|
|
91
|
+
* print — '' when the page held them all. Without it a page one short of the open set
|
|
92
|
+
* (11 open, default page 10) reads as the whole list. Shared by both surfaces; `raise`
|
|
93
|
+
* names the knob in each surface's own spelling. The total comes from the page's own
|
|
94
|
+
* statement (`open_total`), so a concurrent `defer add` cannot make the two disagree.
|
|
95
|
+
* @param {Array<{open_total?: number}>} page rows from listOpenWithOrdinal
|
|
96
|
+
* @param {string} raise e.g. `raise --limit (max 100)`
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function formatDeferMoreHint(page, raise) {
|
|
100
|
+
const hidden = page.length > 0 ? Number(page[0].open_total) - page.length : 0;
|
|
101
|
+
if (!(hidden > 0)) return '';
|
|
102
|
+
return `${hidden} more open item${hidden === 1 ? '' : 's'} not shown — ${raise}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
// ─── G11: list age + stale refresh hint (roadmap 2026-07-18) ─────────────────
|
|
88
106
|
|
|
89
107
|
// Internal const (was exported at v3.51.0 birth with zero external importers —
|
package/lib/fast-summary.mjs
CHANGED
|
@@ -151,6 +151,18 @@ export function parseSummaryNotes(notes) {
|
|
|
151
151
|
return { done: 'titles', left: 'other', lines: text };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The `source` attribute the /clear handoff puts on `<session-summary>`: who wrote the row's
|
|
156
|
+
* Done text — `haiku` (the model worker), `report` (the assistant's own final report,
|
|
157
|
+
* extracted at Stop) or `titles` (observation titles, the fast fallback).
|
|
158
|
+
* @param {string|null|undefined} notes session_summaries.notes
|
|
159
|
+
* @returns {'haiku'|'report'|'titles'}
|
|
160
|
+
*/
|
|
161
|
+
export function summarySourceLabel(notes) {
|
|
162
|
+
const { done } = parseSummaryNotes(notes);
|
|
163
|
+
return done === 'model' ? 'haiku' : done;
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
/** Inverse of parseSummaryNotes. `lines` must already be scrubbed. */
|
|
155
167
|
export function formatSummaryNotes({ done, left, lines }, max) {
|
|
156
168
|
const head = `done${done} left${left}`;
|