claude-mem-lite 6.29.0 → 6.29.2
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/commands/adopt.md +3 -3
- package/commands/bug.md +1 -2
- package/commands/lesson.md +3 -4
- package/commands/mem.md +3 -3
- package/commands/memory.md +3 -3
- package/commands/update.md +12 -9
- package/hook-llm.mjs +9 -3
- package/hook.mjs +14 -1
- package/npm-shrinkwrap.json +10 -10
- package/package.json +6 -6
- package/schema.mjs +79 -9
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.29.
|
|
12
|
+
"version": "6.29.2",
|
|
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.29.
|
|
3
|
+
"version": "6.29.2",
|
|
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/commands/adopt.md
CHANGED
|
@@ -7,9 +7,9 @@ description: "Use when: user asks to put the claude-mem-lite steering block into
|
|
|
7
7
|
|
|
8
8
|
Install the claude-mem-lite **steering block** into the current project's
|
|
9
9
|
`CLAUDE.md` (the canonical home for project instructions — loaded by Claude Code
|
|
10
|
-
at system-prompt authority).
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
at system-prompt authority). It does not go in the project's memory-dir
|
|
11
|
+
`MEMORY.md`: that index belongs to the user's own memories, and it carries no
|
|
12
|
+
more weight than `CLAUDE.md`.
|
|
13
13
|
|
|
14
14
|
Auto-adopt does not add the block to `CLAUDE.md`: in a git repository it keeps the same block
|
|
15
15
|
in `CLAUDE.local.md` at the repository root, excluded from git through `.git/info/exclude`.
|
package/commands/bug.md
CHANGED
|
@@ -10,8 +10,7 @@ Record a known bug + reproduction steps. Writes a searchable **observation**
|
|
|
10
10
|
via `mem_search` and the PreToolUse recall hooks warn when the affected files
|
|
11
11
|
are edited. Does NOT touch memdir. Useful for bugs you can't fix immediately
|
|
12
12
|
but want future sessions (or yourself after `/clear`) to know about and avoid
|
|
13
|
-
re-investigating.
|
|
14
|
-
never read.)
|
|
13
|
+
re-investigating.
|
|
15
14
|
|
|
16
15
|
## When to use
|
|
17
16
|
|
package/commands/lesson.md
CHANGED
|
@@ -9,9 +9,8 @@ Record a lesson / gotcha / workaround. Writes a searchable **observation**
|
|
|
9
9
|
(`type=discovery` with the text as `lesson_learned`) so future sessions can
|
|
10
10
|
find it via `mem_search` and the PreToolUse recall hooks surface it on the
|
|
11
11
|
relevant files. Does NOT touch memdir — so lessons never end up in the L1
|
|
12
|
-
system-prompt memory section and do not conflict with
|
|
13
|
-
|
|
14
|
-
`mem_search` never read.)
|
|
12
|
+
system-prompt memory section and do not conflict with Claude Code's auto-memory
|
|
13
|
+
rules on what not to save there.
|
|
15
14
|
|
|
16
15
|
## When to use
|
|
17
16
|
|
|
@@ -23,7 +22,7 @@ After you (or the user) solve a tricky bug or discover a non-obvious behavior:
|
|
|
23
22
|
|
|
24
23
|
Don't use for trivial fixes (typos, renames), for rules that belong in memdir
|
|
25
24
|
(user preferences, project-wide conventions), or for general project notes
|
|
26
|
-
(use `/mem:memory` or `mem_save` instead).
|
|
25
|
+
(use `/claude-mem-lite:memory` or `mem_save` instead).
|
|
27
26
|
|
|
28
27
|
## Arguments
|
|
29
28
|
|
package/commands/mem.md
CHANGED
|
@@ -30,9 +30,9 @@ When the user invokes `/mem`, parse their intent:
|
|
|
30
30
|
- `/mem save <text>` → call `mem_save` MCP tool with the text as content
|
|
31
31
|
- `/mem stats` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" stats` via Bash
|
|
32
32
|
- `/mem get <ids>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <ids>` via Bash
|
|
33
|
-
- `/mem cleanup` → run `
|
|
34
|
-
- `/mem cleanup Nd` (e.g. `60d`) → same as above but add
|
|
35
|
-
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with
|
|
33
|
+
- `/mem cleanup` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain scan` via Bash, report pending purge count and stale items to user, ask for confirmation, then run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops purge_stale --confirm` via Bash if confirmed. **`--confirm` is required and is not optional politeness:** without it the command prints a dry-run PREVIEW and deletes nothing, while still succeeding — so you would report a cleanup that never happened.
|
|
34
|
+
- `/mem cleanup Nd` (e.g. `60d`) → same as above but add `--retain-days N` to only purge items older than N days. **`--retain-days` must be between 7 and 365**; the CLI rejects anything outside that range, so `/mem cleanup 3d` cannot be honoured — say so rather than silently substituting the default.
|
|
35
|
+
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `--retain-days N`, same 7–365 range.
|
|
36
36
|
- `/mem <query>` (no subcommand) → treat as search, run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" search <query>` via Bash
|
|
37
37
|
|
|
38
38
|
Use Bash commands first. For detailed data, use `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <id>` via Bash.
|
package/commands/memory.md
CHANGED
|
@@ -9,12 +9,12 @@ Save important content to your long-term memory database.
|
|
|
9
9
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
|
-
- `/mem:memory <content>` — Save the given content directly to memory
|
|
13
|
-
- `/mem:memory` (no args) — Auto-summarize recent session highlights and save key findings
|
|
12
|
+
- `/claude-mem-lite:memory <content>` — Save the given content directly to memory
|
|
13
|
+
- `/claude-mem-lite:memory` (no args) — Auto-summarize recent session highlights and save key findings
|
|
14
14
|
|
|
15
15
|
## Instructions
|
|
16
16
|
|
|
17
|
-
When the user invokes `/mem:memory`, determine the intent:
|
|
17
|
+
When the user invokes `/claude-mem-lite:memory`, determine the intent:
|
|
18
18
|
|
|
19
19
|
### With explicit content
|
|
20
20
|
|
package/commands/update.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: update
|
|
3
3
|
description: "Use when: search results seem noisy or for periodic memory maintenance"
|
|
4
|
+
disable-model-invocation: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Memory Maintenance
|
|
@@ -9,23 +10,25 @@ Run intelligent maintenance on the memory database.
|
|
|
9
10
|
|
|
10
11
|
## Usage
|
|
11
12
|
|
|
12
|
-
- `/mem:update` — Run full maintenance cycle
|
|
13
|
+
- `/claude-mem-lite:update` — Run full maintenance cycle
|
|
13
14
|
|
|
14
15
|
## Instructions
|
|
15
16
|
|
|
16
|
-
When the user invokes `/mem:update`, perform the following maintenance cycle:
|
|
17
|
+
When the user invokes `/claude-mem-lite:update`, perform the following maintenance cycle:
|
|
17
18
|
|
|
18
19
|
### Phase 1: Memory Maintenance
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Run each command below via Bash; the CLI reaches the maintenance operations whether or not their MCP tools are in your tool list.
|
|
22
|
+
|
|
23
|
+
1. Run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain scan` to analyze maintenance candidates
|
|
21
24
|
2. Report scan results to the user (duplicates, stale items, broken items, boostable items, **pending purge** items)
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
25
|
+
3. **If pending purge items > 0**: first run the purge command below without `--confirm`: it prints how many rows it would delete and deletes nothing. Report that count to the user and ask for confirmation. If confirmed, run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops purge_stale --confirm`. **`--confirm` is required:** without it the command prints a dry-run preview and deletes nothing while still succeeding, so you would report a purge that never happened. User may optionally add `--retain-days N` (default 30, allowed range 7–365) to both runs to control how many days of data to keep; the CLI rejects a value outside that range, so tell the user instead of substituting the default. Do NOT purge without explicit user confirmation. The purge comes before the next step on purpose: decay there queues more rows for purge, and those wait for a later run, so what gets deleted is what the user approved.
|
|
26
|
+
4. Run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute` to apply safe automatic changes — with no `--ops` it runs the default set: cleanup, decay, boost, then demote_pinned (left out when `CLAUDE_MEM_SKIP_DEMOTE_PINNED=1`)
|
|
27
|
+
5. If duplicates were found in scan, review them and run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops dedup --merge-ids keepId:removeId,...` — keep the more important/recent observation in each pair. Groups are comma-separated and the first id in each is kept (`10:11,20:21:22`). For pairs similar enough to merge without review the scan prints a ready-made list after `--merge-ids`; pass it to the command above, since the command the scan prints uses a bare `claude-mem-lite`, which may not be installed
|
|
28
|
+
6. Run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" compress --execute` for old low-value observations (without `--execute` it only previews)
|
|
26
29
|
|
|
27
30
|
### Phase 2: Summary
|
|
28
31
|
|
|
29
|
-
Summarize all maintenance actions taken in
|
|
30
|
-
- Memory: observations cleaned, decayed, boosted, deduplicated, compressed
|
|
32
|
+
Summarize all maintenance actions taken in the user's language:
|
|
33
|
+
- Memory: observations cleaned, decayed, boosted, demoted, deduplicated, compressed
|
|
31
34
|
- Overall health assessment
|
package/hook-llm.mjs
CHANGED
|
@@ -1429,8 +1429,8 @@ export async function handleLLMEpisode() {
|
|
|
1429
1429
|
// returns 3 for any entry touching schema./migration/prisma/.env/.key paths; via the
|
|
1430
1430
|
// Math.max below that force-promoted ordinary has-lesson episodes to "critical" imp=3
|
|
1431
1431
|
// even when Haiku judged them 1-2 (audit: auto imp=3 = 34.8%, e.g. a thin-lesson edit
|
|
1432
|
-
// to schema.mjs).
|
|
1433
|
-
//
|
|
1432
|
+
// to schema.mjs). Only the path heuristic was capped then; D#414 below caps the model's own
|
|
1433
|
+
// score too. The isLessonLowSignal branch still floors no-lesson
|
|
1434
1434
|
// non-decision autos at ≤1; manual mem_save uses a different path and is unaffected.
|
|
1435
1435
|
// D#69: `!lessonLearned` is the old `isLessonLowSignal && !retryRecovered` (the
|
|
1436
1436
|
// two are equal with grounding off). A lesson the grounding check dropped caps
|
|
@@ -1440,10 +1440,16 @@ export async function handleLLMEpisode() {
|
|
|
1440
1440
|
// carries text whoever controls that output wrote — reproduced on real Haiku,
|
|
1441
1441
|
// 3 of 6 hostile windows stored the directive at importance 2. The row and
|
|
1442
1442
|
// lesson stay searchable; only the automatic injection faces lose them.
|
|
1443
|
+
// D#414: the model's own score is capped at 2 as well, as re-enrich's is (D#376). The window
|
|
1444
|
+
// carries tool output, text whoever controls a file or a command's output wrote, and 3 is
|
|
1445
|
+
// Key Context's top tier; benign episodes did not use it: 72 real sub-episodes from this
|
|
1446
|
+
// machine's transcripts through this prompt (claude -p haiku, 2026-10-17) scored 1 x42,
|
|
1447
|
+
// 2 x30, 3 x0. (A rule-based row, buildImmediateObservation's, saved before the call or when
|
|
1448
|
+
// it fails, can still be 3 after a failing test or build command: that is no model output.)
|
|
1443
1449
|
importance:
|
|
1444
1450
|
(!lessonLearned && (groundingDropped || parsed.type !== 'decision')) || quotesToolOutput
|
|
1445
1451
|
? Math.min(ruleImportance, 1)
|
|
1446
|
-
: Math.
|
|
1452
|
+
: Math.min(Math.max(ruleImportance, clampImportance(parsed.importance)), 2),
|
|
1447
1453
|
lessonLearned,
|
|
1448
1454
|
searchAliases,
|
|
1449
1455
|
// P3 (D#78): lesson applicability scope — whitelist-validated, invalid → null.
|
package/hook.mjs
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
} from './hook-episode.mjs';
|
|
79
79
|
// CODE_DIR, not DB_DIR: the schema-skew notice asks which CODE homes exist, and those are
|
|
80
80
|
// always homedir-rooted even when CLAUDE_MEM_DIR relocates the data.
|
|
81
|
-
import { DB_DIR, DB_PATH, CODE_DIR, refreshPlannerStats } from './schema.mjs';
|
|
81
|
+
import { DB_DIR, DB_PATH, CODE_DIR, refreshPlannerStats, recheckObservationIndexTerms } from './schema.mjs';
|
|
82
82
|
import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
|
|
83
83
|
import { entry as preCompactEntry } from './hook-precompact.mjs';
|
|
84
84
|
import {
|
|
@@ -2335,6 +2335,19 @@ function runSessionStartAutoMaintain(db, project) {
|
|
|
2335
2335
|
debugCatch(e, 'auto-maintain-handoff-gc');
|
|
2336
2336
|
}
|
|
2337
2337
|
|
|
2338
|
+
// The CJK index terms an older build still writing this store left out (D#413).
|
|
2339
|
+
try {
|
|
2340
|
+
const { appended } = recheckObservationIndexTerms(db);
|
|
2341
|
+
if (appended > 0)
|
|
2342
|
+
debugLog(
|
|
2343
|
+
'DEBUG',
|
|
2344
|
+
'auto-maintain',
|
|
2345
|
+
`appended index terms to ${appended} observations an older build wrote`,
|
|
2346
|
+
);
|
|
2347
|
+
} catch (e) {
|
|
2348
|
+
debugCatch(e, 'auto-maintain-recheck-index-terms');
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2338
2351
|
// Planner statistics, the "once a day" half of SQLite's recommendation (#41). Last, so
|
|
2339
2352
|
// the purge/dedup above have already moved the row counts it compares. Never throws.
|
|
2340
2353
|
refreshPlannerStats(db);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.29.
|
|
3
|
+
"version": "6.29.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.29.
|
|
9
|
+
"version": "6.29.2",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
17
17
|
"better-sqlite3": "^13.0.3",
|
|
18
|
-
"zod": "^4.6.
|
|
18
|
+
"zod": "^4.6.7"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"claude-mem-lite": "cli.mjs"
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@eslint/js": "^10.0.1",
|
|
25
25
|
"@vitest/coverage-v8": "^5.0.3",
|
|
26
|
-
"acorn": "8.
|
|
27
|
-
"eslint": "^10.
|
|
26
|
+
"acorn": "8.19.0",
|
|
27
|
+
"eslint": "^10.12.0",
|
|
28
28
|
"fast-check": "^4.10.2",
|
|
29
|
-
"knip": "^6.
|
|
29
|
+
"knip": "^6.41.0",
|
|
30
30
|
"picomatch": "^4.0.4",
|
|
31
|
-
"prettier": "3.9.
|
|
31
|
+
"prettier": "3.9.10",
|
|
32
32
|
"vitest": "^5.0.3"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
@@ -1197,9 +1197,9 @@
|
|
|
1197
1197
|
"license": "ISC"
|
|
1198
1198
|
},
|
|
1199
1199
|
"node_modules/zod": {
|
|
1200
|
-
"version": "4.6.
|
|
1201
|
-
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.
|
|
1202
|
-
"integrity": "sha512-
|
|
1200
|
+
"version": "4.6.7",
|
|
1201
|
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
|
|
1202
|
+
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
|
|
1203
1203
|
"license": "MIT",
|
|
1204
1204
|
"funding": {
|
|
1205
1205
|
"url": "https://github.com/sponsors/colinhacks"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.29.
|
|
3
|
+
"version": "6.29.2",
|
|
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
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"dependencies": {
|
|
238
238
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
239
239
|
"better-sqlite3": "^13.0.3",
|
|
240
|
-
"zod": "^4.6.
|
|
240
|
+
"zod": "^4.6.7"
|
|
241
241
|
},
|
|
242
242
|
"overrides": {
|
|
243
243
|
"hono": ">=4.12.31",
|
|
@@ -250,12 +250,12 @@
|
|
|
250
250
|
"devDependencies": {
|
|
251
251
|
"@eslint/js": "^10.0.1",
|
|
252
252
|
"@vitest/coverage-v8": "^5.0.3",
|
|
253
|
-
"acorn": "8.
|
|
254
|
-
"eslint": "^10.
|
|
253
|
+
"acorn": "8.19.0",
|
|
254
|
+
"eslint": "^10.12.0",
|
|
255
255
|
"fast-check": "^4.10.2",
|
|
256
|
-
"knip": "^6.
|
|
256
|
+
"knip": "^6.41.0",
|
|
257
257
|
"picomatch": "^4.0.4",
|
|
258
|
-
"prettier": "3.9.
|
|
258
|
+
"prettier": "3.9.10",
|
|
259
259
|
"vitest": "^5.0.3"
|
|
260
260
|
}
|
|
261
261
|
}
|
package/schema.mjs
CHANGED
|
@@ -1303,12 +1303,13 @@ const OBS_DOC_SQL = OBS_FTS_COLUMNS.map((c) => `COALESCE(${c}, '')`).join(` || '
|
|
|
1303
1303
|
* appended to and whether the walk is finished.
|
|
1304
1304
|
*
|
|
1305
1305
|
* @param {import('better-sqlite3').Database} db
|
|
1306
|
-
* @param {{ budgetMs?: number, batchSize?: number }} [opts]
|
|
1306
|
+
* @param {{ budgetMs?: number, batchSize?: number, cursor?: string }} [opts] `cursor`: the name of
|
|
1307
|
+
* the walk's row in migration_cursors (recheckObservationIndexTerms walks on its own)
|
|
1307
1308
|
* @returns {{ appended: number, done: boolean }}
|
|
1308
1309
|
*/
|
|
1309
1310
|
export function backfillObservationIndexTerms(
|
|
1310
1311
|
db,
|
|
1311
|
-
{ budgetMs = OBS_TERMS_BUDGET_MS, batchSize = OBS_TERMS_BATCH } = {},
|
|
1312
|
+
{ budgetMs = OBS_TERMS_BUDGET_MS, batchSize = OBS_TERMS_BATCH, cursor = OBS_TERMS_CURSOR } = {},
|
|
1312
1313
|
) {
|
|
1313
1314
|
const start = performance.now();
|
|
1314
1315
|
const prior = db.pragma('busy_timeout', { simple: true });
|
|
@@ -1318,8 +1319,7 @@ export function backfillObservationIndexTerms(
|
|
|
1318
1319
|
const table = db
|
|
1319
1320
|
.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'migration_cursors'`)
|
|
1320
1321
|
.get();
|
|
1321
|
-
const row =
|
|
1322
|
-
table && db.prepare(`SELECT below_id FROM migration_cursors WHERE name = ?`).get(OBS_TERMS_CURSOR);
|
|
1322
|
+
const row = table && db.prepare(`SELECT below_id FROM migration_cursors WHERE name = ?`).get(cursor);
|
|
1323
1323
|
return row ? row.below_id : Number.MAX_SAFE_INTEGER;
|
|
1324
1324
|
};
|
|
1325
1325
|
const pick = db.prepare(
|
|
@@ -1356,7 +1356,7 @@ export function backfillObservationIndexTerms(
|
|
|
1356
1356
|
db.prepare(
|
|
1357
1357
|
`INSERT INTO migration_cursors (name, below_id) VALUES (?, ?)
|
|
1358
1358
|
ON CONFLICT(name) DO UPDATE SET below_id = excluded.below_id`,
|
|
1359
|
-
).run(
|
|
1359
|
+
).run(cursor, below);
|
|
1360
1360
|
return n;
|
|
1361
1361
|
});
|
|
1362
1362
|
let appended = 0;
|
|
@@ -1385,6 +1385,54 @@ export function backfillObservationIndexTerms(
|
|
|
1385
1385
|
}
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
|
+
/** How long recheckObservationIndexTerms keeps walking after its first run (D#413). */
|
|
1389
|
+
const OBS_TERMS_RECHECK_WINDOW_MS = 7 * 24 * 3600 * 1000;
|
|
1390
|
+
/** Its budget per run: the maintain worker has no host deadline, and its lock goes stale at 10 min. */
|
|
1391
|
+
const OBS_TERMS_RECHECK_BUDGET_MS = 30_000;
|
|
1392
|
+
/** Its cursor and done marker; the window's start is stored as `<name>-opened`. */
|
|
1393
|
+
const OBS_TERMS_RECHECK = 'recheck-cjk-index-terms';
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Walk the observations again for the CJK index terms an older build left out (D#413), once a day
|
|
1397
|
+
* from the auto-maintain worker, for OBS_TERMS_RECHECK_WINDOW_MS from this function's first run on
|
|
1398
|
+
* the store, then mark itself done. The open-time pass is marked done once, but an older build can
|
|
1399
|
+
* go on writing the store for as long as a session started before the update lives: it inserts
|
|
1400
|
+
* rows without these terms (live store 2026-10-17: 8 of the 36 rows its 6.27.0 code wrote after
|
|
1401
|
+
* the fill) and rewrites a filled row's text without them (6.28.x mem_update), and no column says
|
|
1402
|
+
* which rows a writer touched. The window starts at the first run, not at the open-time pass's
|
|
1403
|
+
* marker, so a store whose pass finished long before this build arrived still gets a whole walk.
|
|
1404
|
+
* Waits for the open-time pass to be done, so the two never walk at once. The walk is
|
|
1405
|
+
* backfillObservationIndexTerms on a cursor of its own: a finished walk drops the cursor, so the
|
|
1406
|
+
* next run starts from the top again; one cut short by its budget or another writer's lock keeps
|
|
1407
|
+
* it, and the next run continues below it. Returns how many rows it appended to and whether it
|
|
1408
|
+
* walked.
|
|
1409
|
+
*
|
|
1410
|
+
* @param {import('better-sqlite3').Database} db
|
|
1411
|
+
* @param {{ now?: number, budgetMs?: number }} [opts]
|
|
1412
|
+
* @returns {{ appended: number, ran: boolean }}
|
|
1413
|
+
*/
|
|
1414
|
+
export function recheckObservationIndexTerms(
|
|
1415
|
+
db,
|
|
1416
|
+
{ now = Date.now(), budgetMs = OBS_TERMS_RECHECK_BUDGET_MS } = {},
|
|
1417
|
+
) {
|
|
1418
|
+
const doneAt = db.prepare(`SELECT done_at_epoch FROM migration_cleanups WHERE name = ?`).pluck();
|
|
1419
|
+
const mark = db.prepare(`INSERT OR IGNORE INTO migration_cleanups (name, done_at_epoch) VALUES (?, ?)`);
|
|
1420
|
+
// 'backfill-cjk-index-terms' is the open-time pass's marker (DEFERRED_CLEANUPS).
|
|
1421
|
+
if (doneAt.get('backfill-cjk-index-terms') === undefined || doneAt.get(OBS_TERMS_RECHECK) !== undefined)
|
|
1422
|
+
return { appended: 0, ran: false };
|
|
1423
|
+
mark.run(`${OBS_TERMS_RECHECK}-opened`, now);
|
|
1424
|
+
if (now - doneAt.get(`${OBS_TERMS_RECHECK}-opened`) >= OBS_TERMS_RECHECK_WINDOW_MS) {
|
|
1425
|
+
mark.run(OBS_TERMS_RECHECK, now);
|
|
1426
|
+
return { appended: 0, ran: false };
|
|
1427
|
+
}
|
|
1428
|
+
const { appended, done } = backfillObservationIndexTerms(db, { budgetMs, cursor: OBS_TERMS_RECHECK });
|
|
1429
|
+
const cursors = db
|
|
1430
|
+
.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'migration_cursors'`)
|
|
1431
|
+
.get();
|
|
1432
|
+
if (done && cursors) db.prepare(`DELETE FROM migration_cursors WHERE name = ?`).run(OBS_TERMS_RECHECK);
|
|
1433
|
+
return { appended, ran: true };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1388
1436
|
/**
|
|
1389
1437
|
* How long backfillEventSearchTerms runs per open, checked after each row it segments (its reads,
|
|
1390
1438
|
* segmentation and the writes before; the last batch's write comes on top):
|
|
@@ -1737,9 +1785,11 @@ export function refreshPlannerStats(db, { busyTimeoutMs } = {}) {
|
|
|
1737
1785
|
|
|
1738
1786
|
/**
|
|
1739
1787
|
* Whether an open/init error carries a genuine corruption signature. WAL-delete
|
|
1740
|
-
* recovery
|
|
1788
|
+
* recovery runs ONLY for these: on a transient error (SQLITE_BUSY) or the
|
|
1741
1789
|
* forward-version guard throw, deleting the WAL would discard committed-but-
|
|
1742
|
-
* uncheckpointed transactions — silent data loss.
|
|
1790
|
+
* uncheckpointed transactions — silent data loss. A corruption signature does not
|
|
1791
|
+
* make the delete safe either while another connection has the file open (it loses
|
|
1792
|
+
* that connection's commits the same way), so ensureDbWithWalRecovery retries first.
|
|
1743
1793
|
*/
|
|
1744
1794
|
export function isDbCorruptionError(err) {
|
|
1745
1795
|
// R10 P3-9: SQLITE_CORRUPT_VTAB is EXCLUDED. It means a damaged FTS5 index over an
|
|
@@ -1764,7 +1814,8 @@ export { isFtsCorruptionError };
|
|
|
1764
1814
|
* serves all three openers.
|
|
1765
1815
|
*
|
|
1766
1816
|
* Non-corruption failure → rethrows the original error, WAL/SHM left intact.
|
|
1767
|
-
* Corruption →
|
|
1817
|
+
* Corruption → retries once with the files untouched; corruption again → deletes
|
|
1818
|
+
* -wal/-shm, retries once more; a still-failing retry rethrows
|
|
1768
1819
|
* with `err.walRecoveryAttempted = true` so callers can word their fatal hint
|
|
1769
1820
|
* accurately (recovery already tried vs WAL deliberately preserved).
|
|
1770
1821
|
*
|
|
@@ -1783,6 +1834,9 @@ export function ensureDbWithWalRecovery({ warn, info } = {}) {
|
|
|
1783
1834
|
let raw = null;
|
|
1784
1835
|
try {
|
|
1785
1836
|
raw = new Database(DB_PATH);
|
|
1837
|
+
// D#415: otherwise better-sqlite3's 5000 ms per statement, four rebuilds, in a hook
|
|
1838
|
+
// the host kills at 2 s or 5 s.
|
|
1839
|
+
if (busyDeadline !== null) capBusyWaits(raw);
|
|
1786
1840
|
const { errors } = rebuildFTS(raw);
|
|
1787
1841
|
if (errors.length) warn?.(`FTS rebuild reported: ${errors.join('; ')}`);
|
|
1788
1842
|
} catch (rebuildErr) {
|
|
@@ -1808,7 +1862,23 @@ export function ensureDbWithWalRecovery({ warn, info } = {}) {
|
|
|
1808
1862
|
}
|
|
1809
1863
|
}
|
|
1810
1864
|
if (!isDbCorruptionError(firstErr)) throw firstErr;
|
|
1811
|
-
|
|
1865
|
+
// D#427: retry once with the files untouched before deleting anything. A corruption error
|
|
1866
|
+
// that does not come from the file exists (3 of 43 first opens by 6.28.1 of fresh copies of one
|
|
1867
|
+
// intact store threw one on one host, D#424; the other 40 did not), and for it the delete below
|
|
1868
|
+
// is the only thing that loses data: with another connection open (the MCP server keeps one all
|
|
1869
|
+
// session) it can discard what that connection committed since its last checkpoint, and until
|
|
1870
|
+
// it closes the two connections overwrite each other's pages.
|
|
1871
|
+
warn?.(`DB corruption reported, retrying the open with the WAL kept: ${firstErr.message}`);
|
|
1872
|
+
let walErr;
|
|
1873
|
+
try {
|
|
1874
|
+
const db = ensureDb();
|
|
1875
|
+
info?.('DB opened on the retry, WAL kept');
|
|
1876
|
+
return db;
|
|
1877
|
+
} catch (retryErr) {
|
|
1878
|
+
if (!isDbCorruptionError(retryErr)) throw retryErr;
|
|
1879
|
+
walErr = retryErr;
|
|
1880
|
+
}
|
|
1881
|
+
warn?.(`DB corruption detected, attempting WAL recovery: ${walErr.message}`);
|
|
1812
1882
|
try {
|
|
1813
1883
|
rmSync(DB_PATH + '-wal', { force: true });
|
|
1814
1884
|
} catch {
|