claude-mem-lite 6.7.2 → 6.8.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 +25 -0
- package/README.zh-CN.md +18 -0
- package/install.mjs +58 -12
- package/lib/import-jsonl.mjs +37 -13
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schema.mjs +56 -36
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.8.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. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/README.md
CHANGED
|
@@ -236,6 +236,31 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
|
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
<!-- normalize-per-project-note:start -->
|
|
239
|
+
## Upgrading to 6.8.0
|
|
240
|
+
|
|
241
|
+
**Two things change on upgrade. Neither needs an action from you, and neither is a schema
|
|
242
|
+
change — an older build can still open the database.**
|
|
243
|
+
|
|
244
|
+
*The first open backfills the file-lookup table, once.* Observations imported from a
|
|
245
|
+
transcript before 6.7.2 carry their modified-files list but no row in the junction table the
|
|
246
|
+
file-recall paths join, so asking about a file never found them. The repair was gated on that
|
|
247
|
+
table being completely empty, which one ordinary `mem_save` falsifies forever. It now runs
|
|
248
|
+
once per database, keyed on its own marker, and retries on a later open if it fails. A store
|
|
249
|
+
that never ran `import-jsonl` matches no rows and pays nothing.
|
|
250
|
+
|
|
251
|
+
Those rows become reachable through `recall` / `mem_recall` and the prompt-submit path. They
|
|
252
|
+
do **not** become reachable through the pre-tool recall hook, which admits only
|
|
253
|
+
`importance >= 2` and every imported row carries `1`.
|
|
254
|
+
|
|
255
|
+
*`doctor --json` changes shape.* Checks that print a repair command now carry it in a
|
|
256
|
+
`details` array — previously the human screen got the command and the JSON got only the
|
|
257
|
+
diagnosis. And four checks (dev drift, managed files, and both hook-script branches) now
|
|
258
|
+
report `"level": "fail"` where they used to report `"warn"`, with a new `"glyph": "warn"`
|
|
259
|
+
recording that they still render as ⚠ rather than ✗. They always counted toward the issue
|
|
260
|
+
total and the exit code; the level now says so. If you filter on `level === "fail"` you will
|
|
261
|
+
see four findings you were missing. The `issues` count, the summary line and the exit code
|
|
262
|
+
are unchanged.
|
|
263
|
+
|
|
239
264
|
## Upgrading to 6.1.0
|
|
240
265
|
|
|
241
266
|
**One default changes, and only for the daily background pass.** Until 6.1.0 the unattended
|
package/README.zh-CN.md
CHANGED
|
@@ -199,6 +199,24 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
<!-- normalize-per-project-note:start -->
|
|
202
|
+
## 升级到 6.8.0
|
|
203
|
+
|
|
204
|
+
**升级后有两处变化,都不需要你做什么,也都不是 schema 变更——旧版本仍能打开这个数据库。**
|
|
205
|
+
|
|
206
|
+
*第一次开库会做一次性回填。* 6.7.2 之前从 transcript 导入的观察,带着「修改过的文件」列表,
|
|
207
|
+
却没有在文件召回所 JOIN 的联结表里留下行,所以按文件去问永远找不到它们。原来的修复门是
|
|
208
|
+
「这张表完全为空」——任何一次正常的 `mem_save` 都会让它永久为假。现在它按自己的标记每个库跑
|
|
209
|
+
一次,失败则下次开库重试。从没跑过 `import-jsonl` 的库匹配不到任何行,代价为零。
|
|
210
|
+
|
|
211
|
+
这些行会经 `recall` / `mem_recall` 和 UserPromptSubmit 这条路变得可达;**不会**经 pre-tool
|
|
212
|
+
召回钩子变得可达——那条腿只收 `importance >= 2`,而导入的行一律是 `1`。
|
|
213
|
+
|
|
214
|
+
*`doctor --json` 的输出形状变了。* 带修复命令的检查项现在把命令放在 `details` 数组里——此前
|
|
215
|
+
人类界面拿到命令、JSON 界面只拿到诊断。另外四个检查项(dev drift、managed files、hook 脚本的
|
|
216
|
+
两条)现在报 `"level": "fail"`,此前报 `"warn"`,并新增 `"glyph": "warn"` 记录它们在屏幕上
|
|
217
|
+
仍渲染为 ⚠ 而非 ✗。它们一直都计入 issue 总数和退出码,现在 level 与之一致。如果你按
|
|
218
|
+
`level === "fail"` 过滤,会多看到四条此前漏掉的发现。`issues` 计数、摘要行和退出码不变。
|
|
219
|
+
|
|
202
220
|
## 升级到 6.1.0
|
|
203
221
|
|
|
204
222
|
**只有一个默认行为改变,且只影响每日后台任务。** 6.1.0 之前,无人值守的 `normalize` 会一次性
|
package/install.mjs
CHANGED
|
@@ -1715,8 +1715,26 @@ async function doctor() {
|
|
|
1715
1715
|
checks.push({ level: 'fail', message: msg });
|
|
1716
1716
|
if (!json) console.log(` ✗ ${msg}`);
|
|
1717
1717
|
};
|
|
1718
|
+
// Detail / remedy lines. These were `--json`'s blind spot: `log()` was a no-op under
|
|
1719
|
+
// --json and EVERY repair instruction doctor gives goes through it, so the machine face
|
|
1720
|
+
// received the diagnosis and none of the treatment (R12 audit P2-3). 7 of its 16 call
|
|
1721
|
+
// sites carry a runnable command; the other 9 are paths, notes and prose. They attach to
|
|
1722
|
+
// the check they follow, not to a report-level bucket, because the stated purpose of
|
|
1723
|
+
// --json is acting on an individual check.
|
|
1724
|
+
//
|
|
1725
|
+
// The count was first written here as "12 of 14" and both halves were wrong — pre-ship
|
|
1726
|
+
// review recounted. 16 is also the count at the previous release, so no edit of this
|
|
1727
|
+
// round moved it.
|
|
1718
1728
|
const log = (msg) => {
|
|
1719
|
-
if (!json)
|
|
1729
|
+
if (!json) {
|
|
1730
|
+
console.log(` ${msg}`);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const last = checks[checks.length - 1];
|
|
1734
|
+
// A detail before any check has nothing to attach to — same as today's drop, but the
|
|
1735
|
+
// human face would show it, so this is the one line the two faces cannot share.
|
|
1736
|
+
if (!last) return;
|
|
1737
|
+
(last.details ??= []).push(msg.trim());
|
|
1720
1738
|
};
|
|
1721
1739
|
|
|
1722
1740
|
let issues = 0;
|
|
@@ -1730,6 +1748,28 @@ async function doctor() {
|
|
|
1730
1748
|
warnings++;
|
|
1731
1749
|
warn(msg);
|
|
1732
1750
|
};
|
|
1751
|
+
// The fourth cell of the matrix, and the one that had no home: findings rendered at ⚠
|
|
1752
|
+
// severity that nevertheless REQUIRE action. Four checks used to spell it `warn(...)`
|
|
1753
|
+
// followed by `issues++`, which made `issues` count rows reporting `level:'warn'` — so
|
|
1754
|
+
// `checks.filter(c => c.level === 'fail')`, the exact use --json documents, under-reported
|
|
1755
|
+
// by four (R12 audit P2-4). Severity and glyph are separate facts: `level` is what the
|
|
1756
|
+
// counter and the exit code are derived from, `glyph` is how loud the screen is.
|
|
1757
|
+
//
|
|
1758
|
+
// The first draft of this comment justified the split by claiming that promoting these
|
|
1759
|
+
// four to `fail()` would break `doctor-missing-files-severity` and
|
|
1760
|
+
// `doctor-hook-script-manifest`. Pre-ship review measured it: rendering them as ✗ with
|
|
1761
|
+
// `level:'fail'` and no glyph leaves both files 13/13 GREEN, because each asserts
|
|
1762
|
+
// `level === 'warn' || level === 'fail'` — a disjunction over the only two values those
|
|
1763
|
+
// checks can carry, so it cannot say NO about a severity change in either direction.
|
|
1764
|
+
// The real reason is their stated INTENT (an unlinked-but-reachable module must not read
|
|
1765
|
+
// as an error), which nothing enforced until this round's `doctor-failure-branches` pin.
|
|
1766
|
+
// Self-counting like `dwarn`, so neither source scan in doctor-summary.test.mjs needs to
|
|
1767
|
+
// reach inside it; the end-to-end counter check in doctor-json-face-parity.test.mjs does.
|
|
1768
|
+
const issueWarn = (msg) => {
|
|
1769
|
+
issues++;
|
|
1770
|
+
checks.push({ level: 'fail', glyph: 'warn', message: msg });
|
|
1771
|
+
if (!json) console.log(` ⚠ ${msg}`);
|
|
1772
|
+
};
|
|
1733
1773
|
|
|
1734
1774
|
// Node version. The floor is read from package.json#engines rather than restated here —
|
|
1735
1775
|
// see requiredNodeMajor. It is PRINTED on the ok line too, so the guard test has something
|
|
@@ -1992,8 +2032,13 @@ async function doctor() {
|
|
|
1992
2032
|
`Disk footprint: DB ${mb(dbBytes)}MB, ${snaps.length} backup snapshot(s) ${mb(backupBytes)}MB (budget ${mb(backupBudgetBytes())}MB)`,
|
|
1993
2033
|
);
|
|
1994
2034
|
}
|
|
1995
|
-
} catch {
|
|
1996
|
-
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
// "Informational" was the reason this was silent, and silence is the one thing it must
|
|
2037
|
+
// not be: the import above is of a sibling module, so the run where it fails is a broken
|
|
2038
|
+
// install — doctor's entire audience. The line vanished with no trace, which reads
|
|
2039
|
+
// identically to a check that was never written (R12 audit P3-7). Every other "I could
|
|
2040
|
+
// not look" in this file has its own sentence; this is the same shape as the four.
|
|
2041
|
+
dwarn('Disk footprint: check failed — ' + e.message);
|
|
1997
2042
|
}
|
|
1998
2043
|
|
|
1999
2044
|
// Plugin/hook lifecycle state
|
|
@@ -2354,8 +2399,7 @@ async function doctor() {
|
|
|
2354
2399
|
`${r.missingModuleCount} missing module: ${nameList(r.missingModuleFiles, r.missingModuleCount)}`,
|
|
2355
2400
|
);
|
|
2356
2401
|
}
|
|
2357
|
-
|
|
2358
|
-
issues++;
|
|
2402
|
+
issueWarn(`Dev drift: ${parts.join('; ')} (${devRemedy})`);
|
|
2359
2403
|
} else if (r.missingModuleCount > 0) {
|
|
2360
2404
|
// Informational, NOT an issue: in a pure-symlink install every entry point resolves
|
|
2361
2405
|
// to the repo, and Node resolves each module's imports against that REALPATH — so an
|
|
@@ -2391,12 +2435,11 @@ async function doctor() {
|
|
|
2391
2435
|
// `claude-mem-lite update` is the observation editor (`update <id>`); the
|
|
2392
2436
|
// self-updater is `self-update`. Naming the wrong one sent the user to a
|
|
2393
2437
|
// usage error at the exact moment their install was incomplete.
|
|
2394
|
-
|
|
2438
|
+
issueWarn(
|
|
2395
2439
|
`Managed files: ${r.missingCount} missing (${parts.join('; ')}) — a copy install resolves ` +
|
|
2396
2440
|
`imports against the install dir, so these throw at hook time. Fix: claude-mem-lite self-update ` +
|
|
2397
2441
|
`(or: node ${join(INSTALL_DIR, 'cli.mjs')} repair)`,
|
|
2398
2442
|
);
|
|
2399
|
-
issues++;
|
|
2400
2443
|
}
|
|
2401
2444
|
// Complete copy install: no message — drift is a dev-install concern.
|
|
2402
2445
|
} catch (e) {
|
|
@@ -2423,12 +2466,11 @@ async function doctor() {
|
|
|
2423
2466
|
if (skipScripts) {
|
|
2424
2467
|
ok('Hook scripts: n/a (plugin-only install — hooks run from the plugin cache)');
|
|
2425
2468
|
} else if (!h.present) {
|
|
2426
|
-
|
|
2469
|
+
issueWarn(
|
|
2427
2470
|
`Hook scripts: ${join(INSTALL_DIR, 'scripts')} ` +
|
|
2428
2471
|
`${h.dirSymlink ? 'is a dangling symlink' : 'is absent'} — all ${HOOK_SCRIPT_ENTRY_POINTS.size} hook ` +
|
|
2429
2472
|
`commands name absolute paths under it, so no hook can fire. Fix: ${scriptRemedy}`,
|
|
2430
2473
|
);
|
|
2431
|
-
issues++;
|
|
2432
2474
|
} else if (h.missingCount > 0) {
|
|
2433
2475
|
const parts = [];
|
|
2434
2476
|
if (h.missingEntryFiles.length > 0) {
|
|
@@ -2441,8 +2483,7 @@ async function doctor() {
|
|
|
2441
2483
|
`${h.missingModuleFiles.length} imported helper (${h.missingModuleFiles.join(', ')}) — ERR_MODULE_NOT_FOUND at hook time`,
|
|
2442
2484
|
);
|
|
2443
2485
|
}
|
|
2444
|
-
|
|
2445
|
-
issues++;
|
|
2486
|
+
issueWarn(`Hook scripts: ${h.missingCount} missing — ${parts.join('; ')}. Fix: ${scriptRemedy}`);
|
|
2446
2487
|
} else {
|
|
2447
2488
|
ok(
|
|
2448
2489
|
`Hook scripts: ${HOOK_SCRIPT_FILES.length} present ` +
|
|
@@ -2608,7 +2649,12 @@ async function doctor() {
|
|
|
2608
2649
|
} else {
|
|
2609
2650
|
ok(`Plugin cache: ${versions.length} version(s) (${sizeStr})`);
|
|
2610
2651
|
}
|
|
2611
|
-
} catch {
|
|
2652
|
+
} catch (e) {
|
|
2653
|
+
// Was empty to the point of carrying no comment. `existsSync` already said the path is
|
|
2654
|
+
// there, so reaching here means it is unreadable or not a directory — a fact about the
|
|
2655
|
+
// plugin install worth one line (R12 audit P3-7).
|
|
2656
|
+
dwarn(`Plugin cache: could not read ${pluginCacheBase} — ${e.message}`);
|
|
2657
|
+
}
|
|
2612
2658
|
}
|
|
2613
2659
|
|
|
2614
2660
|
if (json) {
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -36,6 +36,27 @@ const TOOL_TO_TYPE = {
|
|
|
36
36
|
WebSearch: 'discovery',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The title an imported tool-use carries — and, byte for byte, the cross-run dedup key.
|
|
41
|
+
*
|
|
42
|
+
* Two call sites need this string: `importToolPair` stores it, and `tryImportToolPair`
|
|
43
|
+
* synthesizes it to ask whether a previous run already stored this row. They were separate
|
|
44
|
+
* expressions, and only one of them was SCRUBBED — so any transcript whose title holds
|
|
45
|
+
* something the scrubber rewrites (a bearer token in a `Bash` command, a path segment shaped
|
|
46
|
+
* like a key) never matched itself and re-imported on every run (R12 pre-ship review P3-4).
|
|
47
|
+
*
|
|
48
|
+
* This returns the RAW title and both sites scrub it through `scrubRecord` exactly once.
|
|
49
|
+
* The first cut scrubbed HERE, which left the storage side scrubbed twice (`scrubRecord`
|
|
50
|
+
* scrubs `title` again) against the preview's once — equal only while `scrubSecrets` is
|
|
51
|
+
* idempotent, which is an assumption about the pattern table, not a property anything
|
|
52
|
+
* enforces. Pre-ship review named the asymmetry; one scrub per side removes the assumption
|
|
53
|
+
* rather than betting on it.
|
|
54
|
+
*/
|
|
55
|
+
function importedObsTitle(toolUse) {
|
|
56
|
+
const detail = toolUse?.input?.command || toolEditPath(toolUse?.input) || '';
|
|
57
|
+
return `${toolUse?.name || 'unknown'}: ${String(detail).slice(0, 80)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
function dedupKey(parts) {
|
|
40
61
|
return createHash('sha256').update(parts.join('\x1f')).digest('hex');
|
|
41
62
|
}
|
|
@@ -141,7 +162,11 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
141
162
|
(toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') && editedPath
|
|
142
163
|
? [editedPath]
|
|
143
164
|
: [];
|
|
144
|
-
|
|
165
|
+
// `file_path`, not the shared `editedPath`: `toolEditPath` answers "which path did this
|
|
166
|
+
// tool WRITE", and falls back to `notebook_path` for NotebookEdit's sake. Routing the read
|
|
167
|
+
// column through it too gave a `Read` carrying only `notebook_path` a files_read entry —
|
|
168
|
+
// a shape no tool emits, and a behaviour change D#35 made without declaring it (P3-5).
|
|
169
|
+
const filesRead = toolName === 'Read' && toolUse.input?.file_path ? [toolUse.input.file_path] : [];
|
|
145
170
|
|
|
146
171
|
// `narrative` carries the body and `text` is the derived search blob
|
|
147
172
|
// (lib/observation-write.mjs rebuildObservationDerived). Writing the payload to `text`
|
|
@@ -151,18 +176,17 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
151
176
|
// write directly.
|
|
152
177
|
const body = `${inputJson}\n---\n${resultText}`;
|
|
153
178
|
const safe = scrubRecord('observations', {
|
|
154
|
-
// This string IS the cross-run dedup key
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// "the surface the round unblocked renders a row that identifies nothing."
|
|
179
|
+
// This string IS the cross-run dedup key — `importedObsTitle` is the single home for
|
|
180
|
+
// it, because the two sites that must agree byte-for-byte did not. An earlier cut of
|
|
181
|
+
// this round kept the title on `file_path` alone to avoid the migration — and shipped
|
|
182
|
+
// `NotebookEdit: ` with an empty label for exactly the rows D#35 had just made
|
|
183
|
+
// reachable. Pre-ship review: "the surface the round unblocked renders a row that
|
|
184
|
+
// identifies nothing."
|
|
161
185
|
//
|
|
162
186
|
// The cost is real and bounded: NotebookEdit rows imported before v6.7.2
|
|
163
187
|
// carry the old key, so the next import re-adds each of them ONCE and then
|
|
164
188
|
// matches forever. Stated in the CHANGELOG rather than absorbed silently.
|
|
165
|
-
title:
|
|
189
|
+
title: importedObsTitle(toolUse),
|
|
166
190
|
subtitle: '',
|
|
167
191
|
text: body,
|
|
168
192
|
narrative: body,
|
|
@@ -292,10 +316,10 @@ export async function importJsonl(db, path, { project }) {
|
|
|
292
316
|
if (seenToolUseIds.has(callKey)) return false;
|
|
293
317
|
seenToolUseIds.add(callKey);
|
|
294
318
|
|
|
295
|
-
// Cross-call dedup: synthesize the title the previous run would have
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
const titlePreview =
|
|
319
|
+
// Cross-call dedup: synthesize the title the previous run would have written and check
|
|
320
|
+
// the seenObs set seeded from the DB. Same raw expression AND the same single scrub as
|
|
321
|
+
// the storage site, so the scrubber cannot rewrite one and not the other.
|
|
322
|
+
const titlePreview = scrubRecord('observations', { title: importedObsTitle(useEv) }).title;
|
|
299
323
|
const ts = useEv.timestamp || new Date().toISOString();
|
|
300
324
|
// Match the storage convention from importToolPair (memId-prefixed) so
|
|
301
325
|
// the seenObs entries seeded from the DB can be matched on a re-run.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.
|
|
9
|
+
"version": "6.8.0",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/schema.mjs
CHANGED
|
@@ -831,42 +831,9 @@ export function initSchema(db) {
|
|
|
831
831
|
}
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
const obsFilesCount = db.prepare('SELECT COUNT(*) as c FROM observation_files').get().c;
|
|
838
|
-
if (obsFilesCount === 0) {
|
|
839
|
-
const obsWithFiles = db
|
|
840
|
-
.prepare(
|
|
841
|
-
`SELECT id, files_modified FROM observations WHERE files_modified IS NOT NULL AND files_modified != '[]'`,
|
|
842
|
-
)
|
|
843
|
-
.all();
|
|
844
|
-
if (obsWithFiles.length > 0) {
|
|
845
|
-
const migrateFiles = db.transaction(() => {
|
|
846
|
-
const insertFile = db.prepare(
|
|
847
|
-
'INSERT OR IGNORE INTO observation_files (obs_id, filename) VALUES (?, ?)',
|
|
848
|
-
);
|
|
849
|
-
for (const row of obsWithFiles) {
|
|
850
|
-
try {
|
|
851
|
-
const files = JSON.parse(row.files_modified);
|
|
852
|
-
if (Array.isArray(files)) {
|
|
853
|
-
for (const f of files) {
|
|
854
|
-
if (typeof f === 'string' && f.length > 0) {
|
|
855
|
-
insertFile.run(row.id, f);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
} catch {
|
|
860
|
-
/* skip malformed JSON */
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
migrateFiles();
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
} catch {
|
|
868
|
-
/* non-critical — migration can retry on next open */
|
|
869
|
-
}
|
|
834
|
+
// The files_modified -> observation_files backfill moved to runDeferredCleanups()
|
|
835
|
+
// (R12 pre-ship review P3-3). It used to live here gated on `COUNT(*) FROM
|
|
836
|
+
// observation_files === 0`, which is not the question: see DEFERRED_CLEANUPS.
|
|
870
837
|
|
|
871
838
|
// observation_files orphan cleanup moved to runDeferredCleanups() (audit P1-5):
|
|
872
839
|
// it now runs retryably outside the version fast-path. See DEFERRED_CLEANUPS.
|
|
@@ -1076,6 +1043,59 @@ const DEFERRED_CLEANUPS = [
|
|
|
1076
1043
|
run: (db) =>
|
|
1077
1044
|
db.prepare(`DELETE FROM observation_files WHERE obs_id NOT IN (SELECT id FROM observations)`).run(),
|
|
1078
1045
|
},
|
|
1046
|
+
{
|
|
1047
|
+
// Backfill the junction from the files_modified JSON column. This ran inside initSchema
|
|
1048
|
+
// for a long time, gated on `COUNT(*) FROM observation_files === 0` — which answers "has
|
|
1049
|
+
// this store ever written an edge", not "has this backfill run". One real `mem_save`
|
|
1050
|
+
// falsifies it forever, so every observation imported before v6.7.2 (the release that
|
|
1051
|
+
// taught import-jsonl to write edges at all) stayed permanently unreachable by file, and
|
|
1052
|
+
// re-importing could not repair them: cross-run dedup skips the row before the edge
|
|
1053
|
+
// write. The marker here answers the question actually being asked, and an exception
|
|
1054
|
+
// leaves it unset so the next open retries.
|
|
1055
|
+
//
|
|
1056
|
+
// Deliberately NOT a schema-version bump. A bump locks every older code home out of the
|
|
1057
|
+
// database permanently (see lib/schema-skew.mjs), and on a plugin install that is
|
|
1058
|
+
// reached routinely — far too much to charge for a derived table.
|
|
1059
|
+
//
|
|
1060
|
+
// One-shot by design: every live path that stores an observation routes its edges
|
|
1061
|
+
// through the single junction writer (`insertObservationFiles`, lib/observation-write),
|
|
1062
|
+
// so rows arriving after this pass need nothing from it. An earlier draft of this line
|
|
1063
|
+
// said "the import and save paths both" — there are more than two entry points reaching
|
|
1064
|
+
// that one writer (save, episode flush, insight promotion, restore, import), and the
|
|
1065
|
+
// claim that matters is the single writer, not the count of callers.
|
|
1066
|
+
// `NOT EXISTS` keeps the scan to the rows that are actually missing an edge, which is
|
|
1067
|
+
// zero on a store that never imported.
|
|
1068
|
+
name: 'backfill-observation-files',
|
|
1069
|
+
run: (db) => {
|
|
1070
|
+
const rows = db
|
|
1071
|
+
.prepare(
|
|
1072
|
+
`SELECT o.id, o.files_modified FROM observations o
|
|
1073
|
+
WHERE o.files_modified IS NOT NULL AND o.files_modified != '[]'
|
|
1074
|
+
AND NOT EXISTS (SELECT 1 FROM observation_files f WHERE f.obs_id = o.id)`,
|
|
1075
|
+
)
|
|
1076
|
+
.all();
|
|
1077
|
+
if (rows.length === 0) return;
|
|
1078
|
+
const insertFile = db.prepare(
|
|
1079
|
+
'INSERT OR IGNORE INTO observation_files (obs_id, filename) VALUES (?, ?)',
|
|
1080
|
+
);
|
|
1081
|
+
db.transaction(() => {
|
|
1082
|
+
for (const row of rows) {
|
|
1083
|
+
let files;
|
|
1084
|
+
try {
|
|
1085
|
+
files = JSON.parse(row.files_modified);
|
|
1086
|
+
} catch {
|
|
1087
|
+
// One unparseable row must not cost every row after it its edges — the whole
|
|
1088
|
+
// pass is marked done afterwards, so "skipped" here means "never backfilled".
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (!Array.isArray(files)) continue;
|
|
1092
|
+
for (const f of files) {
|
|
1093
|
+
if (typeof f === 'string' && f.length > 0) insertFile.run(row.id, f);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
})();
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1079
1099
|
{
|
|
1080
1100
|
// Project-name normalization: migrate short names ("mem") to canonical
|
|
1081
1101
|
// ("projects--mem") by EXACT canonical-suffix match. Idempotent: only acts on
|