claude-mem-lite 6.23.3 → 6.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +45 -3
- package/README.zh-CN.md +29 -0
- package/bash-utils.mjs +36 -3
- package/cli/activity.mjs +10 -1
- package/haiku-client.mjs +6 -2
- package/hook-optimize.mjs +50 -20
- package/hook-shared.mjs +8 -4
- package/hook-update.mjs +139 -64
- package/hook.mjs +6 -1
- package/install.mjs +199 -69
- package/lib/anthropic-base-url.mjs +9 -2
- package/lib/data-paths.mjs +21 -0
- package/lib/err-sampler.mjs +2 -1
- package/lib/hook-telemetry.mjs +2 -0
- package/lib/install-shape.mjs +5 -5
- package/lib/llm-provider-probe.mjs +20 -4
- package/lib/maintain-core.mjs +19 -5
- package/lib/metrics.mjs +2 -0
- package/lib/native-binding-hint.mjs +5 -0
- package/lib/proc-lock.mjs +6 -1
- package/lib/recall-framing.mjs +18 -9
- package/lib/record-once.mjs +3 -1
- package/lib/save-enrich.mjs +2 -1
- package/lib/search-telemetry.mjs +34 -22
- package/lib/time-constants.mjs +7 -0
- package/lib/worker-data-dir.mjs +34 -0
- package/npm-shrinkwrap.json +39 -3134
- package/package.json +3 -2
- package/plugin-cache-guard.mjs +5 -8
- package/schema.mjs +12 -3
- package/scripts/post-tool-use.sh +16 -3
- package/scripts/setup.sh +56 -14
- package/server.mjs +4 -4
- package/source-files.mjs +4 -0
- package/utils.mjs +5 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.25.0",
|
|
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.25.0",
|
|
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
|
@@ -237,6 +237,44 @@ 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.25.0
|
|
241
|
+
|
|
242
|
+
**Every session gets the same first line on a file-recall block again.** No schema-version
|
|
243
|
+
change: 6.24.0 still opens the database after this release has.
|
|
244
|
+
|
|
245
|
+
- **What changes.** Since 6.16.0 half of sessions got a plain statement of where the notes come
|
|
246
|
+
from instead of "system-injected context, continue your planned action", so the two could be
|
|
247
|
+
compared by how often the agent cited a lesson. On the maintainer's own sessions the comparison
|
|
248
|
+
found no difference it could detect, and a scan of the agent's replies found neither line
|
|
249
|
+
surfaced to the user, so the split is closed: unset now means the older line in every session.
|
|
250
|
+
`CLAUDE_MEM_RECALL_FRAMING=factual` keeps the plain statement; `=ab` restores the split.
|
|
251
|
+
- **Also in this release:** when the data directory cannot be read, `doctor` says "not checked"
|
|
252
|
+
for what it could not look at and `status` names the directory, instead of reporting a missing
|
|
253
|
+
or empty store. `maintain execute --ops dedup` names the merge pairs it skips. Details in
|
|
254
|
+
CHANGELOG.md.
|
|
255
|
+
|
|
256
|
+
## Upgrading to 6.24.0
|
|
257
|
+
|
|
258
|
+
**If you set `CLAUDE_CONFIG_DIR`, the installer, updates and hooks now use that directory, as
|
|
259
|
+
Claude Code does.** No schema-version change: 6.23.3 still opens the database after this
|
|
260
|
+
release has. Without `CLAUDE_CONFIG_DIR` nothing in this item changes.
|
|
261
|
+
|
|
262
|
+
- **Who is affected.** Claude Code keeps `settings.json`, `plugins/` and `.claude.json` in
|
|
263
|
+
`CLAUDE_CONFIG_DIR` when it is set. The plugin kept using `~/.claude` and `~/.claude.json`:
|
|
264
|
+
`install` wrote its hooks where Claude Code never reads them, and with two profiles the
|
|
265
|
+
session-start setup and `self-update` pruned the other profile's plugin cache and edited its
|
|
266
|
+
`.claude.json`.
|
|
267
|
+
- **What to do.** Installed with npm or npx: run `claude-mem-lite install` once more with the
|
|
268
|
+
variable set, so the hooks land in your config directory. Plugin installs need nothing. Hook
|
|
269
|
+
entries an older version wrote into `~/.claude/settings.json` stay there; if no other profile
|
|
270
|
+
uses `~/.claude`, `env -u CLAUDE_CONFIG_DIR claude-mem-lite cleanup-hooks` removes them.
|
|
271
|
+
- **Also in this release:** `npm install -g claude-mem-lite` and `npx` no longer install the
|
|
272
|
+
development toolchain. As a dependency install it went from 298 packages / 541 MB to 96 / 57 MB;
|
|
273
|
+
after the release, `-g` and `npx` measured the same 96 packages. A confirmed
|
|
274
|
+
`maintain execute --ops purge_stale` now deletes every row its preview counted in one run,
|
|
275
|
+
behind one snapshot. `doctor` warns about a failing update check and about an
|
|
276
|
+
`ANTHROPIC_BASE_URL` that ends in `/v1`. Details in CHANGELOG.md.
|
|
277
|
+
|
|
240
278
|
## Upgrading to 6.23.1
|
|
241
279
|
|
|
242
280
|
**If you set `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY`, background LLM calls now go to that
|
|
@@ -568,9 +606,13 @@ depends on the Claude Code version. Allow the server once in the `settings.json`
|
|
|
568
606
|
{ "permissions": { "allow": ["mcp__plugin_claude-mem-lite_mem-lite"] } }
|
|
569
607
|
```
|
|
570
608
|
|
|
571
|
-
|
|
609
|
+
For a single run, `claude -p … --allowedTools "mcp__plugin_claude-mem-lite_mem-lite"` does the
|
|
610
|
+
same (both forms checked on 2.1.292, 2026-10-07: no refusal, and `mem_search` answered). That is
|
|
611
|
+
the server's name under a plugin install. An npx or git-clone install registers it as
|
|
572
612
|
`mem-lite`, so the rule there is `mcp__mem-lite`. An interactive session asks instead; answering
|
|
573
|
-
"always allow" there records a rule of its own.
|
|
613
|
+
"always allow" there records a rule of its own. The plugin does not tell the model to fall back
|
|
614
|
+
to the CLI when a call is refused: the refusal is Claude Code's permission check for a tool you
|
|
615
|
+
have not allowed, and routing the same action through Bash would sidestep it.
|
|
574
616
|
|
|
575
617
|
### Skill Commands (in Claude Code chat)
|
|
576
618
|
|
|
@@ -1209,7 +1251,7 @@ and names can change between releases.
|
|
|
1209
1251
|
|----------|-------------|---------|
|
|
1210
1252
|
| `CLAUDE_MEM_TASK_IMPERATIVE` | `on`/`1` injects the single most relevant lesson at prompt position under an imperative template. | _(off)_ |
|
|
1211
1253
|
| `CLAUDE_MEM_SUBAGENT_INJECT` | Dispatch-time memory injection for subagents. | _(off)_ |
|
|
1212
|
-
| `CLAUDE_MEM_RECALL_FRAMING` | First line of a PreToolUse / PostToolUse recall block. `
|
|
1254
|
+
| `CLAUDE_MEM_RECALL_FRAMING` | First line of a PreToolUse / PostToolUse recall block. Unset or `legacy` = "system-injected context, continue your planned action"; `factual` = a plain statement of where the notes come from; `ab` = each session gets one of the two, for `benchmark/citation-live-replay.mjs --by-framing`. Any other value is `legacy`. | `legacy` |
|
|
1213
1255
|
| `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 adoption row in CLAUDE.md and the detail doc keep the new wording, and since 6.17.1 the directive says it overrides them); `bind` / `bridge` = comprehension-bridge arms (`bridge` keeps the pre-6.17 verdict wording as its fallback; neither says it overrides the adopted text, so their measured wording stays fixed); `legacy` = no directive. | _(unset)_ |
|
|
1214
1256
|
| `CLAUDE_MEM_EDGE_DECAY` | Enables decay of file↔observation edges. | _(off)_ |
|
|
1215
1257
|
| `CLAUDE_MEM_EDGE_DECAY_K` | Edge-decay threshold when the flag above is on (clamped to ≥1). | `3` |
|
package/README.zh-CN.md
CHANGED
|
@@ -199,6 +199,35 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
199
199
|
repos/ # 浅克隆的源代码仓库
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
## 升级到 6.25.0
|
|
203
|
+
|
|
204
|
+
**文件召回块的第一行,所有会话重新用同一种写法。** 没有 schema 版本变更:本版本打开过的数据库,6.24.0 仍能打开。
|
|
205
|
+
|
|
206
|
+
- **变了什么。** 从 6.16.0 起,一半会话的第一行是说明笔记来源的事实陈述,另一半保留 "system-injected context,
|
|
207
|
+
continue your planned action",用来比较 agent 引用教训的频率。在维护者自己的会话上,两种写法看不出差别,
|
|
208
|
+
按回复文本扫描,两组里 agent 也都没有把这一行当作可疑内容告诉用户,所以结束分组:
|
|
209
|
+
不设变量时,所有会话都用旧的那一行。
|
|
210
|
+
`CLAUDE_MEM_RECALL_FRAMING=factual` 保留事实陈述的写法,`=ab` 恢复分组。
|
|
211
|
+
- **本版本还有:** 数据目录读不了时,`doctor` 对看不到的项目显示“not checked”,`status` 直接指出这个目录
|
|
212
|
+
无法访问,不再报成数据库缺失或为空。`maintain execute --ops dedup` 会列出跳过的合并对。详见 CHANGELOG.md。
|
|
213
|
+
|
|
214
|
+
## 升级到 6.24.0
|
|
215
|
+
|
|
216
|
+
**如果你设置了 `CLAUDE_CONFIG_DIR`,安装程序、更新和 hook 现在都使用这个目录,和 Claude Code 一致。**
|
|
217
|
+
没有 schema 版本变更:本版本打开过的数据库,6.23.3 仍能打开。没设 `CLAUDE_CONFIG_DIR` 时,这一条不带来任何变化。
|
|
218
|
+
|
|
219
|
+
- **受影响的是谁。** 设了 `CLAUDE_CONFIG_DIR` 时,Claude Code 把 `settings.json`、`plugins/` 和 `.claude.json`
|
|
220
|
+
都放在那个目录里,插件却一直用 `~/.claude` 和 `~/.claude.json`:`install` 把 hook 写到了 Claude Code 不读的
|
|
221
|
+
地方;有两个配置目录时,会话启动的整理步骤和 `self-update` 会清掉另一个配置目录的插件缓存,并改动它的
|
|
222
|
+
`.claude.json`。
|
|
223
|
+
- **要做什么。** 用 npm 或 npx 安装的:在设好这个变量的情况下再运行一次 `claude-mem-lite install`,让 hook 写进
|
|
224
|
+
你的配置目录。插件安装不用做什么。旧版本写进 `~/.claude/settings.json` 的 hook 条目会留在那里;如果没有别的
|
|
225
|
+
配置目录在用 `~/.claude`,可以用 `env -u CLAUDE_CONFIG_DIR claude-mem-lite cleanup-hooks` 清掉。
|
|
226
|
+
- **本版本还有:** `npm install -g claude-mem-lite` 和 `npx` 不再安装开发工具链。按依赖方式安装,从 298 个包 /
|
|
227
|
+
541 MB 降到 96 个包 / 57 MB;发布后实测 `-g` 和 `npx` 也是同样的 96 个包。确认执行的
|
|
228
|
+
`maintain execute --ops purge_stale` 现在一次运行就删掉预览里统计的全部行,只做一次快照。`doctor` 会对失败的
|
|
229
|
+
更新检查、以及以 `/v1` 结尾的 `ANTHROPIC_BASE_URL` 发出警告。详见 CHANGELOG.md。
|
|
230
|
+
|
|
202
231
|
## 升级到 6.23.1
|
|
203
232
|
|
|
204
233
|
**如果你设置了 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_API_KEY`,后台 LLM 调用现在会发往那个网关。**
|
package/bash-utils.mjs
CHANGED
|
@@ -632,6 +632,28 @@ function withoutDiffHunks(text) {
|
|
|
632
632
|
// Paths excluded from observation capture (ephemeral / virtual filesystems) — applied
|
|
633
633
|
// uniformly to both command-parsed paths and direct file_path/path/filePath fields.
|
|
634
634
|
|
|
635
|
+
// A line a test runner prints for one test (`# Subtest:` heads a failing one too): it carries the
|
|
636
|
+
// test's name, not a summary.
|
|
637
|
+
const PASSED_TEST_LINE_RE = /^[ \t]*(?:✔|✓|ok[ \t]+\d+[ \t]+-|# Subtest:)/;
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Whether test-runner output carries a red summary or failure banner (detectBashSignificance's
|
|
641
|
+
* veto on a green "0 fail" marker; D#179).
|
|
642
|
+
* @param {string} scan
|
|
643
|
+
* @returns {boolean}
|
|
644
|
+
*/
|
|
645
|
+
function hasRedTestSummary(scan) {
|
|
646
|
+
const lines = scan
|
|
647
|
+
.split('\n')
|
|
648
|
+
.filter((l) => !PASSED_TEST_LINE_RE.test(l))
|
|
649
|
+
.join('\n');
|
|
650
|
+
return (
|
|
651
|
+
/\b[1-9]\d*[ \t]+(?:fail|failed|failures|failing|errors?)\b(?=[ \t]*(?:$|[,;|(]|\.(?:[ \t]|$)|in[ \t]+\S))|^[ \t]*(?:ℹ|#)[ \t]*(?:fail|cancelled)[ \t]+[1-9]\d*[ \t]*$/im.test(
|
|
652
|
+
lines,
|
|
653
|
+
) || /^[ \t]*(?:--- )?FAIL(?:ED)?(?::|[ \t]|$)/m.test(lines)
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
635
657
|
/**
|
|
636
658
|
* Detect significance signals in a Bash command and its response.
|
|
637
659
|
* Checks for errors, test runs, builds, git operations, and deployments.
|
|
@@ -675,12 +697,23 @@ export function detectBashSignificance(input, response) {
|
|
|
675
697
|
// suites prints one summary each, and a node:test run whose failures were all timeouts says
|
|
676
698
|
// "fail 0" next to "cancelled 1" (pre-tag defect review, item 7 F1). Red = a nonzero fail /
|
|
677
699
|
// cancelled count in either summary form.
|
|
700
|
+
// D#179: a count-first red must sit where a summary puts it — end of line, or before `,` `;`
|
|
701
|
+
// `|` `(` `. ` or `in …` — on ONE line, and never on a line a runner prints for a PASSED test
|
|
702
|
+
// (node:test `✔ name (1ms)`, vitest / bun `✓ name`, TAP `ok 1 - name` and its `# Subtest:`),
|
|
703
|
+
// which carries the test's NAME. Matched anywhere and across newlines, a passing run whose test
|
|
704
|
+
// name or log said "3 failed" / "2 failed attempts" read red, and "want 2\nFAIL" read as
|
|
705
|
+
// "2 FAIL". `errors?` is red too (pytest "1 passed, 1 error", tsc "Found 1 error in …",
|
|
706
|
+
// eslint "(2 errors, 0 warnings)"). go / jest / vitest / shell-suite failures carry a
|
|
707
|
+
// case-sensitive FAIL / FAILED banner at line start ("--- FAIL:", "FAILED: 3 case(s)", pytest
|
|
708
|
+
// "FAILED test_x.py::…"), read on its own. The green marker still
|
|
709
|
+
// spans lines on purpose: made one-line, 17 of this machine's 77,819 recorded results flipped
|
|
710
|
+
// to errors, and 15 of them were passing batteries, file reads and reports that only ever
|
|
711
|
+
// matched "0\nFAIL" by accident; 2 were real failures (measured 2026-10-07, recounted by the
|
|
712
|
+
// pre-tag delta review).
|
|
678
713
|
const hasGreenTestSummary =
|
|
679
714
|
looksLikeError &&
|
|
680
715
|
/\b0\s+(fail|failed|failures)\b|^[ \t]*(?:ℹ|#)[ \t]*fail[ \t]+0[ \t]*$/im.test(scan) &&
|
|
681
|
-
|
|
682
|
-
scan,
|
|
683
|
-
);
|
|
716
|
+
!hasRedTestSummary(scan);
|
|
684
717
|
// NOTE: do not add `\bFAIL\s` here — with /i flag it would re-match the
|
|
685
718
|
// very `0 fail\n` token green-summary is trying to exempt. A real test
|
|
686
719
|
// failure produces "N fail" (N≥1) which never triggers hasGreenTestSummary,
|
package/cli/activity.mjs
CHANGED
|
@@ -188,7 +188,16 @@ export async function cmdActivity(db, args) {
|
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// The opt-in search telemetry keeps a title per returned result; an explicit delete drops the
|
|
192
|
+
// deleted events' titles, as lib/delete-core.mjs does for observations (D#245 N6).
|
|
193
|
+
const result = db.transaction(() => {
|
|
194
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'search_results'").get()) {
|
|
195
|
+
db.prepare(
|
|
196
|
+
`UPDATE search_results SET snapshot_label = NULL WHERE source = 'event' AND result_id IN (${placeholders})`,
|
|
197
|
+
).run(...ids);
|
|
198
|
+
}
|
|
199
|
+
return db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...ids);
|
|
200
|
+
})();
|
|
192
201
|
out(`[mem] Deleted ${result.changes} event(s).`);
|
|
193
202
|
return;
|
|
194
203
|
}
|
package/haiku-client.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { randomUUID } from 'crypto';
|
|
|
20
20
|
import { debugLog, debugCatch, parseJsonFromLLM } from './utils.mjs';
|
|
21
21
|
import { DB_DIR } from './schema.mjs';
|
|
22
22
|
import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
23
|
+
import { workerDirGone } from './lib/worker-data-dir.mjs';
|
|
23
24
|
import { httpConnectProxyFor, postViaConnectProxy, postDirectHttp } from './lib/proxy-fetch.mjs';
|
|
24
25
|
import { resolveAnthropicBaseUrl } from './lib/anthropic-base-url.mjs';
|
|
25
26
|
|
|
@@ -40,6 +41,9 @@ import { resolveAnthropicBaseUrl } from './lib/anthropic-base-url.mjs';
|
|
|
40
41
|
*/
|
|
41
42
|
function cliSpawnCwd() {
|
|
42
43
|
const dir = join(resolveRuntimeDir(DB_DIR), 'cli-cwd');
|
|
44
|
+
// A background worker whose data dir was removed during its API attempt must not re-create
|
|
45
|
+
// it here (D#265); the spawn then fails on the missing cwd, and the worker gives up.
|
|
46
|
+
if (workerDirGone(DB_DIR)) return dir;
|
|
43
47
|
try {
|
|
44
48
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
45
49
|
} catch {
|
|
@@ -555,8 +559,8 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
555
559
|
};
|
|
556
560
|
// Plain http is loopback-only (resolveAnthropicBaseUrl), and loopback must stay on this
|
|
557
561
|
// machine: native fetch would hand it, x-api-key in cleartext, to HTTP_PROXY under
|
|
558
|
-
// NODE_USE_ENV_PROXY=1. (D#250) Parsed, not a prefix test:
|
|
559
|
-
//
|
|
562
|
+
// NODE_USE_ENV_PROXY=1. (D#250) Parsed, not a prefix test: a prefix test let `HTTP://127.0.0.1`
|
|
563
|
+
// through to the proxy when the resolver still returned the raw text (b59e0cec, D#259).
|
|
560
564
|
const plainHttp = new URL(apiUrl).protocol === 'http:';
|
|
561
565
|
const send = (payload) => {
|
|
562
566
|
const json = JSON.stringify(payload);
|
package/hook-optimize.mjs
CHANGED
|
@@ -484,8 +484,8 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
484
484
|
// clampImportance floor it to 1 (kept visible, low-ranked) instead of hiding.
|
|
485
485
|
// A compression keeper is never hidden: hiding it hides every member compressed into it
|
|
486
486
|
// (the same rule NOT_COMPRESSION_KEEPER_SQL enforces on the maintenance writers). It falls
|
|
487
|
-
// through to the
|
|
488
|
-
// re-sent
|
|
487
|
+
// through instead: in narrow scope to the stamp-only write below (floored to importance 1
|
|
488
|
+
// and stamped, so it is not re-sent), in wide scope to the normal update.
|
|
489
489
|
const isKeeper = !!db
|
|
490
490
|
.prepare('SELECT 1 FROM observations WHERE compressed_into = ? LIMIT 1')
|
|
491
491
|
.get(cand.id);
|
|
@@ -494,12 +494,8 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
494
494
|
const humanSet =
|
|
495
495
|
(db.prepare('SELECT importance_set_at FROM observations WHERE id = ?').get(cand.id)
|
|
496
496
|
?.importance_set_at ?? null) !== null;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
scope !== 'wide' &&
|
|
500
|
-
!isKeeper &&
|
|
501
|
-
!humanSet
|
|
502
|
-
) {
|
|
497
|
+
const scoredZero = parsed.importance === 0 || parsed.importance === '0';
|
|
498
|
+
if (scoredZero && scope !== 'wide' && !isKeeper && !humanSet) {
|
|
503
499
|
// D#12, and this one is not a stale-write guard — it is a POINTER guard.
|
|
504
500
|
// `compressed_into` is the child -> keeper link, and COMPRESSED_AUTO is -1. If a
|
|
505
501
|
// concurrent cluster-merge or smart-compress adopts this row during the 45 s Haiku
|
|
@@ -511,7 +507,8 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
511
507
|
const res = db
|
|
512
508
|
.prepare(
|
|
513
509
|
`UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}, optimized_at = ?
|
|
514
|
-
WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL AND ${NOT_COMPRESSION_KEEPER_SQL}
|
|
510
|
+
WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL AND ${NOT_COMPRESSION_KEEPER_SQL}
|
|
511
|
+
AND importance_set_at IS NULL`,
|
|
515
512
|
)
|
|
516
513
|
.run(Date.now(), cand.id);
|
|
517
514
|
if (res.changes === 0) {
|
|
@@ -521,6 +518,31 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
521
518
|
processed++;
|
|
522
519
|
continue;
|
|
523
520
|
}
|
|
521
|
+
// A narrow 0 reply reaches this point only for a row that may not be hidden (a compression
|
|
522
|
+
// keeper, or an importance a person set). It judged the row worthless, so it is no basis for
|
|
523
|
+
// any of the row's text either: the stored title and narrative stay (D#207: 'Weekly summary:
|
|
524
|
+
// auth refactor' became 'Weekly summary', narrative 'x'), and so do type, lesson, concepts,
|
|
525
|
+
// facts, aliases and scope (D#268). Written instead: what hiding would have written bar the
|
|
526
|
+
// hide, i.e. the stamp, so the row is not re-sent, plus the importance floor below. Wide
|
|
527
|
+
// scope stays on the normal path: its rows are substantive by construction, so a 0 there
|
|
528
|
+
// reads as a misjudgment of importance, not of the row.
|
|
529
|
+
if (scoredZero && scope !== 'wide') {
|
|
530
|
+
const res = db
|
|
531
|
+
.prepare(
|
|
532
|
+
`UPDATE observations SET
|
|
533
|
+
importance = CASE WHEN importance_set_at IS NOT NULL THEN importance
|
|
534
|
+
ELSE MAX(?, COALESCE(NULLIF(importance, 0), 1)) END,
|
|
535
|
+
optimized_at = ?
|
|
536
|
+
WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL`,
|
|
537
|
+
)
|
|
538
|
+
.run(clampImportance(parsed.importance), Date.now(), cand.id);
|
|
539
|
+
if (res.changes === 0) {
|
|
540
|
+
skipped++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
processed++;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
524
546
|
|
|
525
547
|
// Enrichment ("add a lesson") must not reclassify a specific type down to the generic
|
|
526
548
|
// 'change' (lower TYPE_QUALITY + faster decay); keep the stored type on that downgrade.
|
|
@@ -562,8 +584,9 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
562
584
|
// fallback — truncating the row's own stored narrative to buy nothing was the same
|
|
563
585
|
// bug in miniature.
|
|
564
586
|
const isWide = scope === 'wide';
|
|
565
|
-
const
|
|
566
|
-
const
|
|
587
|
+
const keepStoredText = isWide;
|
|
588
|
+
const title = keepStoredText ? cand.title : truncate(scrubSecrets(parsed.title || ''), 120);
|
|
589
|
+
const narrative = keepStoredText
|
|
567
590
|
? cand.narrative
|
|
568
591
|
: parsed.narrative
|
|
569
592
|
? truncate(scrubSecrets(parsed.narrative), 500)
|
|
@@ -571,9 +594,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
571
594
|
// Floor at the stored importance: re-enrich adds a lesson, it must never silently downgrade
|
|
572
595
|
// a user-set/promoted importance (the UPDATE also sets optimized_at → the loss is permanent).
|
|
573
596
|
// Upgrades are still honored — except over an importance a person set (D10), which stays.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
597
|
+
// Decided in the UPDATE itself, on the row as it is then: the candidate's importance was read
|
|
598
|
+
// before the model call, and writing it back undid a person's change made during the call
|
|
599
|
+
// (D#206).
|
|
600
|
+
const modelImportance = clampImportance(parsed.importance);
|
|
577
601
|
|
|
578
602
|
const bigramText = cjkBigrams((title || '') + ' ' + (narrative || ''));
|
|
579
603
|
const textField = [conceptsText, factsText, searchAliases || '', bigramText].filter(Boolean).join(' ');
|
|
@@ -607,10 +631,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
607
631
|
// Narrow replaces title and narrative with model text, so an explicit save it rewrites (one
|
|
608
632
|
// whose save-time enrich failed) moves to the re-enrich writer's id, as a cluster-merge
|
|
609
633
|
// keeper does (D#146, D#138). Wide keeps the stored title and narrative (it fills the lesson
|
|
610
|
-
// and the side fields), so the row stays an explicit save
|
|
611
|
-
// best-effort.
|
|
634
|
+
// and the side fields), so the row stays an explicit save; a narrow 0 reply never gets here.
|
|
635
|
+
// The writer's session row is best-effort.
|
|
612
636
|
let rewriteSessionId = null;
|
|
613
|
-
if (!
|
|
637
|
+
if (!keepStoredText) {
|
|
614
638
|
const cur = db.prepare('SELECT memory_session_id FROM observations WHERE id = ?').get(cand.id);
|
|
615
639
|
if (cur?.memory_session_id?.startsWith(MANUAL_SESSION_ID_PREFIX)) {
|
|
616
640
|
const enrichSessionId = writerSessionId('enrich-', cand.project);
|
|
@@ -629,7 +653,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
629
653
|
.prepare(
|
|
630
654
|
`
|
|
631
655
|
UPDATE observations SET type=?, title=?, narrative=?, concepts=?, facts=?,
|
|
632
|
-
text=?,
|
|
656
|
+
text=?,
|
|
657
|
+
importance = CASE WHEN importance_set_at IS NOT NULL THEN importance
|
|
658
|
+
ELSE MAX(?, COALESCE(NULLIF(importance, 0), 1)) END,
|
|
659
|
+
lesson_learned=?, search_aliases=?, minhash_sig=?, optimized_at=?,
|
|
633
660
|
scope=COALESCE(?, scope), memory_session_id = COALESCE(?, memory_session_id)
|
|
634
661
|
WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL
|
|
635
662
|
`,
|
|
@@ -641,7 +668,7 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
641
668
|
safe.concepts,
|
|
642
669
|
safe.facts,
|
|
643
670
|
safe.text,
|
|
644
|
-
|
|
671
|
+
modelImportance,
|
|
645
672
|
safe.lesson_learned,
|
|
646
673
|
safe.search_aliases,
|
|
647
674
|
minhashSig,
|
|
@@ -1300,6 +1327,8 @@ Return ONLY valid JSON:
|
|
|
1300
1327
|
const bigramText = cjkBigrams((title || '') + ' ' + (narrative || ''));
|
|
1301
1328
|
const textField = [conceptsText, factsText, bigramText].filter(Boolean).join(' ');
|
|
1302
1329
|
const minhashSig = computeMinHash((title || '') + ' ' + (narrative || ''));
|
|
1330
|
+
// An importance a person set on the keeper stays (D10); the UPDATE decides that on the row as
|
|
1331
|
+
// it is then, since a person can set it while this call waits on the model (D#198).
|
|
1303
1332
|
const importance = Math.max(clampImportance(parsed.importance || 2), maxClusterImportance);
|
|
1304
1333
|
|
|
1305
1334
|
// Scrub LLM-output cluster-merge text fields at the UPDATE boundary.
|
|
@@ -1382,7 +1411,8 @@ Return ONLY valid JSON:
|
|
|
1382
1411
|
db.prepare(
|
|
1383
1412
|
`
|
|
1384
1413
|
UPDATE observations SET title=?, narrative=?, concepts=?, facts=?, text=?,
|
|
1385
|
-
importance
|
|
1414
|
+
importance = CASE WHEN importance_set_at IS NOT NULL THEN importance ELSE ? END,
|
|
1415
|
+
lesson_learned=?, minhash_sig=?, optimized_at=?,
|
|
1386
1416
|
memory_session_id = COALESCE(?, memory_session_id)
|
|
1387
1417
|
WHERE id = ?
|
|
1388
1418
|
`,
|
package/hook-shared.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './lib/schema-skew.mjs';
|
|
31
31
|
import { isDbUnusableError, DB_UNUSABLE_MARKER_PREFIX } from './lib/db-unusable.mjs';
|
|
32
32
|
import { shouldRecordOnce } from './lib/record-once.mjs';
|
|
33
|
+
import { backgroundWorkerEnv, workerDirGone } from './lib/worker-data-dir.mjs';
|
|
33
34
|
import { hookSessionId } from './lib/provenance.mjs';
|
|
34
35
|
import { PROJECT_REKEY_MARKER_PREFIX } from './lib/project-rekey.mjs';
|
|
35
36
|
// Audit 2026-09-05 P1-2 (carried from 2026-09-02 P2-9): `callLLM`, the quiet/adoption
|
|
@@ -280,7 +281,8 @@ export const GC_PRESERVED_MARKER_PREFIXES = Object.freeze([
|
|
|
280
281
|
// out. Both are version-keyed one-shot migration sentinels written by
|
|
281
282
|
// scripts/setup.sh, and their gate is `! -f <marker>` — deleting one re-runs
|
|
282
283
|
// its migration. `.mcp-dedup-v2.78` gates a block that removes
|
|
283
|
-
// mcpServers.mem / mcpServers["mem-lite"] from the
|
|
284
|
+
// mcpServers.mem / mcpServers["mem-lite"] from the config home's .claude.json (one marker per
|
|
285
|
+
// .claude.json since D#270, the same prefix) with
|
|
284
286
|
// a raw writeFileSync (no tmp+rename, no backup), which the repo's own test
|
|
285
287
|
// documents as intentionally one-shot: "If a user later runs `claude mcp add
|
|
286
288
|
// mem ...` themselves, the gate intentionally lets it stand." A 30-day sweep
|
|
@@ -378,9 +380,11 @@ export function sweepStaleProjectMarkers(
|
|
|
378
380
|
// exists, so harden here too: create 0700, and chmod a pre-existing dir a prior version
|
|
379
381
|
// created at the default umask. A 0700 dir blocks traversal to every file inside,
|
|
380
382
|
// current and future, regardless of individual file mode (audit sec P3-2 2026-07-24).
|
|
383
|
+
// A background worker whose data dir was removed while it ran does not re-create it (D#265).
|
|
381
384
|
try {
|
|
382
|
-
if (!existsSync(RUNTIME_DIR))
|
|
383
|
-
|
|
385
|
+
if (!existsSync(RUNTIME_DIR)) {
|
|
386
|
+
if (!workerDirGone(DB_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true, mode: 0o700 });
|
|
387
|
+
} else chmodSync(RUNTIME_DIR, 0o700);
|
|
384
388
|
} catch {}
|
|
385
389
|
|
|
386
390
|
// ─── Session ID Management ───────────────────────────────────────────────────
|
|
@@ -604,7 +608,7 @@ export function spawnBackground(bgEvent, ...extraArgs) {
|
|
|
604
608
|
const child = spawn(process.execPath, args, {
|
|
605
609
|
detached: true,
|
|
606
610
|
stdio: 'ignore',
|
|
607
|
-
env:
|
|
611
|
+
env: backgroundWorkerEnv(),
|
|
608
612
|
});
|
|
609
613
|
child.on('error', (err) => {
|
|
610
614
|
debugCatch(err, 'spawnBackground');
|