claude-mem-lite 6.27.0 → 6.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +35 -5
- package/README.zh-CN.md +20 -2
- package/adopt-cli.mjs +36 -2
- package/claudemd.mjs +45 -8
- package/cli/activity.mjs +45 -17
- package/cli/common.mjs +17 -23
- package/cli/fts-check.mjs +6 -0
- package/cli.mjs +84 -18
- package/deep-search.mjs +10 -3
- package/haiku-client.mjs +44 -5
- package/hash-utils.mjs +43 -2
- package/hook-context.mjs +8 -1
- package/hook-handoff.mjs +52 -19
- package/hook-llm.mjs +188 -21
- package/hook-optimize.mjs +88 -60
- package/hook-semaphore.mjs +7 -1
- package/hook-shared.mjs +116 -6
- package/hook.mjs +223 -43
- package/install.mjs +501 -77
- package/lib/activity.mjs +9 -3
- package/lib/blank-text.mjs +21 -0
- package/lib/cite-back-hint.mjs +2 -0
- package/lib/cite-recall-path.mjs +26 -3
- package/lib/compress-core.mjs +10 -2
- package/lib/db-busy.mjs +18 -0
- package/lib/db-unusable.mjs +11 -4
- package/lib/deferred-work.mjs +15 -1
- package/lib/delete-core.mjs +4 -1
- package/lib/edit-distance.mjs +25 -0
- package/lib/export-columns.mjs +6 -0
- package/lib/get-core.mjs +17 -1
- package/lib/handoff-constants.mjs +8 -0
- package/lib/hook-prune.mjs +168 -25
- package/lib/id-routing.mjs +25 -1
- package/lib/import-jsonl.mjs +45 -14
- package/lib/install-shape.mjs +73 -0
- package/lib/maintain-core.mjs +74 -13
- package/lib/memory-input-guard.mjs +11 -6
- package/lib/npx-run.mjs +39 -0
- package/lib/observation-write.mjs +24 -6
- package/lib/plugin-key.mjs +58 -2
- package/lib/recall-shown.mjs +78 -1
- package/lib/record-once.mjs +4 -1
- package/lib/save-enrich.mjs +58 -9
- package/lib/save-observation.mjs +37 -10
- package/lib/search-core.mjs +34 -2
- package/lib/startup-dashboard.mjs +28 -6
- package/lib/time-constants.mjs +42 -0
- package/lib/timeline-core.mjs +34 -13
- package/lib/verify-apply-core.mjs +3 -1
- package/mem-cli.mjs +357 -55
- package/nlp.mjs +57 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -1
- package/project-utils.mjs +85 -2
- package/rerank.mjs +3 -2
- package/schema.mjs +181 -46
- package/scoring-sql.mjs +6 -3
- package/scripts/hook-launcher.mjs +52 -6
- package/scripts/launch.mjs +12 -1
- package/scripts/post-tool-use.sh +35 -7
- package/scripts/pre-tool-recall.js +3 -1
- package/scripts/user-prompt-search.js +9 -3
- package/search-scoring.mjs +9 -1
- package/server/fts-check.mjs +8 -10
- package/server.mjs +154 -40
- package/source-files.mjs +186 -169
- package/tool-schemas.mjs +103 -24
- package/utils.mjs +11 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.28.1",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.28.1",
|
|
4
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"
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
88
88
|
- **Observation relations** -- Bidirectional links between related observations based on file overlap
|
|
89
89
|
- **User prompt capture** -- Records user prompts via UserPromptSubmit hook for intent tracking
|
|
90
90
|
- **Read file tracking** -- Tracks files read during sessions for richer episode context
|
|
91
|
-
- **Degraded fallback when the LLM fails** -- An episode
|
|
91
|
+
- **Degraded fallback when the LLM fails** -- An episode that fixed an error is saved with inferred metadata (as an activity event) instead of being discarded, when a command that exited 0 printed the error (`npm test 2>&1 | tail`): a command Claude Code reports as failed does not enter the episode, so a fix that follows one counts as edit-only. An edit-only episode, config and schema changes included, is summarised from raw tool output, which the noise rules drop in most cases (`Modified a.js, b.js` with no lesson; only a short single-file summary survives) — as they do when the model answers without a lesson — so a failing provider loses those episodes unless `CLAUDE_MEM_KEEP_LOW_SIGNAL=1` is set; `claude-mem-lite doctor` warns when the `claude` CLI it would call does not resolve
|
|
92
92
|
- **Two-tier dedup** -- Jaccard similarity (5-minute window) + MinHash signatures (7-day cross-session window) prevent duplicates
|
|
93
93
|
- **Synonym expansion** -- Abbreviations like `K8s`, `DB`, `auth` automatically expand to full forms in FTS5 search (100+ pairs including CJK↔EN cross-language mappings)
|
|
94
94
|
- **CJK synonym extraction** -- Unsegmented Chinese text is scanned for known vocabulary words (数据库→database, 搜索→search, etc.) enabling cross-language memory recall
|
|
@@ -237,6 +237,36 @@ 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.28.1
|
|
241
|
+
|
|
242
|
+
**After the update, events are re-indexed so Chinese text in them is searchable** (the Han ranges
|
|
243
|
+
U+4E00–9FFF and U+3400–4DBF; kana, Hangul and the rarer Han extensions are not segmented). No
|
|
244
|
+
schema-version change: 6.27.0 still opens, searches and writes the database after this release has,
|
|
245
|
+
and an event it writes is filled the next time this release opens the database.
|
|
246
|
+
|
|
247
|
+
- **What it costs.** The first open adds a column, rebuilds the events index and adds the Latin
|
|
248
|
+
identifiers glued to Chinese text to existing observations' index text. Existing events are then
|
|
249
|
+
filled newest first, about 250 ms per database open (checked after each row), each batch
|
|
250
|
+
committed, so a killed open keeps its progress; with nothing left an open takes 3–4 ms. On the
|
|
251
|
+
maintainer's store (6963 events) the first three opens took 295–327, 80–102 and 4 ms, and the file
|
|
252
|
+
grew from 14 to 15 MB; on a CJK-heavy synthetic store of 200,000 events the first open takes 964
|
|
253
|
+
ms, the fill 79 opens, and the file grows from 93 to 273 MB. Until an event is filled, a Chinese
|
|
254
|
+
query misses it as before.
|
|
255
|
+
- **The MCP server waits out another writer's lock at startup** (up to 15 s of retries) instead of
|
|
256
|
+
exiting with "database is locked".
|
|
257
|
+
- **Exit codes.** Some commands that exited 0 now exit 1, among them `save`, `update`, `defer add`
|
|
258
|
+
and `activity save` given a flag they do not take, `save` and `defer add` given a mistyped
|
|
259
|
+
project, `delete` given an id list with a malformed part (`1,abc`), `self-update` when it checked
|
|
260
|
+
nothing, and `import-jsonl` on lines none of which is a transcript event (an empty file still
|
|
261
|
+
exits 0); CHANGELOG.md lists the rest. `install` exits 3 when only the MCP registration failed. A
|
|
262
|
+
script that relied on exit 0 there needs a look.
|
|
263
|
+
- **Also in this release:** `install` no longer empties the marketplace clone's `hooks/hooks.json`
|
|
264
|
+
(it puts back, with `git checkout`, one an earlier install emptied); four MCP tool texts say what
|
|
265
|
+
the tools do; a hook under a long write lock exits instead of being killed (UserPromptSubmit and
|
|
266
|
+
SessionStart with their context), and a lock that ends between 3.5 s and the 5 s kill, which
|
|
267
|
+
6.27.0 waited out, now drops the write the hook was waiting to make (UserPromptSubmit's prompt
|
|
268
|
+
row, for one). Details in CHANGELOG.md.
|
|
269
|
+
|
|
240
270
|
## Upgrading to 6.25.0
|
|
241
271
|
|
|
242
272
|
**Every session gets the same first line on a file-recall block again.** No schema-version
|
|
@@ -824,7 +854,7 @@ Episodes are batched related operations (edits to the same file group) that get
|
|
|
824
854
|
Episode buffer -> Flush to JSON -> claude -p --model haiku -> Structured observation -> SQLite
|
|
825
855
|
```
|
|
826
856
|
|
|
827
|
-
Each observation includes type, title, narrative, concepts, facts, importance (1-3), and is automatically deduplicated via two tiers: Jaccard similarity (>70% within 5 minutes) and MinHash signatures (>80% within 7 days across sessions). If the LLM call fails, an episode
|
|
857
|
+
Each observation includes type, title, narrative, concepts, facts, importance (1-3), and is automatically deduplicated via two tiers: Jaccard similarity (>70% within 5 minutes) and MinHash signatures (>80% within 7 days across sessions). If the LLM call fails, an episode that fixed an error a successful command printed is saved with inferred metadata (a command reported as failed does not enter the episode); an edit-only episode (config and schema changes included) is usually dropped as noise. Related observations are linked via `related_ids` based on FTS5 title similarity and file overlap.
|
|
828
858
|
|
|
829
859
|
## Management Commands
|
|
830
860
|
|
|
@@ -1232,7 +1262,7 @@ what is already stored — only whether new work runs.
|
|
|
1232
1262
|
|----------|-------------|---------|
|
|
1233
1263
|
| `CLAUDE_MEM_SKIP_SUMMARY` | Skip the background LLM session summary at **both** of its spawn sites — `Stop`, and the SessionStart `/clear`-handoff path. Until v5.3.0 only the `Stop` one honoured it. | _(runs)_ |
|
|
1234
1264
|
| `CLAUDE_MEM_LEGACY_STOP_UNLINK` | Restore the pre-v5.4.0 behaviour where `Stop` deletes the session file. Documented revert path for the session-lifecycle change, not a supported configuration: it re-mints a mem session per turn and makes the `/clear` handoff unreachable again. Only reach for it on a host that fires `Stop` once per session rather than once per turn. | _(file kept)_ |
|
|
1235
|
-
| `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush
|
|
1265
|
+
| `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush. Only the rule-based pre-save runs, through the noise gate: an error-fixing episode with a low-signal title ("Error: …") is dropped, where a failed call would save it as an activity event; an edit-only one is usually dropped as noise. It also turns off SessionStart's hand-off of a killed episode worker's flush file to a new worker. | _(runs)_ |
|
|
1236
1266
|
| `CLAUDE_MEM_SKIP_SAVE_ENRICH` | Skip the background Haiku call that backfills `lesson_learned` / search aliases after a save. | _(runs)_ |
|
|
1237
1267
|
| `CLAUDE_MEM_SKIP_COMPRESS` | Skip auto-compression of old observations. | _(runs)_ |
|
|
1238
1268
|
| `CLAUDE_MEM_SKIP_MAINTAIN` | Skip the 24h auto-maintain pass (decay, purge, backup). | _(runs)_ |
|
|
@@ -1242,7 +1272,7 @@ what is already stored — only whether new work runs.
|
|
|
1242
1272
|
| `CLAUDE_MEM_SKIP_UPDATE` | Skip the 24h update check against GitHub Releases, its session-start notice, and `self-update` / `install.mjs update` (unset it to update). A `repair` triggered by a missing module or dependency still runs. | _(runs)_ |
|
|
1243
1273
|
| `CLAUDE_MEM_SKIP_SIG_VERIFY` | Skip Ed25519 signature verification of a downloaded update. **Escape hatch — leaves updates unauthenticated.** | _(verifies)_ |
|
|
1244
1274
|
| `CLAUDE_MEM_NO_LESSON_RETRY` | `1` disables the one-shot retry that re-asks for a missing `lesson_learned`. | _(retries)_ |
|
|
1245
|
-
| `CLAUDE_MEM_FLUSH_TIMEOUT` | Seconds the
|
|
1275
|
+
| `CLAUDE_MEM_FLUSH_TIMEOUT` | Seconds the background session-summary worker waits for its pending episode flushes; it stops as soon as they clear. The default spans the episode worker's longest legitimate run. | `275` |
|
|
1246
1276
|
| `CLAUDE_MEM_BACKUP_BUDGET_MB` | Disk budget for backup snapshots; the next maintain/save evicts oldest snapshots past the 7-day undo grace. | `256` |
|
|
1247
1277
|
|
|
1248
1278
|
### Experimental
|
|
@@ -1311,7 +1341,7 @@ The original called an LLM on every tool use with raw JSON inputs. claude-mem-li
|
|
|
1311
1341
|
|
|
1312
1342
|
### Does this work cross-project? Cross-machine?
|
|
1313
1343
|
|
|
1314
|
-
|
|
1344
|
+
Each memory belongs to the project it was saved in (an id like `parent--dir`). Search covers every project and weights the current project's rows ×2 in the ranking; name a project (`project` on the MCP tools, `--project` on the CLI) to search only that one. `mem_recent`, mem_search's `obs_type` fallback and the timeline without an anchor use the current project. Single-machine only (SQLite, not networked). Use `mem_export` (JSON / JSONL) to back up or migrate between machines.
|
|
1315
1345
|
|
|
1316
1346
|
### What about privacy? Does it call external APIs?
|
|
1317
1347
|
|
package/README.zh-CN.md
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
- **观察关联** -- 基于文件重叠自动建立观察之间的双向链接
|
|
68
68
|
- **用户提示捕获** -- 通过 UserPromptSubmit 钩子记录用户提示,追踪用户意图
|
|
69
69
|
- **Read 文件追踪** -- 追踪会话中读取的文件,丰富 episode 上下文
|
|
70
|
-
- **LLM 失败时的降级保存** --
|
|
70
|
+
- **LLM 失败时的降级保存** -- 修掉了报错的 episode 用推断的元数据保存(作为活动事件),而非丢弃,前提是报错由一条正常退出的命令打印出来(如 `npm test 2>&1 | tail`):Claude Code 报告为失败的命令不进入 episode,紧随其后的修复按只有编辑的 episode 处理;只有编辑的 episode(包括改配置、改 schema)只能用原始工具输出拼成摘要,多数会被噪声规则丢掉(`Modified a.js, b.js` 且没有 lesson;只有很短的单文件摘要能留下)——模型成功但没给 lesson 时也一样——所以 LLM 不可用期间这类 episode 会丢失,除非设了 `CLAUDE_MEM_KEEP_LOW_SIGNAL=1`。`claude-mem-lite doctor` 会在要调用的 `claude` CLI 不存在时告警
|
|
71
71
|
- **两级去重** -- Jaccard 相似度(5 分钟窗口)+ MinHash 签名(7 天跨会话窗口)双重防重
|
|
72
72
|
- **同义词扩展** -- 缩写如 `K8s`、`DB`、`auth` 在 FTS5 搜索时自动扩展为全称(48+ 对)
|
|
73
73
|
- **伪相关反馈(PRF)** -- 首轮结果作为种子扩展查询,提升召回率
|
|
@@ -199,6 +199,24 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
199
199
|
repos/ # 浅克隆的源代码仓库
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
## 升级到 6.28.1
|
|
203
|
+
|
|
204
|
+
**更新后会给事件重建索引,事件里的中文从此能搜到**(切分 U+4E00–9FFF 和 U+3400–4DBF 两段汉字;假名、韩文和更少见的汉字扩展区不切分)。
|
|
205
|
+
没有 schema 版本变更:本版本打开过的数据库,6.27.0 仍能打开、搜索和写入;它写入的事件,本版本下一次打开数据库时会补上。
|
|
206
|
+
|
|
207
|
+
- **代价。** 第一次打开时加一列、重建事件索引,并给已有 observation 的索引文本补上粘在中文里的英文标识符。已有事件随后从新到旧补齐,
|
|
208
|
+
每次打开数据库约做 250 ms(每切分一行检查一次),每一批单独提交,所以被杀的那次打开也会保留进度;补完之后每次打开 3–4 ms。
|
|
209
|
+
在维护者的库上(6963 条事件)前三次打开分别为 295–327、80–102、4 ms,文件从 14 MB 变为 15 MB;在中文较多的 20 万条事件合成库上,
|
|
210
|
+
第一次打开 964 ms,补齐需 79 次打开,文件从 93 MB 变为 273 MB。补齐之前,用中文查询仍搜不到这条事件,和以前一样。
|
|
211
|
+
- **MCP server 启动时遇到别的进程的写锁会等待**(最多重试 15 s),不再报 "database is locked" 退出。
|
|
212
|
+
- **退出码。** 一些原来退出 0 的命令现在退出 1,其中包括:`save`、`update`、`defer add`、`activity save` 遇到不接受的 flag,
|
|
213
|
+
`save`、`defer add` 遇到拼错的项目名,`delete` 的 id 列表里有格式错误的部分(`1,abc`),`self-update` 什么都没检查成,`import-jsonl`
|
|
214
|
+
的输入里没有一行是 transcript 事件(空文件仍退出 0);其余见 CHANGELOG.md。其余步骤都成功、只有 MCP 注册失败时,`install` 退出 3。
|
|
215
|
+
依赖这些地方退出 0 的脚本需要检查。
|
|
216
|
+
- **本版本还有:** `install` 不再清空 marketplace clone 里的 `hooks/hooks.json`(早先被清空的,会用 `git checkout` 恢复);
|
|
217
|
+
四处 MCP 工具文本改成了工具实际做的事;钩子遇到长时间的写锁时会自行退出,不再被杀(UserPromptSubmit 和 SessionStart 仍带着上下文),
|
|
218
|
+
代价是:锁在 3.5 s 之后、5 s 被杀之前结束时,6.27.0 会等到并完成的那次写入(例如 UserPromptSubmit 的 prompt 行),现在会丢掉。详见 CHANGELOG.md。
|
|
219
|
+
|
|
202
220
|
## 升级到 6.25.0
|
|
203
221
|
|
|
204
222
|
**文件召回块的第一行,所有会话重新用同一种写法。** 没有 schema 版本变更:本版本打开过的数据库,6.24.0 仍能打开。
|
|
@@ -660,7 +678,7 @@ Episode 是一批相关操作(对同一组文件的编辑),由后台 LLM w
|
|
|
660
678
|
Episode 缓冲区 -> 刷新为 JSON -> claude -p --model haiku -> 结构化观察 -> SQLite
|
|
661
679
|
```
|
|
662
680
|
|
|
663
|
-
每条观察包含类型、标题、叙述、概念、事实和重要度(1-3),并通过两级机制自动去重:Jaccard 相似度(5 分钟内 >70%)和 MinHash 签名(7 天跨会话 >80%)。LLM
|
|
681
|
+
每条观察包含类型、标题、叙述、概念、事实和重要度(1-3),并通过两级机制自动去重:Jaccard 相似度(5 分钟内 >70%)和 MinHash 签名(7 天跨会话 >80%)。LLM 调用失败时,修掉了报错(报错由正常退出的命令打印;报告为失败的命令不进入 episode)的 episode 用推断的元数据保存降级记录;只有编辑的 episode(包括改配置、改 schema)多数会作为噪声丢掉。相关观察通过 FTS5 标题相似度和文件重叠自动建立 `related_ids` 链接。
|
|
664
682
|
|
|
665
683
|
## 管理命令
|
|
666
684
|
|
package/adopt-cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
writeFileSync,
|
|
24
24
|
unlinkSync,
|
|
25
25
|
readFileSync,
|
|
26
|
+
realpathSync,
|
|
26
27
|
} from 'fs';
|
|
27
28
|
import { claudeConfigDir, claudeStatePath } from './lib/data-paths.mjs';
|
|
28
29
|
import { join, isAbsolute, relative, dirname } from 'path';
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
detailDocPath,
|
|
51
52
|
addAgentsImports,
|
|
52
53
|
holdsOnlyOwnLines,
|
|
54
|
+
straySentinelLines,
|
|
53
55
|
} from './claudemd.mjs';
|
|
54
56
|
import { PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc } from './adopt-content.mjs';
|
|
55
57
|
import {
|
|
@@ -100,8 +102,23 @@ function log(msg) {
|
|
|
100
102
|
console.log(msg);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// $PWD keeps a symlinked project's logical spelling (the one Claude Code's CLAUDE_PROJECT_DIR
|
|
106
|
+
// uses), but only when it names the directory this process is in: a program that spawns `adopt`
|
|
107
|
+
// with `cwd` set leaves its OWN PWD in the env, and adopt wrote into the caller's directory
|
|
108
|
+
// instead of the one it ran in (E2E 2026-10-16).
|
|
109
|
+
/** @public install.mjs reads it through a dynamic-import namespace (`adopt.detectCwd?.()`). */
|
|
110
|
+
export function detectCwd() {
|
|
111
|
+
if (process.env.CLAUDE_PROJECT_DIR) return process.env.CLAUDE_PROJECT_DIR;
|
|
112
|
+
const cwd = process.cwd();
|
|
113
|
+
const pwd = process.env.PWD;
|
|
114
|
+
if (pwd) {
|
|
115
|
+
try {
|
|
116
|
+
if (realpathSync(pwd) === realpathSync(cwd)) return pwd;
|
|
117
|
+
} catch {
|
|
118
|
+
/* PWD names nothing that exists: use the real cwd */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return cwd;
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
function projectsRoot() {
|
|
@@ -182,6 +199,18 @@ function adoptOne(cwd, { force, dryRun }) {
|
|
|
182
199
|
const block = buildClaudeMdBlock();
|
|
183
200
|
const doc = getDetailDoc();
|
|
184
201
|
const version = CURRENT_SENTINEL_VERSION;
|
|
202
|
+
// A damaged sentinel line pairs with no block, so writing would add a second block beside it
|
|
203
|
+
// (E2E 2026-10-16 round 2, B14). Which lines are the block is the user's call: refuse first.
|
|
204
|
+
const stray = straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
|
|
205
|
+
if (stray.length > 0) {
|
|
206
|
+
log(`[adopt] ${cwd} → refused, nothing was written:`);
|
|
207
|
+
for (const s of stray) log(` ${claudeMdPath(cwd)} line ${s.line}: ${s.text}`);
|
|
208
|
+
log(
|
|
209
|
+
` ${stray.length === 1 ? 'This sentinel line pairs' : 'These sentinel lines pair'} with no well-formed block, so adopt would add a second one. Make the block's first line exactly \`<!-- ${PLUGIN_SLUG}:begin ${version} -->\` and its last \`<!-- ${PLUGIN_SLUG}:end -->\`, each on a line of its own, or delete the marker lines and the text between them; then run adopt again.`,
|
|
210
|
+
);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return { action: 'refused' };
|
|
213
|
+
}
|
|
185
214
|
// Claude Code stops reading AGENTS.md once a CLAUDE.md exists: the one written here imports
|
|
186
215
|
// the AGENTS.md files beside it and names the ones it cannot import.
|
|
187
216
|
const agents = agentsMdForNewClaudeMd(cwd, PLUGIN_SLUG);
|
|
@@ -347,6 +376,11 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
|
|
|
347
376
|
const hasBlock = blockIn(cwd);
|
|
348
377
|
if (!hasBlock) {
|
|
349
378
|
if (markerDir && markerKey) writeMarker(markerDir, markerKey);
|
|
379
|
+
// A damaged sentinel line (`… --> (team note)`) leaves the block's text in CLAUDE.md, which
|
|
380
|
+
// the host loads, while the block no longer parses: a copy written to CLAUDE.local.md or
|
|
381
|
+
// injected here was a second one. Neither; `adopt` names the line and the fix (D#369 B14).
|
|
382
|
+
const stray = straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
|
|
383
|
+
if (stray.length > 0) return { ok: true, action: 'damaged', reason: 'stray-sentinel', lines: stray };
|
|
350
384
|
const root = localSteeringRoot(cwd);
|
|
351
385
|
if (root) {
|
|
352
386
|
// A session started below the top-level (pre-tag claims review P2-3, P1-4): the host
|
package/claudemd.mjs
CHANGED
|
@@ -104,6 +104,35 @@ function orphanSentinelCount(raw, slug) {
|
|
|
104
104
|
});
|
|
105
105
|
return total - paired;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The lines of `p` that open with a sentinel of this slug and that no well-formed block
|
|
109
|
+
* accounts for: a begin marker with text after it, a lost version tag, a missing partner. The
|
|
110
|
+
* block pattern cannot see them, so a write that trusts it adds a second block beside them
|
|
111
|
+
* (E2E 2026-10-16 round 2, B14). A sentinel inside a sentence is prose, not a marker (pre-ship
|
|
112
|
+
* review P2-3), and is not listed.
|
|
113
|
+
* @param {string} p absolute path of the instructions file
|
|
114
|
+
* @param {string} slug
|
|
115
|
+
* @returns {{line: number, text: string}[]} 1-based; empty for a missing or unreadable file
|
|
116
|
+
*/
|
|
117
|
+
export function straySentinelLines(p, slug) {
|
|
118
|
+
let raw;
|
|
119
|
+
try {
|
|
120
|
+
raw = readFileSync(p, 'utf8');
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const spans = [...raw.matchAll(blockRegexG(slug))].map((m) => [m.index, m.index + m[0].length]);
|
|
125
|
+
const opensLine = new RegExp(`^\\s*<!-- ${escapeRe(slug)}:(?:begin|end)\\b`);
|
|
126
|
+
const stray = [];
|
|
127
|
+
let offset = 0;
|
|
128
|
+
raw.split('\n').forEach((text, i) => {
|
|
129
|
+
const at = offset + text.indexOf('<!--');
|
|
130
|
+
if (opensLine.test(text) && !spans.some(([s, e]) => at >= s && at < e))
|
|
131
|
+
stray.push({ line: i + 1, text: text.trim() });
|
|
132
|
+
offset += text.length + 1;
|
|
133
|
+
});
|
|
134
|
+
return stray;
|
|
135
|
+
}
|
|
107
136
|
function blockRegex(slug) {
|
|
108
137
|
return new RegExp(blockBody(escapeRe(slug)));
|
|
109
138
|
}
|
|
@@ -331,8 +360,8 @@ export function needsRefresh(cwd, { slug, version, block, doc }) {
|
|
|
331
360
|
/**
|
|
332
361
|
* Write (insert-or-replace) the managed block in CLAUDE.md and the detail doc
|
|
333
362
|
* under .claude/, plus a state sidecar. Idempotent on the user-visible files:
|
|
334
|
-
* re-running with identical inputs leaves CLAUDE.md
|
|
335
|
-
* byte unchanged
|
|
363
|
+
* re-running with identical inputs leaves CLAUDE.md, the detail doc and the state
|
|
364
|
+
* sidecar byte-for-byte unchanged.
|
|
336
365
|
*
|
|
337
366
|
* CLAUDE.md is created if absent. Only our slug-scoped region is rewritten;
|
|
338
367
|
* everything else in the file is preserved verbatim.
|
|
@@ -351,12 +380,20 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
|
|
|
351
380
|
const existingDoc = existsSync(dp) ? readFileSync(dp, 'utf8') : null;
|
|
352
381
|
if (existingDoc !== docContent) atomicWrite(dp, docContent);
|
|
353
382
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
383
|
+
// Only when what the sidecar records changed: it is committed alongside the block in a team
|
|
384
|
+
// repo, and a fresh writtenAt on every re-run of `adopt` left a diff each time (E2E
|
|
385
|
+
// 2026-10-16). Nothing reads writtenAt.
|
|
386
|
+
const blockHash = sha256(block);
|
|
387
|
+
const docHash = sha256(doc);
|
|
388
|
+
let prev = null;
|
|
389
|
+
try {
|
|
390
|
+
prev = JSON.parse(readFileSync(stateFilePath(cwd, slug), 'utf8'));
|
|
391
|
+
} catch {
|
|
392
|
+
/* absent or unreadable: write it */
|
|
393
|
+
}
|
|
394
|
+
if (!(prev?.version === version && prev?.blockHash === blockHash && prev?.docHash === docHash)) {
|
|
395
|
+
writeState(cwd, slug, { version, blockHash, docHash, writtenAt: new Date().toISOString() });
|
|
396
|
+
}
|
|
360
397
|
return { action };
|
|
361
398
|
}
|
|
362
399
|
|
package/cli/activity.mjs
CHANGED
|
@@ -13,12 +13,21 @@ import { resolveProject } from '../project-utils.mjs';
|
|
|
13
13
|
import { resolveCliProject as cliProject } from '../lib/cli-project.mjs';
|
|
14
14
|
import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
|
|
15
15
|
import { parseIntFlag, isNumericToken } from '../lib/cli-flags.mjs';
|
|
16
|
+
import { snapshotDb } from '../lib/db-backup.mjs';
|
|
17
|
+
import { isBlankText } from '../lib/blank-text.mjs';
|
|
16
18
|
|
|
17
19
|
function formatActivityResults(rows) {
|
|
18
20
|
if (!rows || rows.length === 0) return '(no events)';
|
|
19
21
|
return rows.map((r) => `#${r.id} [${r.event_type}] ${r.title}`).join('\n');
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** An event id as typed: N, #N or E#N (the form search and get print), else null. */
|
|
25
|
+
function eventIdOf(token) {
|
|
26
|
+
const m = /^(?:[Ee]#?|#)?(\d+)$/.exec(String(token).trim());
|
|
27
|
+
const n = m ? Number(m[1]) : NaN;
|
|
28
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export async function cmdActivity(db, args) {
|
|
23
32
|
const sub = args[0];
|
|
24
33
|
if (!sub) {
|
|
@@ -50,7 +59,7 @@ export async function cmdActivity(db, args) {
|
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
const title = flags.title || positional.join(' ').trim();
|
|
53
|
-
if (
|
|
62
|
+
if (isBlankText(title)) {
|
|
54
63
|
fail('[mem] activity save: --title or positional text required');
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
@@ -126,9 +135,11 @@ export async function cmdActivity(db, args) {
|
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
if (sub === 'show') {
|
|
129
|
-
const id = positional.length > 0 ?
|
|
130
|
-
if (
|
|
131
|
-
fail(
|
|
138
|
+
const id = positional.length > 0 ? eventIdOf(positional[0]) : null;
|
|
139
|
+
if (id === null) {
|
|
140
|
+
fail(
|
|
141
|
+
`[mem] activity show: an event id is required (N, #N or E#N)${positional[0] ? `, got "${positional[0]}"` : ''}`,
|
|
142
|
+
);
|
|
132
143
|
return;
|
|
133
144
|
}
|
|
134
145
|
const row = getEvent(db, id);
|
|
@@ -153,12 +164,20 @@ export async function cmdActivity(db, args) {
|
|
|
153
164
|
fail('[mem] Usage: claude-mem-lite activity delete <id1,id2,...> [--confirm]');
|
|
154
165
|
return;
|
|
155
166
|
}
|
|
156
|
-
const
|
|
167
|
+
const tokens = idStr
|
|
157
168
|
.split(',')
|
|
158
169
|
.map((s) => s.trim())
|
|
159
|
-
.filter(Boolean)
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
// Every token must be an id, or nothing is deleted: `2x` used to parseInt to 2 and delete
|
|
172
|
+
// a row the caller never named (E2E 2026-10-16).
|
|
173
|
+
const bad = tokens.filter((t) => eventIdOf(t) === null);
|
|
174
|
+
if (bad.length > 0) {
|
|
175
|
+
fail(
|
|
176
|
+
`[mem] activity delete: not an event id: ${bad.join(', ')} (use N, #N or E#N) — nothing was deleted`,
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const ids = [...new Set(tokens.map(eventIdOf))];
|
|
162
181
|
if (ids.length === 0) {
|
|
163
182
|
fail('[mem] activity delete: no valid IDs provided (must be positive integers)');
|
|
164
183
|
return;
|
|
@@ -188,16 +207,23 @@ export async function cmdActivity(db, args) {
|
|
|
188
207
|
return;
|
|
189
208
|
}
|
|
190
209
|
|
|
210
|
+
// A pre-image first, as deleteObservations takes for observations (E2E 2026-10-16 round 2:
|
|
211
|
+
// this hard delete had none). Best-effort; outside the transaction (VACUUM INTO).
|
|
212
|
+
snapshotDb(db, { tag: 'pre-delete' });
|
|
191
213
|
// The opt-in search telemetry keeps a title per returned result; an explicit delete drops the
|
|
192
214
|
// deleted events' titles, as lib/delete-core.mjs does for observations (D#245 N6).
|
|
193
|
-
const result = db
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const result = db
|
|
216
|
+
.transaction(() => {
|
|
217
|
+
if (
|
|
218
|
+
db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'search_results'").get()
|
|
219
|
+
) {
|
|
220
|
+
db.prepare(
|
|
221
|
+
`UPDATE search_results SET snapshot_label = NULL WHERE source = 'event' AND result_id IN (${placeholders})`,
|
|
222
|
+
).run(...ids);
|
|
223
|
+
}
|
|
224
|
+
return db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...ids);
|
|
225
|
+
})
|
|
226
|
+
.immediate(); // IMMEDIATE: it starts with a read (E2E 2026-10-16 round 3; see delete-core)
|
|
201
227
|
out(`[mem] Deleted ${result.changes} event(s).`);
|
|
202
228
|
return;
|
|
203
229
|
}
|
|
@@ -206,7 +232,9 @@ export async function cmdActivity(db, args) {
|
|
|
206
232
|
// P2(b): promote insight-bearing events (body present, importance>=N) into
|
|
207
233
|
// searchable observations. Preview by default; --execute applies. One-time
|
|
208
234
|
// backfill — the source events are marked promoted (idempotent re-runs).
|
|
209
|
-
|
|
235
|
+
// Whole digits only: a bare parseInt read `2x` as 2.
|
|
236
|
+
const rawMin = flags['min-importance'];
|
|
237
|
+
const minImp = rawMin === undefined ? 2 : /^\d+$/.test(String(rawMin)) ? Number(rawMin) : NaN;
|
|
210
238
|
if (isNaN(minImp) || minImp < 1 || minImp > 3) {
|
|
211
239
|
fail('[mem] activity promote: --min-importance must be 1, 2, or 3.');
|
|
212
240
|
return;
|
package/cli/common.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// relative-time formatting — every command imports from here so the CLI stays
|
|
9
9
|
// consistent.
|
|
10
10
|
|
|
11
|
+
import { editDistance } from '../lib/edit-distance.mjs';
|
|
11
12
|
import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from '../format-utils.mjs';
|
|
12
13
|
|
|
13
14
|
// ─── Argument Parsing ────────────────────────────────────────────────────────
|
|
@@ -17,7 +18,7 @@ import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from '../forma
|
|
|
17
18
|
* `--key value` → flags.key = value; `--flag` (no value) → flags.key = true.
|
|
18
19
|
* `-h` → flags.help = true.
|
|
19
20
|
*/
|
|
20
|
-
export function parseArgs(argv) {
|
|
21
|
+
export function parseArgs(argv, { track = true } = {}) {
|
|
21
22
|
const positional = [];
|
|
22
23
|
const flags = {};
|
|
23
24
|
// Canonical flag name for a raw `--key`. Two normalizations, both aimed at the
|
|
@@ -53,6 +54,13 @@ export function parseArgs(argv) {
|
|
|
53
54
|
let i = 0;
|
|
54
55
|
while (i < argv.length) {
|
|
55
56
|
const arg = argv[i];
|
|
57
|
+
// POSIX end of options: everything after a bare `--` is positional, so text that starts
|
|
58
|
+
// with `--` can be saved (`save -- "--dry-run is ignored by deploy.sh"`). It parsed as an
|
|
59
|
+
// empty-named flag and the text after it as a flag of its own (E2E 2026-10-16).
|
|
60
|
+
if (arg === '--') {
|
|
61
|
+
positional.push(...argv.slice(i + 1));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
56
64
|
if (arg.startsWith('--')) {
|
|
57
65
|
const body = arg.slice(2);
|
|
58
66
|
// `--key=value` (GNU long-option form). Split on the FIRST '=' so values that
|
|
@@ -83,7 +91,9 @@ export function parseArgs(argv) {
|
|
|
83
91
|
i++;
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
|
-
|
|
94
|
+
// `track: false` for the dispatcher's own peeks (typo hints, the unknown-project note): a read
|
|
95
|
+
// there is not the command consuming the flag, and recording it hid the inert-flag notice.
|
|
96
|
+
return { positional, flags: track ? trackFlagReads(flags) : flags };
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
// ─── Inert selection flags ───────────────────────────────────────────────────
|
|
@@ -269,7 +279,11 @@ export function fail(text) {
|
|
|
269
279
|
export function rejectBareStringFlags(flags, keys) {
|
|
270
280
|
for (const key of keys) {
|
|
271
281
|
if (flags[key] === true) {
|
|
272
|
-
|
|
282
|
+
// A value that itself starts with `-` parses as a flag of its own and lands here; the
|
|
283
|
+
// `=` form is the way to pass one, and nothing said so (E2E 2026-10-16 round 2).
|
|
284
|
+
fail(
|
|
285
|
+
`[mem] --${key} requires a value (received a bare flag with no value). A value that starts with - is written --${key}="…".`,
|
|
286
|
+
);
|
|
273
287
|
return true;
|
|
274
288
|
}
|
|
275
289
|
}
|
|
@@ -436,26 +450,6 @@ export const COMMAND_SCOPED_FLAGS = new Map([
|
|
|
436
450
|
['undo', 'verify-apply'],
|
|
437
451
|
]);
|
|
438
452
|
|
|
439
|
-
/** Levenshtein distance, early-exit past `max` (cheap enough for a handful of flags). */
|
|
440
|
-
function editDistance(a, b, max = 2) {
|
|
441
|
-
const m = a.length,
|
|
442
|
-
n = b.length;
|
|
443
|
-
if (Math.abs(m - n) > max) return max + 1;
|
|
444
|
-
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
445
|
-
for (let i = 1; i <= m; i++) {
|
|
446
|
-
const cur = [i];
|
|
447
|
-
let rowMin = i;
|
|
448
|
-
for (let j = 1; j <= n; j++) {
|
|
449
|
-
const d = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0));
|
|
450
|
-
cur[j] = d;
|
|
451
|
-
if (d < rowMin) rowMin = d;
|
|
452
|
-
}
|
|
453
|
-
if (rowMin > max) return max + 1; // whole row already past budget → give up
|
|
454
|
-
prev = cur;
|
|
455
|
-
}
|
|
456
|
-
return prev[n];
|
|
457
|
-
}
|
|
458
|
-
|
|
459
453
|
/**
|
|
460
454
|
* Detect likely-typo flags: names NOT in KNOWN_CLI_FLAGS but within edit distance 2 of
|
|
461
455
|
* a known flag. parseArgs silently drops unknown flags, so `save --improtance 3` used to
|
package/cli/fts-check.mjs
CHANGED
|
@@ -32,6 +32,12 @@ export function cmdFtsCheck(db, args) {
|
|
|
32
32
|
const result = checkFTSIntegrity(db);
|
|
33
33
|
if (result.healthy) {
|
|
34
34
|
out('[mem] FTS5 indexes are healthy — all integrity checks passed.');
|
|
35
|
+
} else if (result.busy) {
|
|
36
|
+
// Not a finding: "issues found" invited a rebuild of a healthy store, a long write that
|
|
37
|
+
// blocks the live hooks (E2E 2026-10-16 round 2). Exit 1 all the same: nothing was checked.
|
|
38
|
+
out('[mem] FTS5 check not run — the database is in use by another process. This is not');
|
|
39
|
+
out(' corruption; do not rebuild. Run the check again when the store is idle.');
|
|
40
|
+
process.exitCode = 1;
|
|
35
41
|
} else {
|
|
36
42
|
out(`[mem] FTS5 issues found:`);
|
|
37
43
|
for (const d of result.details) out(` ${d}`);
|