claude-mem-lite 2.87.0 → 2.88.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.mjs +21 -106
- package/lib/compress-core.mjs +98 -0
- package/lib/maintain-core.mjs +236 -0
- package/mem-cli.mjs +50 -248
- package/package.json +3 -1
- package/server.mjs +40 -252
- package/source-files.mjs +9 -0
package/mem-cli.mjs
CHANGED
|
@@ -4,17 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDb, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject,
|
|
7
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, scrubSecrets, cjkBigrams, SESS_BM25, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause } from './utils.mjs';
|
|
8
8
|
import { cjkPrecisionOk } from './nlp.mjs';
|
|
9
9
|
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
10
10
|
import { resolveProject } from './project-utils.mjs';
|
|
11
11
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
12
|
-
import { getVocabulary, computeVector,
|
|
12
|
+
import { getVocabulary, computeVector, _resetVocabCache } from './tfidf.mjs';
|
|
13
13
|
import { autoBoostIfNeeded, reRankWithContext, markSuperseded } from './server-internals.mjs';
|
|
14
14
|
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
15
15
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
16
16
|
import { searchResources } from './registry-retriever.mjs';
|
|
17
|
-
import {
|
|
17
|
+
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
18
|
+
import {
|
|
19
|
+
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
20
|
+
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
21
|
+
OP_CAP, STALE_AGE_MS, PINNED_INJ_THRESHOLD,
|
|
22
|
+
} from './lib/maintain-core.mjs';
|
|
18
23
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
19
24
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
20
25
|
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
@@ -1772,33 +1777,15 @@ function cmdCompress(db, args) {
|
|
|
1772
1777
|
}
|
|
1773
1778
|
const cutoff = Date.now() - ageDays * 86400000;
|
|
1774
1779
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1775
|
-
const projectFilter = project ? 'AND project = ?' : '';
|
|
1776
|
-
const baseParams = project ? [project] : [];
|
|
1777
1780
|
|
|
1778
|
-
const candidates = db
|
|
1779
|
-
SELECT id, project, type, title, created_at, created_at_epoch
|
|
1780
|
-
FROM observations
|
|
1781
|
-
WHERE COALESCE(importance, 1) = 1
|
|
1782
|
-
AND COALESCE(access_count, 0) = 0
|
|
1783
|
-
AND created_at_epoch < ?
|
|
1784
|
-
AND compressed_into IS NULL
|
|
1785
|
-
${projectFilter}
|
|
1786
|
-
ORDER BY project, created_at_epoch
|
|
1787
|
-
`).all(cutoff, ...baseParams);
|
|
1781
|
+
const candidates = selectCompressionCandidates(db, { cutoff, project });
|
|
1788
1782
|
|
|
1789
1783
|
if (candidates.length === 0) {
|
|
1790
1784
|
out('[mem] No candidates for compression.');
|
|
1791
1785
|
return;
|
|
1792
1786
|
}
|
|
1793
1787
|
|
|
1794
|
-
|
|
1795
|
-
const groups = new Map();
|
|
1796
|
-
for (const c of candidates) {
|
|
1797
|
-
const key = `${c.project}::${isoWeekKey(c.created_at_epoch)}`;
|
|
1798
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
1799
|
-
groups.get(key).push(c);
|
|
1800
|
-
}
|
|
1801
|
-
const compressableGroups = [...groups.entries()].filter(([, obs]) => obs.length >= 3);
|
|
1788
|
+
const compressableGroups = groupByProjectWeek(candidates);
|
|
1802
1789
|
|
|
1803
1790
|
if (preview) {
|
|
1804
1791
|
const totalCandidates = compressableGroups.reduce((s, [, obs]) => s + obs.length, 0);
|
|
@@ -1817,46 +1804,12 @@ function cmdCompress(db, args) {
|
|
|
1817
1804
|
return;
|
|
1818
1805
|
}
|
|
1819
1806
|
|
|
1820
|
-
// Execute compression
|
|
1807
|
+
// Execute compression — one transaction over all groups (the hook transacts per group).
|
|
1821
1808
|
let totalCompressed = 0;
|
|
1822
|
-
const insertSummary = db.prepare(`
|
|
1823
|
-
INSERT INTO observations (memory_session_id, project, text, type, title, subtitle, narrative, concepts, facts, files_read, files_modified, importance, created_at, created_at_epoch)
|
|
1824
|
-
VALUES (?, ?, ?, ?, ?, '', ?, '', '', '[]', '[]', 2, ?, ?)
|
|
1825
|
-
`);
|
|
1826
|
-
|
|
1827
1809
|
db.transaction(() => {
|
|
1828
1810
|
for (const [key, obs] of compressableGroups) {
|
|
1829
1811
|
const [proj] = key.split('::');
|
|
1830
|
-
|
|
1831
|
-
for (const o of obs) types[o.type] = (types[o.type] || 0) + 1;
|
|
1832
|
-
const dominantType = Object.entries(types).sort((a, b) => b[1] - a[1])[0][0];
|
|
1833
|
-
const title = `Weekly summary: ${obs.length} ${dominantType} observations`;
|
|
1834
|
-
const narrative = obs.map(o => `- ${o.title || '(untitled)'}`).join('\n');
|
|
1835
|
-
const sessionId = `compress-${proj}`;
|
|
1836
|
-
|
|
1837
|
-
const sortedEpochs = obs.map(o => o.created_at_epoch).sort((a, b) => a - b);
|
|
1838
|
-
const medianEpoch = sortedEpochs[Math.floor(sortedEpochs.length / 2)];
|
|
1839
|
-
const medianDate = new Date(medianEpoch);
|
|
1840
|
-
|
|
1841
|
-
const now = new Date();
|
|
1842
|
-
db.prepare(`
|
|
1843
|
-
INSERT OR IGNORE INTO sdk_sessions (content_session_id, memory_session_id, project, started_at, started_at_epoch, status)
|
|
1844
|
-
VALUES (?, ?, ?, ?, ?, 'active')
|
|
1845
|
-
`).run(sessionId, sessionId, proj, now.toISOString(), now.getTime());
|
|
1846
|
-
|
|
1847
|
-
// Defense-in-depth: source rows already scrubbed at original ingest, but
|
|
1848
|
-
// the new compressed narrative is constructed here and re-persisted.
|
|
1849
|
-
const safe = scrubRecord('observations', { text: narrative, title, narrative });
|
|
1850
|
-
const summaryResult = insertSummary.run(
|
|
1851
|
-
sessionId, proj, safe.text, dominantType, safe.title, safe.narrative,
|
|
1852
|
-
medianDate.toISOString(), medianEpoch
|
|
1853
|
-
);
|
|
1854
|
-
const summaryId = Number(summaryResult.lastInsertRowid);
|
|
1855
|
-
|
|
1856
|
-
const obsIds = obs.map(o => o.id);
|
|
1857
|
-
const obsPh = obsIds.map(() => '?').join(',');
|
|
1858
|
-
db.prepare(`UPDATE observations SET compressed_into = ? WHERE id IN (${obsPh})`).run(summaryId, ...obsIds);
|
|
1859
|
-
totalCompressed += obs.length;
|
|
1812
|
+
totalCompressed += compressGroup(db, proj, obs).compressed;
|
|
1860
1813
|
}
|
|
1861
1814
|
})();
|
|
1862
1815
|
|
|
@@ -1876,60 +1829,12 @@ function cmdMaintain(db, args) {
|
|
|
1876
1829
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1877
1830
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
1878
1831
|
const baseParams = project ? [project] : [];
|
|
1879
|
-
const STALE_AGE_MS = 30 * 86400000;
|
|
1880
|
-
const SCAN_LIMIT = 500;
|
|
1881
|
-
const SIMILARITY_THRESHOLD = 0.7;
|
|
1882
|
-
// demote_pinned threshold: a memory injected this many times with zero
|
|
1883
|
-
// citations is "pinned noise" the regular `decay` op can't touch (decay
|
|
1884
|
-
// protects injection_count > 0). 8 aligns with the noise-penalty tier-2 cut.
|
|
1885
|
-
const PINNED_INJ_THRESHOLD = 8;
|
|
1886
1832
|
|
|
1887
1833
|
if (action === 'scan') {
|
|
1888
1834
|
const staleAge = Date.now() - STALE_AGE_MS;
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
const
|
|
1892
|
-
SELECT id, title, importance, access_count, created_at_epoch
|
|
1893
|
-
FROM observations
|
|
1894
|
-
WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
|
|
1895
|
-
ORDER BY created_at_epoch DESC LIMIT ${SCAN_LIMIT}
|
|
1896
|
-
`).all(...baseParams);
|
|
1897
|
-
|
|
1898
|
-
const titles = recent.map(r => (r.title || '').trim());
|
|
1899
|
-
const minhashes = titles.map(t => t ? computeMinHash(t) : null);
|
|
1900
|
-
const duplicates = [];
|
|
1901
|
-
for (let i = 0; i < recent.length && duplicates.length < 50; i++) {
|
|
1902
|
-
if (!titles[i] || !minhashes[i]) continue;
|
|
1903
|
-
for (let j = i + 1; j < recent.length; j++) {
|
|
1904
|
-
if (!titles[j] || !minhashes[j]) continue;
|
|
1905
|
-
if (estimateJaccardFromMinHash(minhashes[i], minhashes[j]) < 0.5) continue;
|
|
1906
|
-
const sim = jaccardSimilarity(titles[i], titles[j]);
|
|
1907
|
-
if (sim > SIMILARITY_THRESHOLD) {
|
|
1908
|
-
duplicates.push({ a: recent[i], b: recent[j], similarity: sim.toFixed(2) });
|
|
1909
|
-
}
|
|
1910
|
-
if (duplicates.length >= 50) break;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
const stats = db.prepare(`
|
|
1915
|
-
SELECT
|
|
1916
|
-
COUNT(*) as total,
|
|
1917
|
-
COALESCE(SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
|
|
1918
|
-
AND created_at_epoch < ? THEN 1 ELSE 0 END), 0) as stale,
|
|
1919
|
-
COALESCE(SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
|
|
1920
|
-
THEN 1 ELSE 0 END), 0) as broken,
|
|
1921
|
-
COALESCE(SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
|
|
1922
|
-
THEN 1 ELSE 0 END), 0) as boostable,
|
|
1923
|
-
COALESCE(SUM(CASE WHEN COALESCE(injection_count, 0) >= ${PINNED_INJ_THRESHOLD}
|
|
1924
|
-
AND COALESCE(cited_count, 0) = 0 AND COALESCE(importance, 1) > 1
|
|
1925
|
-
THEN 1 ELSE 0 END), 0) as pinned
|
|
1926
|
-
FROM observations
|
|
1927
|
-
WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
|
|
1928
|
-
`).get(staleAge, ...baseParams);
|
|
1929
|
-
|
|
1930
|
-
const pendingPurge = db.prepare(
|
|
1931
|
-
`SELECT COUNT(*) as count FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE} ${projectFilter}`
|
|
1932
|
-
).get(...baseParams);
|
|
1835
|
+
const mctx = { projectFilter, baseParams, staleAge };
|
|
1836
|
+
const duplicates = findDuplicates(db, mctx);
|
|
1837
|
+
const stats = maintenanceStats(db, mctx);
|
|
1933
1838
|
|
|
1934
1839
|
out(`[mem] Maintenance scan:`);
|
|
1935
1840
|
out(` Total active: ${stats.total}`);
|
|
@@ -1938,7 +1843,7 @@ function cmdMaintain(db, args) {
|
|
|
1938
1843
|
out(` Broken (no title/narrative): ${stats.broken}`);
|
|
1939
1844
|
out(` Boostable (accessed>3, imp<3): ${stats.boostable}`);
|
|
1940
1845
|
out(` Pinned-but-uncited (inj>=${PINNED_INJ_THRESHOLD}, cited=0, imp>1): ${stats.pinned} — run: maintain execute --ops demote_pinned`);
|
|
1941
|
-
out(` Pending purge: ${pendingPurge
|
|
1846
|
+
out(` Pending purge: ${stats.pendingPurge} (compressed originals awaiting cleanup)`);
|
|
1942
1847
|
if (duplicates.length > 0) {
|
|
1943
1848
|
const AUTO_MERGE_THRESHOLD = 0.85;
|
|
1944
1849
|
const autoMergeable = duplicates.filter(d => parseFloat(d.similarity) >= AUTO_MERGE_THRESHOLD);
|
|
@@ -1979,7 +1884,7 @@ function cmdMaintain(db, args) {
|
|
|
1979
1884
|
return;
|
|
1980
1885
|
}
|
|
1981
1886
|
const staleAge = Date.now() - STALE_AGE_MS;
|
|
1982
|
-
const
|
|
1887
|
+
const mctx = { projectFilter, baseParams, staleAge, opCap: OP_CAP };
|
|
1983
1888
|
const results = [];
|
|
1984
1889
|
|
|
1985
1890
|
// T2-P1-B: surface the OP_CAP hit so users know to re-run, matching MCP mem_maintain.
|
|
@@ -1987,108 +1892,43 @@ function cmdMaintain(db, args) {
|
|
|
1987
1892
|
|
|
1988
1893
|
db.transaction(() => {
|
|
1989
1894
|
if (ops.includes('cleanup')) {
|
|
1990
|
-
const deleted = db
|
|
1991
|
-
|
|
1992
|
-
SELECT id FROM observations
|
|
1993
|
-
WHERE COALESCE(compressed_into, 0) = 0
|
|
1994
|
-
AND (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
|
|
1995
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
1996
|
-
)
|
|
1997
|
-
`).run(...baseParams);
|
|
1998
|
-
results.push(`Cleaned up ${deleted.changes} broken observations${capHint(deleted.changes)}`);
|
|
1895
|
+
const deleted = cleanupBroken(db, mctx);
|
|
1896
|
+
results.push(`Cleaned up ${deleted} broken observations${capHint(deleted)}`);
|
|
1999
1897
|
}
|
|
2000
1898
|
|
|
2001
1899
|
if (ops.includes('decay')) {
|
|
2002
|
-
//
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
UPDATE observations SET importance = MAX(1, COALESCE(importance, 1) - 1)
|
|
2007
|
-
WHERE id IN (
|
|
2008
|
-
SELECT id FROM observations
|
|
2009
|
-
WHERE COALESCE(compressed_into, 0) = 0
|
|
2010
|
-
AND COALESCE(importance, 1) > 1
|
|
2011
|
-
AND COALESCE(access_count, 0) = 0
|
|
2012
|
-
AND COALESCE(injection_count, 0) = 0
|
|
2013
|
-
AND created_at_epoch < ?
|
|
2014
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
2015
|
-
)
|
|
2016
|
-
`).run(staleAge, ...baseParams);
|
|
2017
|
-
|
|
2018
|
-
// Mark importance=1, never-accessed, never-injected, old → pending-purge.
|
|
2019
|
-
const idleMarked = db.prepare(`
|
|
2020
|
-
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
2021
|
-
WHERE id IN (
|
|
2022
|
-
SELECT id FROM observations
|
|
2023
|
-
WHERE COALESCE(compressed_into, 0) = 0
|
|
2024
|
-
AND COALESCE(importance, 1) = 1
|
|
2025
|
-
AND COALESCE(access_count, 0) = 0
|
|
2026
|
-
AND COALESCE(injection_count, 0) = 0
|
|
2027
|
-
AND created_at_epoch < ?
|
|
2028
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
2029
|
-
)
|
|
2030
|
-
`).run(staleAge, ...baseParams);
|
|
2031
|
-
const decayCap = (decayed.changes >= OP_CAP || idleMarked.changes >= OP_CAP) ? ' (cap reached, re-run for more)' : '';
|
|
2032
|
-
results.push(`Decayed ${decayed.changes} stale observations, marked ${idleMarked.changes} idle as pending-purge${decayCap}`);
|
|
1900
|
+
// injection_count>0 protected (maintain-core; shared with MCP + hook auto-maintain).
|
|
1901
|
+
const { decayed, idleMarked } = decayAndMarkIdle(db, mctx);
|
|
1902
|
+
const decayCap = (decayed >= OP_CAP || idleMarked >= OP_CAP) ? ' (cap reached, re-run for more)' : '';
|
|
1903
|
+
results.push(`Decayed ${decayed} stale observations, marked ${idleMarked} idle as pending-purge${decayCap}`);
|
|
2033
1904
|
}
|
|
2034
1905
|
|
|
2035
1906
|
if (ops.includes('demote_pinned')) {
|
|
2036
|
-
// Repair the citation-decay blind spot:
|
|
2037
|
-
//
|
|
2038
|
-
//
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
// importance to 1 in a SINGLE pass. Injection priority is binary
|
|
2042
|
-
// (importance >= 2 → full weight; hook-memory.mjs), so a gentle 3→2 step
|
|
2043
|
-
// would leave the obs dominating injection just the same; only reaching 1
|
|
2044
|
-
// actually de-ranks it. Floors at 1 (not 0/purge) so a later boost (access)
|
|
2045
|
-
// or a genuine cite can still rescue a useful entry.
|
|
2046
|
-
const demoted = db.prepare(`
|
|
2047
|
-
UPDATE observations SET importance = 1
|
|
2048
|
-
WHERE id IN (
|
|
2049
|
-
SELECT id FROM observations
|
|
2050
|
-
WHERE COALESCE(compressed_into, 0) = 0
|
|
2051
|
-
AND COALESCE(injection_count, 0) >= ${PINNED_INJ_THRESHOLD}
|
|
2052
|
-
AND COALESCE(cited_count, 0) = 0
|
|
2053
|
-
AND COALESCE(importance, 1) > 1
|
|
2054
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
2055
|
-
)
|
|
2056
|
-
`).run(...baseParams);
|
|
2057
|
-
results.push(`Demoted ${demoted.changes} pinned-but-uncited observations to importance 1 (inj>=${PINNED_INJ_THRESHOLD}, cited=0)${capHint(demoted.changes)}`);
|
|
1907
|
+
// Repair the citation-decay blind spot: decay protects injection_count>0, so a
|
|
1908
|
+
// heavily-injected-but-uncited memory stays pinned at max importance forever.
|
|
1909
|
+
// demotePinned (maintain-core) drops it to 1 in one pass. Floor 1, not purge.
|
|
1910
|
+
const demoted = demotePinned(db, mctx);
|
|
1911
|
+
results.push(`Demoted ${demoted} pinned-but-uncited observations to importance 1 (inj>=${PINNED_INJ_THRESHOLD}, cited=0)${capHint(demoted)}`);
|
|
2058
1912
|
}
|
|
2059
1913
|
|
|
2060
1914
|
if (ops.includes('boost')) {
|
|
2061
|
-
const boosted = db
|
|
2062
|
-
|
|
2063
|
-
WHERE id IN (
|
|
2064
|
-
SELECT id FROM observations
|
|
2065
|
-
WHERE COALESCE(compressed_into, 0) = 0
|
|
2066
|
-
AND COALESCE(access_count, 0) > 3
|
|
2067
|
-
AND COALESCE(importance, 1) < 3
|
|
2068
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
2069
|
-
)
|
|
2070
|
-
`).run(...baseParams);
|
|
2071
|
-
results.push(`Boosted ${boosted.changes} frequently-accessed observations${capHint(boosted.changes)}`);
|
|
1915
|
+
const boosted = boostAccessed(db, mctx);
|
|
1916
|
+
results.push(`Boosted ${boosted} frequently-accessed observations${capHint(boosted)}`);
|
|
2072
1917
|
}
|
|
2073
1918
|
|
|
2074
1919
|
if (ops.includes('dedup') && flags['merge-ids']) {
|
|
2075
|
-
// Parse
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
let totalMerged = 0;
|
|
1920
|
+
// Parse "keepId:removeId1:removeId2,keepId2:removeId3"; surface malformed segments
|
|
1921
|
+
// (non-numeric / single-element) instead of silently dropping them. The merge SQL
|
|
1922
|
+
// itself lives in maintain-core mergeDuplicates (shared with MCP).
|
|
2079
1923
|
const invalidSegments = [];
|
|
2080
|
-
const
|
|
2081
|
-
const
|
|
2082
|
-
for (const seg of rawSegments) {
|
|
1924
|
+
const groups = [];
|
|
1925
|
+
for (const seg of flags['merge-ids'].split(',').map(s => s.trim()).filter(Boolean)) {
|
|
2083
1926
|
const parts = seg.split(':').map(s => s.trim());
|
|
2084
1927
|
const nums = parts.map(p => Number(p));
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
const [keepId, ...removeIds] = nums;
|
|
2088
|
-
for (const removeId of removeIds) {
|
|
2089
|
-
totalMerged += mergeStmt.run(keepId, removeId).changes;
|
|
2090
|
-
}
|
|
1928
|
+
if (parts.length < 2 || nums.some(n => !Number.isFinite(n) || n <= 0)) { invalidSegments.push(seg); continue; }
|
|
1929
|
+
groups.push(nums);
|
|
2091
1930
|
}
|
|
1931
|
+
const totalMerged = mergeDuplicates(db, groups);
|
|
2092
1932
|
if (invalidSegments.length) {
|
|
2093
1933
|
results.push(`Warning: ignored ${invalidSegments.length} malformed --merge-ids segment(s): ${invalidSegments.join(', ')} (expected keepId:removeId[:removeId...] with positive integers)`);
|
|
2094
1934
|
}
|
|
@@ -2107,11 +1947,7 @@ function cmdMaintain(db, args) {
|
|
|
2107
1947
|
// --confirm so a mis-typed `maintain execute --ops purge_stale` can't wipe rows silently.
|
|
2108
1948
|
const confirmed = flags.confirm === true || flags.confirm === 'true';
|
|
2109
1949
|
if (!confirmed) {
|
|
2110
|
-
const previewRow = db
|
|
2111
|
-
SELECT COUNT(*) AS candidates, MIN(created_at_epoch) AS oldest, MAX(created_at_epoch) AS newest
|
|
2112
|
-
FROM observations
|
|
2113
|
-
WHERE compressed_into = ${COMPRESSED_PENDING_PURGE} AND created_at_epoch < ? ${projectFilter}
|
|
2114
|
-
`).get(retainCutoff, ...baseParams);
|
|
1950
|
+
const previewRow = purgeStalePreview(db, mctx, retainCutoff);
|
|
2115
1951
|
const pushLines = [`purge_stale preview (no --confirm):`,
|
|
2116
1952
|
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`];
|
|
2117
1953
|
if (previewRow.candidates > 0) {
|
|
@@ -2121,14 +1957,8 @@ function cmdMaintain(db, args) {
|
|
|
2121
1957
|
pushLines.push(` To delete, re-run with --confirm.`);
|
|
2122
1958
|
results.push(pushLines.join('\n'));
|
|
2123
1959
|
} else {
|
|
2124
|
-
const purged = db
|
|
2125
|
-
|
|
2126
|
-
SELECT id FROM observations
|
|
2127
|
-
WHERE compressed_into = ${COMPRESSED_PENDING_PURGE} AND created_at_epoch < ?
|
|
2128
|
-
${projectFilter} LIMIT ${OP_CAP}
|
|
2129
|
-
)
|
|
2130
|
-
`).run(retainCutoff, ...baseParams);
|
|
2131
|
-
results.push(`Purged ${purged.changes} stale observations (retained last ${retainDays} days)${capHint(purged.changes)}`);
|
|
1960
|
+
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1961
|
+
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)${capHint(purged)}`);
|
|
2132
1962
|
}
|
|
2133
1963
|
}
|
|
2134
1964
|
})();
|
|
@@ -2137,52 +1967,24 @@ function cmdMaintain(db, args) {
|
|
|
2137
1967
|
db.exec("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
|
|
2138
1968
|
results.push('FTS5 index optimized');
|
|
2139
1969
|
|
|
2140
|
-
// rebuild_vectors: outside main transaction (
|
|
1970
|
+
// rebuild_vectors: outside main transaction (maintain-core, shared with MCP).
|
|
2141
1971
|
if (ops.includes('rebuild_vectors')) {
|
|
2142
1972
|
try {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
} else {
|
|
2148
|
-
const allObs = db.prepare(`
|
|
2149
|
-
SELECT id, title, narrative, concepts FROM observations
|
|
2150
|
-
WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
|
|
2151
|
-
`).all();
|
|
2152
|
-
let updated = 0;
|
|
2153
|
-
const insertStmt = db.prepare('INSERT OR REPLACE INTO observation_vectors (observation_id, vector, vocab_version, created_at_epoch) VALUES (?, ?, ?, ?)');
|
|
2154
|
-
const vecNow = Date.now();
|
|
2155
|
-
db.transaction(() => {
|
|
2156
|
-
db.prepare('DELETE FROM observation_vectors').run();
|
|
2157
|
-
for (const obs of allObs) {
|
|
2158
|
-
const text = [obs.title || '', obs.narrative || '', obs.concepts || ''].filter(Boolean).join(' ');
|
|
2159
|
-
const vec = computeVector(text, vocab);
|
|
2160
|
-
if (vec) {
|
|
2161
|
-
insertStmt.run(obs.id, Buffer.from(vec.buffer), vocab.version, vecNow);
|
|
2162
|
-
updated++;
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
})();
|
|
2166
|
-
results.push(`Vectors: rebuilt vocabulary (${vocab.terms.size} terms), updated ${updated}/${allObs.length} vectors`);
|
|
2167
|
-
}
|
|
1973
|
+
const r = rebuildVectors(db);
|
|
1974
|
+
results.push(r.ok
|
|
1975
|
+
? `Vectors: rebuilt vocabulary (${r.terms} terms), updated ${r.updated}/${r.total} vectors`
|
|
1976
|
+
: `Vectors: ${r.reason}`);
|
|
2168
1977
|
} catch (e) {
|
|
2169
1978
|
results.push(`Vectors: rebuild failed — ${e.message}`);
|
|
2170
1979
|
}
|
|
2171
1980
|
}
|
|
2172
1981
|
|
|
2173
|
-
// vacuum: reclaim freelist pages left
|
|
2174
|
-
//
|
|
2175
|
-
// VACUUM, which is absent everywhere else (auto_vacuum=0). Must run OUTSIDE any
|
|
2176
|
-
// transaction. Whole-DB regardless of --project. Reports freelist before/after
|
|
2177
|
-
// as the §7 reclaim metric.
|
|
1982
|
+
// vacuum: reclaim freelist pages left by DELETEs. Whole-DB, outside any transaction.
|
|
1983
|
+
// maintain-core, shared with MCP. Reports freelist before/after as the §7 reclaim metric.
|
|
2178
1984
|
if (ops.includes('vacuum')) {
|
|
2179
1985
|
try {
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
db.exec('VACUUM');
|
|
2183
|
-
const freeAfter = db.pragma('freelist_count', { simple: true });
|
|
2184
|
-
const reclaimedMB = ((Math.max(0, freeBefore - freeAfter) * pageSize) / 1048576).toFixed(1);
|
|
2185
|
-
results.push(`VACUUM: reclaimed ~${reclaimedMB}MB (freelist ${freeBefore} → ${freeAfter} pages)`);
|
|
1986
|
+
const v = vacuum(db);
|
|
1987
|
+
results.push(`VACUUM: reclaimed ~${v.reclaimedMB}MB (freelist ${v.freeBefore} → ${v.freeAfter} pages)`);
|
|
2186
1988
|
} catch (e) {
|
|
2187
1989
|
results.push(`VACUUM failed — ${e.message}`);
|
|
2188
1990
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.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. Alternative to claude-mem with 600x lower cost.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"lib/binding-probe.mjs",
|
|
68
68
|
"lib/mem-override.mjs",
|
|
69
69
|
"lib/save-observation.mjs",
|
|
70
|
+
"lib/compress-core.mjs",
|
|
71
|
+
"lib/maintain-core.mjs",
|
|
70
72
|
"lib/deferred-work.mjs",
|
|
71
73
|
"lib/upgrade-banner.mjs",
|
|
72
74
|
"lib/scrub-record.mjs",
|