claude-mem-lite 3.20.0 → 3.21.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/hook-update.mjs +58 -19
- package/hook.mjs +22 -15
- package/lib/db-backup.mjs +55 -0
- package/lib/maintain-core.mjs +25 -0
- package/mem-cli.mjs +10 -1
- package/package.json +2 -1
- package/server.mjs +10 -1
- package/source-files.mjs +4 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.21.0",
|
|
14
14
|
"source": "./",
|
|
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)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.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/hook-update.mjs
CHANGED
|
@@ -489,6 +489,51 @@ export async function verifyReleaseAuthenticity(extractedDir, assets, publicKey
|
|
|
489
489
|
// below skips the 'node_modules' switchable path (existsSync guard), leaving
|
|
490
490
|
// the target's node_modules untouched. Dependency bumps still flow through the
|
|
491
491
|
// GitHub-tarball path (downloadAndInstall), which keeps skipNpmInstall=false.
|
|
492
|
+
// Undo a (partial or complete) file swap: delete the freshly-installed files, then
|
|
493
|
+
// rename each backup back into place. Shared by the error path and the MED-5
|
|
494
|
+
// post-install smoke gate so there is ONE rollback implementation.
|
|
495
|
+
function rollbackInstall(installed, backedUp, backupDir, targetDir) {
|
|
496
|
+
for (const relPath of installed.reverse()) {
|
|
497
|
+
try { rmSync(join(targetDir, relPath), { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
498
|
+
}
|
|
499
|
+
for (const relPath of backedUp.reverse()) {
|
|
500
|
+
const backupPath = join(backupDir, relPath);
|
|
501
|
+
const targetPath = join(targetDir, relPath);
|
|
502
|
+
try {
|
|
503
|
+
if (existsSync(backupPath)) {
|
|
504
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
505
|
+
renameSync(backupPath, targetPath);
|
|
506
|
+
}
|
|
507
|
+
} catch (restoreErr) {
|
|
508
|
+
debugCatch(restoreErr, `installExtractedRelease-restore-${relPath}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// MED-5 post-install health gate: load-test the freshly-switched code in a SEPARATE
|
|
514
|
+
// process before the backup is discarded. A clean file swap can still yield an
|
|
515
|
+
// install that won't boot — a syntax error, an unresolved import from a half-applied
|
|
516
|
+
// mixed-version swap (renameSync loop hard-killed mid-way), or a native ABI mismatch.
|
|
517
|
+
// `cli.mjs help` boots the CLI entry, which dynamically imports the mem-cli module
|
|
518
|
+
// graph (maintain-core / search-engine / scoring / db-backup / schema / …) and exits
|
|
519
|
+
// 0 without opening the DB or a server. hook.mjs and server.mjs auto-execute on
|
|
520
|
+
// import (so they can't be import-smoked) — they get a `node --check` syntax pass.
|
|
521
|
+
// execSync (not execFileSync) so the unit-test child_process mock intercepts it.
|
|
522
|
+
function smokeInstalledRelease(targetDir) {
|
|
523
|
+
const q = (s) => JSON.stringify(s);
|
|
524
|
+
try {
|
|
525
|
+
execSync(`${q(process.execPath)} ${q(join(targetDir, 'cli.mjs'))} help`, { timeout: 20000, stdio: 'ignore' });
|
|
526
|
+
for (const entry of ['hook.mjs', 'server.mjs']) {
|
|
527
|
+
const p = join(targetDir, entry);
|
|
528
|
+
if (existsSync(p)) execSync(`${q(process.execPath)} --check ${q(p)}`, { timeout: 10000, stdio: 'ignore' });
|
|
529
|
+
}
|
|
530
|
+
return true;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
debugLog('WARN', 'hook-update', `post-install smoke failed (rolling back): ${e.message}`);
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
492
537
|
export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR, opts = {}) {
|
|
493
538
|
// Cross-process lock: concurrent SessionStart self-heals / auto-updates must
|
|
494
539
|
// not interleave the rename loop below (→ mixed-version install). A live peer
|
|
@@ -541,6 +586,16 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
541
586
|
installed.push(relPath);
|
|
542
587
|
}
|
|
543
588
|
|
|
589
|
+
// MED-5: before discarding the rollback backup, prove the switched code boots.
|
|
590
|
+
// If it can't, restore the backup and report failure — the running (old) version
|
|
591
|
+
// keeps working rather than leaving a broken install with no way back.
|
|
592
|
+
if (!opts.skipSmoke && !smokeInstalledRelease(targetDir)) {
|
|
593
|
+
rollbackInstall(installed, backedUp, backupDir, targetDir);
|
|
594
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
595
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
|
|
544
599
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
545
600
|
rmSync(backupDir, { recursive: true, force: true });
|
|
546
601
|
|
|
@@ -580,25 +635,9 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
|
|
|
580
635
|
return true;
|
|
581
636
|
} catch (err) {
|
|
582
637
|
debugCatch(err, 'installExtractedRelease');
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
for (const relPath of backedUp.reverse()) {
|
|
588
|
-
const backupPath = join(backupDir, relPath);
|
|
589
|
-
const targetPath = join(targetDir, relPath);
|
|
590
|
-
try {
|
|
591
|
-
if (existsSync(backupPath)) {
|
|
592
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
593
|
-
renameSync(backupPath, targetPath);
|
|
594
|
-
}
|
|
595
|
-
} catch (restoreErr) {
|
|
596
|
-
debugCatch(restoreErr, `installExtractedRelease-restore-${relPath}`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
|
|
601
|
-
try { rmSync(backupDir, { recursive: true, force: true }); } catch {}
|
|
638
|
+
rollbackInstall(installed, backedUp, backupDir, targetDir);
|
|
639
|
+
try { rmSync(stagingDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
640
|
+
try { rmSync(backupDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
602
641
|
return false;
|
|
603
642
|
} finally {
|
|
604
643
|
release();
|
package/hook.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
truncate, inferProject, detectBashSignificance,
|
|
27
27
|
extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
|
|
28
28
|
makeEntryDesc, scrubSecrets, stripPrivate, EDIT_TOOLS, debugCatch, debugLog,
|
|
29
|
-
COMPRESSED_AUTO,
|
|
29
|
+
COMPRESSED_AUTO, OBS_BM25, notLowSignalTitleClause, formatErrorRecallHints,
|
|
30
30
|
} from './utils.mjs';
|
|
31
31
|
import {
|
|
32
32
|
readEpisodeRaw, episodeFile,
|
|
@@ -46,7 +46,8 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
|
|
|
46
46
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
47
47
|
import { formatHookError } from './lib/native-binding-hint.mjs';
|
|
48
48
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
49
|
-
import { cleanupBroken, decayAndMarkIdle, boostAccessed, selectFuzzyDedupeIds } from './lib/maintain-core.mjs';
|
|
49
|
+
import { cleanupBroken, decayAndMarkIdle, boostAccessed, selectFuzzyDedupeIds, hardDeleteCandidateCount, purgeStale } from './lib/maintain-core.mjs';
|
|
50
|
+
import { snapshotDb } from './lib/db-backup.mjs';
|
|
50
51
|
import {
|
|
51
52
|
extractCitationsFromTranscript,
|
|
52
53
|
extractAllInjected,
|
|
@@ -749,21 +750,27 @@ function runSessionStartAutoMaintain(db) {
|
|
|
749
750
|
try {
|
|
750
751
|
const STALE_AGE = Date.now() - 30 * 86400000;
|
|
751
752
|
const OP_CAP = 500;
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
// Older cutoffs (e.g. 7d) were always redundant with the 30d marking filter and
|
|
756
|
-
// made purge effectively immediate on the next maintenance cycle — fix for T4-P1-A.
|
|
757
|
-
const purged = db.prepare(`
|
|
758
|
-
DELETE FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
759
|
-
AND created_at_epoch < ?
|
|
760
|
-
`).run(Date.now() - 37 * 86400000);
|
|
761
|
-
if (purged.changes > 0) debugLog('DEBUG', 'auto-maintain', `purged ${purged.changes} stale observations`);
|
|
762
|
-
|
|
763
|
-
// cleanup / decay+mark-idle / boost via maintain-core (shared with CLI + MCP).
|
|
764
|
-
// injection_count>0 protection lives in decayAndMarkIdle. Whole-DB, cap 500.
|
|
753
|
+
// Shared maintenance context (whole-DB, cap 500) — used by every maintain-core
|
|
754
|
+
// op below AND the MED-2 snapshot guard. injection_count>0 protection lives in
|
|
755
|
+
// decayAndMarkIdle.
|
|
765
756
|
const mctx = { projectFilter: '', baseParams: [], staleAge: STALE_AGE, opCap: OP_CAP };
|
|
766
757
|
|
|
758
|
+
// MED-2: snapshot the DB before the irreversible purge/cleanup hard-deletes
|
|
759
|
+
// below, but only when rows will actually be removed (cheap COUNT). Must run
|
|
760
|
+
// here, outside any transaction — VACUUM cannot run inside one. Best-effort:
|
|
761
|
+
// snapshotDb never throws, so a backup failure cannot block auto-maintain.
|
|
762
|
+
if (hardDeleteCandidateCount(db, mctx, { cleanup: true, purge: true }) > 0) {
|
|
763
|
+
snapshotDb(db, { tag: 'pre-maintain' });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Purge FIRST via the SHARED purgeStale — was an inline DELETE that skipped
|
|
767
|
+
// recoverChildrenOf, so purging a keeper that had absorbed dups orphaned its
|
|
768
|
+
// children (compressed_into dangling at a deleted id). purgeStale recovers them
|
|
769
|
+
// first and caps at opCap. Schema has no marked_at_epoch, so retention anchors on
|
|
770
|
+
// created_at_epoch: 30d marking gate + 7d grace = 37d.
|
|
771
|
+
const purged = purgeStale(db, mctx, Date.now() - 37 * 86400000);
|
|
772
|
+
if (purged > 0) debugLog('DEBUG', 'auto-maintain', `purged ${purged} stale observations`);
|
|
773
|
+
|
|
767
774
|
const cleaned = cleanupBroken(db, mctx);
|
|
768
775
|
if (cleaned > 0) debugLog('DEBUG', 'auto-maintain', `cleaned ${cleaned} broken observations`);
|
|
769
776
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// lib/db-backup.mjs — point-in-time DB snapshot before irreversible maintenance.
|
|
2
|
+
//
|
|
3
|
+
// VACUUM INTO produces a consistent, compact copy of the database (WAL-safe —
|
|
4
|
+
// unlike copyFileSync, which would miss un-checkpointed WAL frames). Best-effort:
|
|
5
|
+
// a failure logs a WARN and returns null so it NEVER blocks the maintenance it
|
|
6
|
+
// guards (a backup that aborts a disk-full purge would be worse than no backup).
|
|
7
|
+
//
|
|
8
|
+
// MUST be called OUTSIDE any transaction — VACUUM cannot run inside one, which is
|
|
9
|
+
// why the maintenance entry points snapshot before opening their db.transaction().
|
|
10
|
+
import { readdirSync, unlinkSync } from 'fs';
|
|
11
|
+
import { dirname, basename, join } from 'path';
|
|
12
|
+
import { debugLog } from '../utils.mjs';
|
|
13
|
+
|
|
14
|
+
// Monotonic per-process suffix so two snapshots in the same millisecond (same pid)
|
|
15
|
+
// still get unique filenames (VACUUM INTO fails if the target already exists).
|
|
16
|
+
let _seq = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Snapshot `db` to `<db-path>.<tag>-<ts>.bak` via VACUUM INTO, then prune to the
|
|
20
|
+
* newest `retain` snapshots. No-op (returns null) for :memory: DBs (tests) and on
|
|
21
|
+
* any error (logged). Returns the snapshot path on success.
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
export function snapshotDb(db, { tag = 'pre-maintain', retain = 3 } = {}) {
|
|
25
|
+
try {
|
|
26
|
+
const dbPath = db && db.name;
|
|
27
|
+
if (!dbPath || dbPath === ':memory:') return null; // in-memory — nothing to snapshot
|
|
28
|
+
const stamp = `${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}-${_seq++}`;
|
|
29
|
+
const out = `${dbPath}.${tag}-${stamp}.bak`;
|
|
30
|
+
// Path is internal (db.name from our own config), but escape single quotes
|
|
31
|
+
// defensively since VACUUM INTO takes a string literal, not a bound param.
|
|
32
|
+
db.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
33
|
+
pruneSnapshots(dbPath, tag, retain);
|
|
34
|
+
return out;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
try { debugLog('WARN', 'db-backup', `snapshot skipped (proceeding without): ${e.message}`); } catch { /* ignore */ }
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Keep the newest `retain` `<base>.<tag>-*.bak` files, unlink the rest. The ISO
|
|
42
|
+
// timestamp embedded in the name makes a lexical sort chronological.
|
|
43
|
+
function pruneSnapshots(dbPath, tag, retain) {
|
|
44
|
+
try {
|
|
45
|
+
const dir = dirname(dbPath);
|
|
46
|
+
const prefix = `${basename(dbPath)}.${tag}-`;
|
|
47
|
+
const names = readdirSync(dir)
|
|
48
|
+
.filter(n => n.startsWith(prefix) && n.endsWith('.bak'))
|
|
49
|
+
.sort()
|
|
50
|
+
.reverse(); // newest first
|
|
51
|
+
for (const n of names.slice(retain)) {
|
|
52
|
+
try { unlinkSync(join(dir, n)); } catch { /* per-entry best-effort */ }
|
|
53
|
+
}
|
|
54
|
+
} catch { /* best-effort — dir may be unreadable */ }
|
|
55
|
+
}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -238,6 +238,31 @@ export function mergeDuplicates(db, groups) {
|
|
|
238
238
|
return merged;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Count rows a destructive maintenance run would hard-DELETE: pending-purge rows
|
|
243
|
+
* (any age — a cheap proxy for "purge has something to remove", deliberately not
|
|
244
|
+
* age-filtered so the guard never under-counts) and/or broken empty-content rows
|
|
245
|
+
* (cleanupBroken's doomed set). Used by the maintenance entry points to decide
|
|
246
|
+
* whether to VACUUM-snapshot the DB first (audit MED-2) — over-counting only costs
|
|
247
|
+
* one extra bounded backup; under-counting would skip the safety net.
|
|
248
|
+
*/
|
|
249
|
+
export function hardDeleteCandidateCount(db, { projectFilter, baseParams }, { cleanup = false, purge = false } = {}) {
|
|
250
|
+
let n = 0;
|
|
251
|
+
if (purge) {
|
|
252
|
+
n += db.prepare(
|
|
253
|
+
`SELECT COUNT(*) AS c FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE} ${projectFilter}`
|
|
254
|
+
).get(...baseParams).c;
|
|
255
|
+
}
|
|
256
|
+
if (cleanup) {
|
|
257
|
+
n += db.prepare(
|
|
258
|
+
`SELECT COUNT(*) AS c FROM observations
|
|
259
|
+
WHERE COALESCE(compressed_into, 0) = 0
|
|
260
|
+
AND (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '') ${projectFilter}`
|
|
261
|
+
).get(...baseParams).c;
|
|
262
|
+
}
|
|
263
|
+
return n;
|
|
264
|
+
}
|
|
265
|
+
|
|
241
266
|
/** Preview pending-purge candidates older than the retain cutoff (no deletion). */
|
|
242
267
|
export function purgeStalePreview(db, { projectFilter, baseParams }, retainCutoff) {
|
|
243
268
|
return db.prepare(`
|
package/mem-cli.mjs
CHANGED
|
@@ -19,9 +19,10 @@ import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
|
19
19
|
import {
|
|
20
20
|
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
21
21
|
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
22
|
-
recoverChildrenOf,
|
|
22
|
+
recoverChildrenOf, hardDeleteCandidateCount,
|
|
23
23
|
OP_CAP, STALE_AGE_MS, PINNED_INJ_THRESHOLD,
|
|
24
24
|
} from './lib/maintain-core.mjs';
|
|
25
|
+
import { snapshotDb } from './lib/db-backup.mjs';
|
|
25
26
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
26
27
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
27
28
|
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
@@ -1867,6 +1868,14 @@ function cmdMaintain(db, args) {
|
|
|
1867
1868
|
// T2-P1-B: surface the OP_CAP hit so users know to re-run, matching MCP mem_maintain.
|
|
1868
1869
|
const capHint = (changes) => (changes >= OP_CAP ? ' (cap reached, re-run for more)' : '');
|
|
1869
1870
|
|
|
1871
|
+
// MED-2: snapshot the DB before the irreversible cleanup/purge hard-deletes —
|
|
1872
|
+
// only when rows will actually be removed, and OUTSIDE the transaction below
|
|
1873
|
+
// (VACUUM cannot run inside one). Best-effort; snapshotDb never throws.
|
|
1874
|
+
const willPurge = ops.includes('purge_stale') && (flags.confirm === true || flags.confirm === 'true');
|
|
1875
|
+
if (hardDeleteCandidateCount(db, mctx, { cleanup: ops.includes('cleanup'), purge: willPurge }) > 0) {
|
|
1876
|
+
snapshotDb(db, { tag: 'pre-maintain' });
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1870
1879
|
db.transaction(() => {
|
|
1871
1880
|
if (ops.includes('cleanup')) {
|
|
1872
1881
|
const deleted = cleanupBroken(db, mctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.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",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"lib/search-core.mjs",
|
|
85
85
|
"lib/rrf.mjs",
|
|
86
86
|
"lib/compress-core.mjs",
|
|
87
|
+
"lib/db-backup.mjs",
|
|
87
88
|
"lib/maintain-core.mjs",
|
|
88
89
|
"lib/dedup-constants.mjs",
|
|
89
90
|
"lib/deferred-work.mjs",
|
package/server.mjs
CHANGED
|
@@ -18,9 +18,10 @@ import { buildSearchFtsQuery, parseDateBounds, coreRunSearchPipeline } from './l
|
|
|
18
18
|
import {
|
|
19
19
|
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
20
20
|
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
21
|
-
recoverChildrenOf,
|
|
21
|
+
recoverChildrenOf, hardDeleteCandidateCount,
|
|
22
22
|
OP_CAP, STALE_AGE_MS,
|
|
23
23
|
} from './lib/maintain-core.mjs';
|
|
24
|
+
import { snapshotDb } from './lib/db-backup.mjs';
|
|
24
25
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
25
26
|
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
26
27
|
import { formatObsFieldValue } from './cli/common.mjs';
|
|
@@ -1078,6 +1079,14 @@ server.registerTool(
|
|
|
1078
1079
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1079
1080
|
}
|
|
1080
1081
|
|
|
1082
|
+
// MED-2: snapshot the DB before the irreversible cleanup/purge hard-deletes —
|
|
1083
|
+
// only when rows will actually be removed, and OUTSIDE the transaction below
|
|
1084
|
+
// (VACUUM cannot run inside one). purge_stale is already confirmed by here (the
|
|
1085
|
+
// preview branch returned above otherwise). Best-effort; snapshotDb never throws.
|
|
1086
|
+
if (hardDeleteCandidateCount(db, mctx, { cleanup: ops.includes('cleanup'), purge: ops.includes('purge_stale') }) > 0) {
|
|
1087
|
+
snapshotDb(db, { tag: 'pre-maintain' });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1081
1090
|
db.transaction(() => {
|
|
1082
1091
|
if (ops.includes('cleanup')) {
|
|
1083
1092
|
const deleted = cleanupBroken(db, mctx);
|
package/source-files.mjs
CHANGED
|
@@ -131,6 +131,10 @@ export const SOURCE_FILES = [
|
|
|
131
131
|
// Statically imported by mem-cli.mjs (cmdMaintain), server.mjs (mem_maintain),
|
|
132
132
|
// and hook.mjs (handleAutoMaintain) — missing it would break maintain on auto-update.
|
|
133
133
|
'lib/maintain-core.mjs',
|
|
134
|
+
// Pre-maintenance VACUUM INTO snapshot (MED-2). Statically imported by mem-cli.mjs,
|
|
135
|
+
// server.mjs, and hook.mjs before their destructive purge/cleanup — missing it
|
|
136
|
+
// would crash maintain on auto-update with an unresolved import.
|
|
137
|
+
'lib/db-backup.mjs',
|
|
134
138
|
// P10 dedup/merge threshold constants — single source of truth for the Jaccard
|
|
135
139
|
// dedup/merge cutoffs. Statically imported by hook.mjs, hook-llm.mjs,
|
|
136
140
|
// hook-optimize.mjs, mem-cli.mjs, server.mjs, and the save/maintain cores;
|