claude-mem-lite 3.91.0 → 3.92.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 +1 -0
- package/README.zh-CN.md +1 -0
- package/claudemd.mjs +9 -6
- package/hook-context.mjs +9 -8
- package/hook-llm.mjs +56 -28
- package/hook-optimize.mjs +47 -28
- package/hook-shared.mjs +56 -11
- package/hook.mjs +94 -25
- package/install.mjs +9 -2
- package/lib/atomic-write.mjs +14 -3
- package/lib/cite-back-hint.mjs +3 -3
- package/lib/cite-recall-path.mjs +52 -0
- package/lib/compress-core.mjs +31 -15
- package/lib/frontmatter.mjs +67 -0
- package/lib/injected-ids.mjs +93 -4
- package/lib/maintain-core.mjs +191 -5
- package/lib/observation-write.mjs +71 -13
- package/lib/registry-core.mjs +117 -1
- package/lib/save-enrich.mjs +10 -6
- package/lib/save-observation.mjs +44 -0
- package/lib/transcript-scan.mjs +59 -8
- package/mem-cli.mjs +75 -193
- package/memdir.mjs +6 -6
- package/npm-shrinkwrap.json +20 -2
- package/package.json +9 -1
- package/registry-importer.mjs +4 -34
- package/registry.mjs +2 -1
- package/scripts/post-tool-recall.js +12 -4
- package/scripts/pre-tool-recall.js +9 -33
- package/scripts/user-prompt-search.js +13 -36
- package/search-scoring.mjs +10 -0
- package/server.mjs +68 -170
- package/source-files.mjs +2 -0
package/mem-cli.mjs
CHANGED
|
@@ -21,18 +21,17 @@ import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, OBS_FIELDS, SESSIO
|
|
|
21
21
|
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
22
22
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
23
23
|
import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
24
|
-
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources } from './lib/registry-core.mjs';
|
|
24
|
+
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichResourceRow, enrichImportedResources, enrichNamedResource, REGISTRY_CONFINE_ENV } from './lib/registry-core.mjs';
|
|
25
25
|
import { searchResources } from './registry-retriever.mjs';
|
|
26
26
|
import { computeFunnel, formatFunnel, computeSweep, formatSweep, DEFAULT_SWEEP_FLOORS, DEFAULT_SWEEP_MARGINS } from './registry-recommend.mjs';
|
|
27
27
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
recoverOrphanedChildren, recoverBuriedLessons, sweepDeferredWorkOrphans,
|
|
31
|
-
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
32
|
-
hardDeleteCandidateCount,
|
|
29
|
+
runMaintainOps, findDuplicates, maintenanceStats,
|
|
33
30
|
OP_CAP, STALE_AGE_MS, PINNED_INJ_THRESHOLD, resolveDefaultMaintainOps,
|
|
34
31
|
} from './lib/maintain-core.mjs';
|
|
35
|
-
|
|
32
|
+
// snapshotDb left with maintain-core: the pre-maintain snapshot is part of the op ORDER
|
|
33
|
+
// (it must see the pre-existing pending rows), so it moved into runMaintainOps (P1-5).
|
|
34
|
+
import { listSnapshots, backupBudgetBytes } from './lib/db-backup.mjs';
|
|
36
35
|
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
37
36
|
import { OBS_TYPE_SET } from './lib/obs-types.mjs';
|
|
38
37
|
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
@@ -55,7 +54,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
|
55
54
|
import { isNativeBindingError, healAndReexec } from './lib/binding-probe.mjs';
|
|
56
55
|
import { CLI_PATH, CLI_INVOKE } from './cli-path.mjs';
|
|
57
56
|
import { parseArgs, out, outVerbatim, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, resolvePositionalAlias, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cli/common.mjs';
|
|
58
|
-
import { saveObservation, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
|
|
57
|
+
import { saveObservation, saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
|
|
59
58
|
import { normalizeScope, insertObservationVector, applyObsUpdate } from './lib/observation-write.mjs';
|
|
60
59
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
61
60
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
@@ -81,7 +80,7 @@ const SURFACE_LABELS = {
|
|
|
81
80
|
import { aggregateMetrics, readMetrics } from './lib/metrics.mjs';
|
|
82
81
|
import {
|
|
83
82
|
insertDeferred, listOpenWithOrdinal, dropDeferred, formatDropReasonHint,
|
|
84
|
-
resolveDeferredIds,
|
|
83
|
+
resolveDeferredIds,
|
|
85
84
|
getDeferredByIds, formatDeferredDetail,
|
|
86
85
|
searchDeferredWork, formatDeferredSearchTrailer,
|
|
87
86
|
formatDeferListRow, countStaleOpen, formatDeferStaleHint,
|
|
@@ -970,36 +969,21 @@ function cmdSave(db, args) {
|
|
|
970
969
|
}
|
|
971
970
|
}
|
|
972
971
|
|
|
973
|
-
let result;
|
|
974
|
-
let closesIds = null;
|
|
972
|
+
let result, closesIds;
|
|
975
973
|
try {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
// Resolving deferred ids only on the non-duplicate path keeps repeated
|
|
990
|
-
// save commands (with the same --closes-deferred) idempotent even after
|
|
991
|
-
// the deferred row has transitioned out of 'open'.
|
|
992
|
-
if (r.kind === 'duplicate') return r;
|
|
993
|
-
if (closesTokens) {
|
|
994
|
-
// D#195: the close verb accepts a 'dropped' row and converts it to
|
|
995
|
-
// 'done'. `defer drop` used on an item that was actually fixed was
|
|
996
|
-
// otherwise a one-way gate, permanently losing the obs link. Kept in
|
|
997
|
-
// sync with the same policy in server.mjs mem_save.
|
|
998
|
-
closesIds = resolveDeferredIds(db, project, closesTokens, { allowStatuses: ['open', 'dropped'] });
|
|
999
|
-
closeDeferredItems(db, closesIds, r.id);
|
|
1000
|
-
}
|
|
1001
|
-
return r;
|
|
1002
|
-
})();
|
|
974
|
+
// The transaction body — dedup short-circuit BEFORE the resolver, and D#195's
|
|
975
|
+
// allowStatuses — is lib/save-observation.mjs's (audit 2026-09-02 P1-6). Both faces
|
|
976
|
+
// used to write it out and each carried a "kept in sync with the other" comment.
|
|
977
|
+
({ result, closesIds } = saveWithClosures(db, {
|
|
978
|
+
content: text,
|
|
979
|
+
title: flags.title,
|
|
980
|
+
type,
|
|
981
|
+
importance: rawImp,
|
|
982
|
+
project,
|
|
983
|
+
files: saveFiles,
|
|
984
|
+
lesson_learned: rawLesson,
|
|
985
|
+
supersedes: supersedesIds || undefined,
|
|
986
|
+
}, { closesTokens, project }));
|
|
1003
987
|
} catch (e) {
|
|
1004
988
|
if (closesTokens) {
|
|
1005
989
|
fail(`[mem] save with --closes-deferred failed: ${e.message}`);
|
|
@@ -2084,10 +2068,6 @@ function cmdMaintain(db, args) {
|
|
|
2084
2068
|
}
|
|
2085
2069
|
const staleAge = Date.now() - STALE_AGE_MS;
|
|
2086
2070
|
const mctx = { projectFilter, baseParams, staleAge, opCap: OP_CAP };
|
|
2087
|
-
const results = [];
|
|
2088
|
-
|
|
2089
|
-
// T2-P1-B: surface the OP_CAP hit so users know to re-run, matching MCP mem_maintain.
|
|
2090
|
-
const capHint = (changes) => (changes >= OP_CAP ? ' (cap reached, re-run for more)' : '');
|
|
2091
2071
|
|
|
2092
2072
|
// Parse + validate --retain-days BEFORE the transaction so an invalid value rejects the
|
|
2093
2073
|
// whole command atomically. The old code validated inside db.transaction() with a bare
|
|
@@ -2108,134 +2088,40 @@ function cmdMaintain(db, args) {
|
|
|
2108
2088
|
// purge_stale is the only DELETE here — require --confirm so a mis-typed run can't wipe rows.
|
|
2109
2089
|
const confirmed = flags.confirm === true || flags.confirm === 'true';
|
|
2110
2090
|
|
|
2111
|
-
//
|
|
2112
|
-
//
|
|
2113
|
-
//
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
if (ops.includes('cleanup')) {
|
|
2144
|
-
const deleted = cleanupBroken(db, mctx);
|
|
2145
|
-
results.push(`Cleaned up ${deleted} broken observations${capHint(deleted)}`);
|
|
2146
|
-
// Self-heal legacy orphans (keeper hard-deleted pre-recoverChildrenOf): resurface
|
|
2147
|
-
// unreachable children. Non-destructive — un-hide only, no delete.
|
|
2148
|
-
const orphans = recoverOrphanedChildren(db, mctx);
|
|
2149
|
-
if (orphans > 0) results.push(`Recovered ${orphans} orphaned compression children`);
|
|
2150
|
-
// Heal lesson rows citation-decay buried at importance 0 (pre floor=1). 0→1 on
|
|
2151
|
-
// lesson-bearing rows only; idempotent no-op once none remain.
|
|
2152
|
-
const lessonsHealed = recoverBuriedLessons(db, mctx);
|
|
2153
|
-
if (lessonsHealed > 0) results.push(`Healed ${lessonsHealed} lesson rows buried at importance 0`);
|
|
2154
|
-
// Heal deferred_work rows whose closing obs / source prompt was hard-deleted while FK was
|
|
2155
|
-
// OFF (dangling ref foreign_key_check flags). Applies the ON DELETE SET NULL the FK would.
|
|
2156
|
-
const deferredHealed = sweepDeferredWorkOrphans(db, mctx);
|
|
2157
|
-
if (deferredHealed > 0) results.push(`Healed ${deferredHealed} deferred-work rows with dangling references`);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
if (ops.includes('decay')) {
|
|
2161
|
-
// injection_count>0 protected (maintain-core; shared with MCP + hook auto-maintain).
|
|
2162
|
-
const { decayed, idleMarked } = decayAndMarkIdle(db, mctx);
|
|
2163
|
-
const decayCap = (decayed >= OP_CAP || idleMarked >= OP_CAP) ? ' (cap reached, re-run for more)' : '';
|
|
2164
|
-
results.push(`Decayed ${decayed} stale observations, marked ${idleMarked} idle as pending-purge${decayCap}`);
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
if (ops.includes('boost')) {
|
|
2168
|
-
const boosted = boostAccessed(db, mctx);
|
|
2169
|
-
results.push(`Boosted ${boosted} frequently-accessed observations${capHint(boosted)}`);
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// AFTER boost, matching server.mjs and hook.mjs. This block used to sit BEFORE
|
|
2173
|
-
// it, and the order was load-bearing in the wrong direction: boostAccessed lifts
|
|
2174
|
-
// any access_count>3 row with importance<3, so demoting a pinned row to 1 and
|
|
2175
|
-
// then boosting handed it straight back at 2 — the demotion silently undone
|
|
2176
|
-
// inside a single maintain run. DEFAULT_MAINTAIN_OPS pins the order; this block
|
|
2177
|
-
// has to physically follow the boost block for that order to be real.
|
|
2178
|
-
if (ops.includes('demote_pinned')) {
|
|
2179
|
-
// Repair the citation-decay blind spot: decay protects injection_count>0, so a
|
|
2180
|
-
// heavily-injected-but-uncited memory stays pinned at max importance forever.
|
|
2181
|
-
// demotePinned (maintain-core) floors it in one pass: no lesson_learned -> 1,
|
|
2182
|
-
// lesson-bearing -> 2 (v3.76.1 dual floor). Floor, not purge.
|
|
2183
|
-
const demoted = demotePinned(db, mctx);
|
|
2184
|
-
results.push(`Demoted ${demoted} pinned-but-uncited observations (inj>=${PINNED_INJ_THRESHOLD}, cited=0; no lesson → importance 1, lesson → 2)${capHint(demoted)}`);
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
if (ops.includes('dedup') && flags['merge-ids']) {
|
|
2188
|
-
// Parse "keepId:removeId1:removeId2,keepId2:removeId3"; surface malformed segments
|
|
2189
|
-
// (non-numeric / single-element) instead of silently dropping them. The merge SQL
|
|
2190
|
-
// itself lives in maintain-core mergeDuplicates (shared with MCP).
|
|
2191
|
-
const invalidSegments = [];
|
|
2192
|
-
const groups = [];
|
|
2193
|
-
for (const seg of flags['merge-ids'].split(',').map(s => s.trim()).filter(Boolean)) {
|
|
2194
|
-
const parts = seg.split(':').map(s => s.trim());
|
|
2195
|
-
const nums = parts.map(p => Number(p));
|
|
2196
|
-
if (parts.length < 2 || nums.some(n => !Number.isFinite(n) || n <= 0)) { invalidSegments.push(seg); continue; }
|
|
2197
|
-
groups.push(nums);
|
|
2091
|
+
// Parse "keepId:removeId1:removeId2,keepId2:removeId3"; surface malformed segments
|
|
2092
|
+
// (non-numeric / single-element) instead of silently dropping them. The merge SQL and the
|
|
2093
|
+
// op ORDER both live in maintain-core (shared with MCP, audit 2026-09-02 P1-5) — this is
|
|
2094
|
+
// the one genuinely CLI-shaped part, because the MCP tool receives groups already typed.
|
|
2095
|
+
const invalidMergeSegments = [];
|
|
2096
|
+
const mergeGroups = [];
|
|
2097
|
+
if (flags['merge-ids']) {
|
|
2098
|
+
for (const seg of String(flags['merge-ids']).split(',').map(x => x.trim()).filter(Boolean)) {
|
|
2099
|
+
const parts = seg.split(':').map(x => x.trim());
|
|
2100
|
+
const nums = parts.map(x => Number(x));
|
|
2101
|
+
if (parts.length < 2 || nums.some(n => !Number.isFinite(n) || n <= 0)) { invalidMergeSegments.push(seg); continue; }
|
|
2102
|
+
mergeGroups.push(nums);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const results = runMaintainOps(db, mctx, ops, {
|
|
2107
|
+
retainDays,
|
|
2108
|
+
retainCutoff,
|
|
2109
|
+
confirmed,
|
|
2110
|
+
mergeGroups,
|
|
2111
|
+
mergeIdsProvided: Boolean(flags['merge-ids']),
|
|
2112
|
+
invalidMergeSegments,
|
|
2113
|
+
mergeIdsFlagName: '--merge-ids',
|
|
2114
|
+
renderPurgePreview: (previewRow) => {
|
|
2115
|
+
const lines = ['purge_stale preview (no --confirm):',
|
|
2116
|
+
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`];
|
|
2117
|
+
if (previewRow.candidates > 0) {
|
|
2118
|
+
lines.push(` Oldest: ${new Date(previewRow.oldest).toISOString().slice(0, 10)}`);
|
|
2119
|
+
lines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
2198
2120
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
results.push(`Merged ${totalMerged} duplicate observations`);
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// T2-P1-B parity with MCP: warn when merge-ids is provided but dedup wasn't requested.
|
|
2207
|
-
if (!ops.includes('dedup') && flags['merge-ids']) {
|
|
2208
|
-
results.push('Warning: --merge-ids provided but "dedup" not in operations — merge-ids ignored');
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
})();
|
|
2212
|
-
|
|
2213
|
-
// FTS optimize
|
|
2214
|
-
db.exec("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
|
|
2215
|
-
results.push('FTS5 index optimized');
|
|
2216
|
-
|
|
2217
|
-
// rebuild_vectors: outside main transaction (maintain-core, shared with MCP).
|
|
2218
|
-
if (ops.includes('rebuild_vectors')) {
|
|
2219
|
-
try {
|
|
2220
|
-
const r = rebuildVectors(db);
|
|
2221
|
-
results.push(r.ok
|
|
2222
|
-
? `Vectors: rebuilt vocabulary (${r.terms} terms), updated ${r.updated}/${r.total} vectors`
|
|
2223
|
-
: `Vectors: ${r.reason}`);
|
|
2224
|
-
} catch (e) {
|
|
2225
|
-
results.push(`Vectors: rebuild failed — ${e.message}`);
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// vacuum: reclaim freelist pages left by DELETEs. Whole-DB, outside any transaction.
|
|
2230
|
-
// maintain-core, shared with MCP. Reports freelist before/after as the §7 reclaim metric.
|
|
2231
|
-
if (ops.includes('vacuum')) {
|
|
2232
|
-
try {
|
|
2233
|
-
const v = vacuum(db);
|
|
2234
|
-
results.push(`VACUUM: reclaimed ~${v.reclaimedMB}MB (freelist ${v.freeBefore} → ${v.freeAfter} pages)`);
|
|
2235
|
-
} catch (e) {
|
|
2236
|
-
results.push(`VACUUM failed — ${e.message}`);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2121
|
+
lines.push(' To delete, re-run with --confirm.');
|
|
2122
|
+
return lines.join('\n');
|
|
2123
|
+
},
|
|
2124
|
+
});
|
|
2239
2125
|
|
|
2240
2126
|
out(`[mem] ${results.join('\n[mem] ')}`);
|
|
2241
2127
|
}
|
|
@@ -2844,7 +2730,7 @@ Commands:
|
|
|
2844
2730
|
--merge-ids K:R,... For dedup: keepId:removeId pairs (e.g. 10:11,20:21:22)
|
|
2845
2731
|
--project P Filter by project
|
|
2846
2732
|
--retain-days N For purge_stale: keep last N days (default 30)
|
|
2847
|
-
demote_pinned: floors importance for inj
|
|
2733
|
+
demote_pinned: floors importance for inj>=${PINNED_INJ_THRESHOLD} & cited=0 — to 1 with no
|
|
2848
2734
|
lesson_learned, to 2 with one (clears pinned noise; a lesson-bearing
|
|
2849
2735
|
row keeps eligibility on every importance>=2 injection face).
|
|
2850
2736
|
In the default set since v3.76.0; runs AFTER boost, which would
|
|
@@ -2993,17 +2879,9 @@ async function cmdImport(argv) {
|
|
|
2993
2879
|
if (flags.enrich) {
|
|
2994
2880
|
out('[mem] Running LLM enrichment...');
|
|
2995
2881
|
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
2996
|
-
|
|
2997
|
-
for (const r of results) {
|
|
2998
|
-
const row = rdb.prepare('SELECT local_path FROM resources WHERE id = ?').get(r.id);
|
|
2999
|
-
if (!row?.local_path) continue;
|
|
3000
|
-
try {
|
|
3001
|
-
const content = readFileSync(row.local_path, 'utf8');
|
|
3002
|
-
const ok = await enrichResource(rdb, r.name, r.type, content);
|
|
3003
|
-
if (ok) enriched++;
|
|
3004
|
-
} catch {}
|
|
3005
|
-
}
|
|
2882
|
+
const { ok: enriched, denied } = await enrichImportedResources(rdb, results, { confineTo: DB_DIR, enrichResource });
|
|
3006
2883
|
out(`[mem] Enriched ${enriched}/${results.length} resources.`);
|
|
2884
|
+
if (denied) out(`[mem] Refused ${denied}: local_path outside the managed directory (${REGISTRY_CONFINE_ENV}=off to override).`);
|
|
3007
2885
|
}
|
|
3008
2886
|
} catch (e) {
|
|
3009
2887
|
fail(`[mem] Import failed: ${e.message}`);
|
|
@@ -3121,24 +2999,28 @@ async function cmdEnrich(argv) {
|
|
|
3121
2999
|
const rows = rdb.prepare("SELECT name, type, local_path FROM resources WHERE status = 'active' AND (enrichment_status IS NULL OR enrichment_status = 'failed')").all();
|
|
3122
3000
|
if (rows.length === 0) { out('[mem] All resources already enriched.'); return; }
|
|
3123
3001
|
out(`[mem] Enriching ${rows.length} resources...`);
|
|
3124
|
-
let ok = 0, failCount = 0;
|
|
3002
|
+
let ok = 0, failCount = 0, denied = 0;
|
|
3125
3003
|
for (const r of rows) {
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3004
|
+
const { status, error } = await enrichResourceRow(rdb, r, { confineTo: DB_DIR, enrichResource });
|
|
3005
|
+
if (status === 'enriched') ok++;
|
|
3006
|
+
else if (status === 'denied') denied++;
|
|
3007
|
+
else failCount++;
|
|
3008
|
+
// Pace exactly where the pre-refactor loop did: after an enrichResource call that
|
|
3009
|
+
// RETURNED. A missing path, an unreadable file, a gate refusal and a throw out of
|
|
3010
|
+
// enrichResource all skipped the sleep before, and still do — `error` present on a
|
|
3011
|
+
// 'failed' row is what distinguishes "returned false" from "threw".
|
|
3012
|
+
const apiReturned = status === 'enriched' || (status === 'failed' && !error);
|
|
3013
|
+
if (apiReturned && !flags.batch) await new Promise(resolve => setTimeout(resolve, 500));
|
|
3133
3014
|
}
|
|
3134
3015
|
out(`[mem] Done: ${ok} enriched, ${failCount} failed.`);
|
|
3016
|
+
if (denied) out(`[mem] Refused ${denied}: local_path outside the managed directory (${REGISTRY_CONFINE_ENV}=off to override).`);
|
|
3135
3017
|
} else if (name) {
|
|
3136
|
-
const
|
|
3137
|
-
if (
|
|
3138
|
-
if (
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
out(
|
|
3018
|
+
const { status, error } = await enrichNamedResource(rdb, name, { confineTo: DB_DIR, enrichResource });
|
|
3019
|
+
if (status === 'not-found') { fail(`[mem] Resource not found: ${name}`); return; }
|
|
3020
|
+
if (status === 'no-path') { fail(`[mem] No local_path for ${name}`); return; }
|
|
3021
|
+
if (status === 'denied') { fail(`[mem] Access denied: local_path outside the managed directory (${REGISTRY_CONFINE_ENV}=off to override).`); return; }
|
|
3022
|
+
if (status === 'unreadable') { fail(`[mem] Enrich error: ${error?.message || 'cannot read local_path'}`); return; }
|
|
3023
|
+
out(status === 'enriched' ? `[mem] Enriched: ${name}` : `[mem] Enrichment failed for ${name}`);
|
|
3142
3024
|
} else {
|
|
3143
3025
|
fail('[mem] Usage: claude-mem-lite enrich <name> OR claude-mem-lite enrich --all [--batch]');
|
|
3144
3026
|
}
|
package/memdir.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// See docs/plans/2026-04-16-invited-memory-pattern.md for rationale.
|
|
11
11
|
|
|
12
|
-
import { readFileSync,
|
|
12
|
+
import { readFileSync, existsSync, unlinkSync, mkdirSync, readdirSync } from 'fs';
|
|
13
|
+
import { atomicWriteFileSync as atomicWrite } from './lib/atomic-write.mjs';
|
|
13
14
|
import { join } from 'path';
|
|
14
15
|
import { homedir } from 'os';
|
|
15
16
|
import { createHash } from 'crypto';
|
|
@@ -100,11 +101,10 @@ function canonicalBody(contentLine) {
|
|
|
100
101
|
|
|
101
102
|
function sha256(s) { return createHash('sha256').update(s).digest('hex'); }
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
104
|
+
// Third byte-identical copy of the same temp+rename, all three missing the same lstat
|
|
105
|
+
// (audit 2026-09-02 P0-5). MEMORY.md under a memory dir is a prime symlink candidate —
|
|
106
|
+
// users point it at a dotfiles repo so the same index follows them between machines —
|
|
107
|
+
// and renaming onto the link NAME severs it silently. See lib/atomic-write.mjs.
|
|
108
108
|
|
|
109
109
|
function readState(memdir, slug) {
|
|
110
110
|
const p = stateFile(memdir, slug);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.92.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.92.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@eslint/js": "^10.0.1",
|
|
20
20
|
"@vitest/coverage-v8": "^4.0.18",
|
|
21
|
+
"acorn": "8.16.0",
|
|
21
22
|
"eslint": "^10.0.0",
|
|
22
23
|
"fast-check": "^4.5.3",
|
|
23
24
|
"knip": "^6.12.1",
|
|
24
25
|
"picomatch": "^4.0.4",
|
|
26
|
+
"prettier": "3.6.2",
|
|
25
27
|
"vitest": "^4.0.18"
|
|
26
28
|
},
|
|
27
29
|
"engines": {
|
|
@@ -3796,6 +3798,22 @@
|
|
|
3796
3798
|
"node": ">= 0.8.0"
|
|
3797
3799
|
}
|
|
3798
3800
|
},
|
|
3801
|
+
"node_modules/prettier": {
|
|
3802
|
+
"version": "3.6.2",
|
|
3803
|
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
|
3804
|
+
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
|
3805
|
+
"dev": true,
|
|
3806
|
+
"license": "MIT",
|
|
3807
|
+
"bin": {
|
|
3808
|
+
"prettier": "bin/prettier.cjs"
|
|
3809
|
+
},
|
|
3810
|
+
"engines": {
|
|
3811
|
+
"node": ">=14"
|
|
3812
|
+
},
|
|
3813
|
+
"funding": {
|
|
3814
|
+
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
3815
|
+
}
|
|
3816
|
+
},
|
|
3799
3817
|
"node_modules/proxy-addr": {
|
|
3800
3818
|
"version": "2.0.7",
|
|
3801
3819
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.92.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",
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"lint": "eslint .",
|
|
19
|
+
"format": "prettier --write \"**/*.{mjs,js}\"",
|
|
20
|
+
"format:check": "prettier --check \"**/*.{mjs,js}\"",
|
|
19
21
|
"dead-code": "knip",
|
|
22
|
+
"audit:metrics": "node scripts/audit-metrics.mjs --md",
|
|
23
|
+
"audit:baseline": "node scripts/audit-metrics.mjs --run-tests --md",
|
|
20
24
|
"test": "vitest run",
|
|
21
25
|
"test:smoke": "vitest run tests/smoke.test.mjs",
|
|
22
26
|
"test:coverage": "vitest run --coverage",
|
|
@@ -65,6 +69,8 @@
|
|
|
65
69
|
"lib/edge-attribution.mjs",
|
|
66
70
|
"lib/file-edge-match.mjs",
|
|
67
71
|
"lib/cite-back-hint.mjs",
|
|
72
|
+
"lib/cite-recall-path.mjs",
|
|
73
|
+
"lib/frontmatter.mjs",
|
|
68
74
|
"lib/cooldown-path.mjs",
|
|
69
75
|
"lib/tmp-fixture-sweep.mjs",
|
|
70
76
|
"lib/summary-extractor.mjs",
|
|
@@ -201,10 +207,12 @@
|
|
|
201
207
|
"devDependencies": {
|
|
202
208
|
"@eslint/js": "^10.0.1",
|
|
203
209
|
"@vitest/coverage-v8": "^4.0.18",
|
|
210
|
+
"acorn": "8.16.0",
|
|
204
211
|
"eslint": "^10.0.0",
|
|
205
212
|
"fast-check": "^4.5.3",
|
|
206
213
|
"knip": "^6.12.1",
|
|
207
214
|
"picomatch": "^4.0.4",
|
|
215
|
+
"prettier": "3.6.2",
|
|
208
216
|
"vitest": "^4.0.18"
|
|
209
217
|
}
|
|
210
218
|
}
|
package/registry-importer.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { parseGitHubUrl, buildTreeUrl, buildContentUrl, buildRepoUrl, buildHeaders } from './registry-github.mjs';
|
|
6
6
|
import { upsertResource } from './registry.mjs';
|
|
7
7
|
import { debugLog, isPathConfined } from './utils.mjs';
|
|
8
|
+
import { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
8
9
|
import { createHash } from 'crypto';
|
|
9
10
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
11
|
import { join } from 'path';
|
|
@@ -98,40 +99,9 @@ export function discoverFromTree(treeData, pathFilter) {
|
|
|
98
99
|
* @param {string} content Full file content
|
|
99
100
|
* @returns {{ frontmatter: Record<string, any>, body: string }}
|
|
100
101
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const raw = match[1];
|
|
106
|
-
const body = content.slice(match[0].length).trim();
|
|
107
|
-
const fm = {};
|
|
108
|
-
let currentKey = null, currentValue = '', inMultiline = false;
|
|
109
|
-
|
|
110
|
-
for (const line of raw.split('\n')) {
|
|
111
|
-
if (inMultiline && (line.startsWith(' ') || line.startsWith('\t') || line.trim() === '')) {
|
|
112
|
-
currentValue += ' ' + line.trim();
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (inMultiline && currentKey) { fm[currentKey] = currentValue.trim(); inMultiline = false; }
|
|
116
|
-
|
|
117
|
-
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.*)/);
|
|
118
|
-
if (kv) {
|
|
119
|
-
currentKey = kv[1];
|
|
120
|
-
let val = kv[2].trim();
|
|
121
|
-
if (val === '|' || val === '>') { inMultiline = true; currentValue = ''; continue; }
|
|
122
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
123
|
-
try { fm[currentKey] = JSON.parse(val); } catch { fm[currentKey] = val; }
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))
|
|
127
|
-
val = val.slice(1, -1);
|
|
128
|
-
if (currentKey === 'description' && val) { inMultiline = true; currentValue = val; continue; }
|
|
129
|
-
fm[currentKey] = val;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (inMultiline && currentKey) fm[currentKey] = currentValue.trim();
|
|
133
|
-
return { frontmatter: fm, body };
|
|
134
|
-
}
|
|
102
|
+
// The parser itself is lib/frontmatter.mjs's (audit 2026-09-02 P1-16); re-exported here
|
|
103
|
+
// because callers and tests import it from this module.
|
|
104
|
+
export { parseFrontmatter };
|
|
135
105
|
|
|
136
106
|
// ─── Keyword Extraction ─────────────────────────────────────────────────────
|
|
137
107
|
|
package/registry.mjs
CHANGED
|
@@ -443,7 +443,8 @@ export function ensureRegistryDb(dbPath) {
|
|
|
443
443
|
return db;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
// ─── Exported Schema (
|
|
446
|
+
// ─── Exported Schema (test-helpers.mjs, and scripts/index-managed.mjs since P1-16, which
|
|
447
|
+
// rebuilds resources_fts from scratch and used to carry its own copy of both blocks) ──
|
|
447
448
|
|
|
448
449
|
export { RESOURCES_SCHEMA, FTS5_SCHEMA, TRIGGERS_SCHEMA, INVOCATIONS_SCHEMA, PREINSTALLED_SCHEMA };
|
|
449
450
|
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
// presentIdents excluded it); that class is carried by the pre-edit
|
|
11
11
|
// BIND_DIRECTIVE, not here. See the spec's component-2 limits.
|
|
12
12
|
//
|
|
13
|
-
// Safety: readonly, no DB, exit 0 always.
|
|
14
|
-
//
|
|
13
|
+
// Safety: readonly, no DB, exit 0 always. The cooldown path rule comes from
|
|
14
|
+
// lib/cooldown-path.mjs — it used to be inlined here "per the #8447 fast-path
|
|
15
|
+
// convention", but that exemption stopped applying to THIS script the moment it grew
|
|
16
|
+
// the four lib imports below (audit 2026-09-02 P1-1: the fourth copy; the previous
|
|
17
|
+
// round collapsed only three of four). A writer/reader disagreement on this name does
|
|
18
|
+
// not error — it reads a file nobody wrote, silently zeroing the bind-mode
|
|
19
|
+
// dropped-identifier check.
|
|
15
20
|
|
|
16
21
|
import { existsSync, readFileSync } from 'fs';
|
|
17
22
|
import { basename, join } from 'path';
|
|
@@ -22,6 +27,7 @@ import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
|
22
27
|
// emit added later merges instead of producing two JSON documents, which the host
|
|
23
28
|
// parses as neither (lib/hook-stdout.mjs). Import-free module over no runtime deps.
|
|
24
29
|
import { queueHookContext, flushHookStdout } from '../lib/hook-stdout.mjs';
|
|
30
|
+
import { cooldownPathFor as sharedCooldownPathFor } from '../lib/cooldown-path.mjs';
|
|
25
31
|
|
|
26
32
|
const SALIENCE_BIND = process.env.CLAUDE_MEM_SALIENCE === 'bind';
|
|
27
33
|
|
|
@@ -29,10 +35,12 @@ const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
|
29
35
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
|
|
30
36
|
const LEGACY_COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
31
37
|
|
|
38
|
+
// The no-session legacy fallback stays local: it is this script's own back-compat with
|
|
39
|
+
// pre-session-id cooldown files, not part of the shared naming rule. Same split as
|
|
40
|
+
// scripts/pre-tool-recall.js, which is the writer.
|
|
32
41
|
function cooldownPathFor(sessionId) {
|
|
33
42
|
if (!sessionId) return LEGACY_COOLDOWN_PATH;
|
|
34
|
-
|
|
35
|
-
return join(RUNTIME_DIR, `pre-recall-cooldown-${safe}.json`);
|
|
43
|
+
return sharedCooldownPathFor(RUNTIME_DIR, sessionId);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
async function main() {
|
|
@@ -8,7 +8,7 @@ import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
9
|
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
10
10
|
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
11
|
-
import { injectedIdsFileName, injectedIdKey, EVENT_ID_PREFIX } from '../lib/injected-ids.mjs';
|
|
11
|
+
import { injectedIdsFileName, injectedIdKey, EVENT_ID_PREFIX, readInjectedMarker, mergeInjectedMarker } from '../lib/injected-ids.mjs';
|
|
12
12
|
import { liveObsFilterSql } from '../lib/inject-search-core.mjs';
|
|
13
13
|
import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
|
|
14
14
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
@@ -253,45 +253,21 @@ function crossHookInjectedFile(project, sessionId) {
|
|
|
253
253
|
// read and merge now carry the CC session id: a payload written by a DIFFERENT
|
|
254
254
|
// session is ignored (read) / replaced (merge), mirroring the v3.35.2 episode
|
|
255
255
|
// session-key fix. Legacy payloads without `session` keep the old behavior.
|
|
256
|
+
// Predicate + payload shape live in lib/injected-ids.mjs (audit 2026-09-02 P1-2) — this
|
|
257
|
+
// script held two of the five hand-typed copies. The window stays this script's own
|
|
258
|
+
// constant; only the gate and the write are shared.
|
|
256
259
|
function readCrossHookInjected(project, sessionId) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (session && sessionId && session !== sessionId) return new Set();
|
|
261
|
-
if (!ts || Date.now() - ts > CROSS_HOOK_DEDUP_MS) return new Set();
|
|
262
|
-
if (!Array.isArray(ids)) return new Set();
|
|
263
|
-
return new Set(ids.map(String));
|
|
264
|
-
} catch { return new Set(); }
|
|
260
|
+
const { ids } = readInjectedMarker(crossHookInjectedFile(project, sessionId),
|
|
261
|
+
{ sessionId, maxAgeMs: CROSS_HOOK_DEDUP_MS });
|
|
262
|
+
return new Set(ids.map(String));
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
function mergeCrossHookInjected(project, newIds, sessionId) {
|
|
268
266
|
if (!newIds || newIds.length === 0) return;
|
|
269
267
|
try {
|
|
270
268
|
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const raw = readFileSync(file, 'utf8');
|
|
275
|
-
const parsed = JSON.parse(raw);
|
|
276
|
-
// Within the staleness window AND same session (or legacy): union.
|
|
277
|
-
// Outside / other session: replace.
|
|
278
|
-
if (parsed.ts && Date.now() - parsed.ts < CROSS_HOOK_DEDUP_MS
|
|
279
|
-
&& !(parsed.session && sessionId && parsed.session !== sessionId)) {
|
|
280
|
-
prev = parsed;
|
|
281
|
-
}
|
|
282
|
-
} catch { /* fresh file */ }
|
|
283
|
-
const ids = [...new Set([
|
|
284
|
-
...(Array.isArray(prev.ids) ? prev.ids.map(String) : []),
|
|
285
|
-
...newIds.map(String),
|
|
286
|
-
])];
|
|
287
|
-
// Atomic (tmp+rename, M-6): a plain write torn by a concurrent hook left the
|
|
288
|
-
// shared marker as invalid JSON, silently disabling cross-hook dedup.
|
|
289
|
-
atomicWriteFileSync(file, JSON.stringify({
|
|
290
|
-
ids,
|
|
291
|
-
ts: Date.now(),
|
|
292
|
-
count: (prev.count || 0) + 1,
|
|
293
|
-
...(sessionId ? { session: sessionId } : {}),
|
|
294
|
-
}));
|
|
269
|
+
mergeInjectedMarker(crossHookInjectedFile(project, sessionId), newIds,
|
|
270
|
+
{ sessionId, maxAgeMs: CROSS_HOOK_DEDUP_MS, mode: 'union' });
|
|
295
271
|
} catch { /* silent — dedup is best-effort */ }
|
|
296
272
|
}
|
|
297
273
|
|