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
|
@@ -15,11 +15,10 @@ import { join, sep } from 'path';
|
|
|
15
15
|
import { pathToFileURL } from 'url';
|
|
16
16
|
import Database from 'better-sqlite3';
|
|
17
17
|
import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, extractDeferredRefs, DEDUP_STALE_MS, matchRegistrySkillName, detectMemOverride } from './prompt-search-utils.mjs';
|
|
18
|
-
import { injectedIdsFileName } from '../lib/injected-ids.mjs';
|
|
18
|
+
import { injectedIdsFileName, mergeInjectedMarker } from '../lib/injected-ids.mjs';
|
|
19
19
|
import { getDeferredByIds } from '../lib/deferred-work.mjs';
|
|
20
20
|
import { recommendSkill } from '../registry-recommend.mjs';
|
|
21
21
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
22
|
-
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
23
22
|
|
|
24
23
|
import { DAY_MS } from '../lib/time-constants.mjs';
|
|
25
24
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
@@ -671,26 +670,12 @@ async function main() {
|
|
|
671
670
|
// window skips re-injection. A later FTS-path write replaces ids wholesale
|
|
672
671
|
// (accepted: worst case is one cheap re-injection after an obs-emitting
|
|
673
672
|
// prompt inside the same 5-min window).
|
|
673
|
+
// union: the M-6 same-session/staleness gate + the atomic write live in
|
|
674
|
+
// lib/injected-ids.mjs (audit 2026-09-02 P1-2). `dedupIds` are `D<id>` strings, so
|
|
675
|
+
// the lib's union-side String() is the identity here — same bytes as before.
|
|
674
676
|
try {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
try {
|
|
678
|
-
const prev = JSON.parse(readFileSync(injectedIdsFile, 'utf8'));
|
|
679
|
-
// M-6: inherit only same-session (or legacy) state — another session's
|
|
680
|
-
// ids/count must not carry over. Atomic write below: a torn concurrent
|
|
681
|
-
// write left the shared marker as invalid JSON (dedup silently off).
|
|
682
|
-
if (prev.ts && Date.now() - prev.ts < DEDUP_STALE_MS
|
|
683
|
-
&& !(prev.session && hookData.session_id && prev.session !== hookData.session_id)) {
|
|
684
|
-
prevIds = Array.isArray(prev.ids) ? prev.ids : [];
|
|
685
|
-
prevCount = prev.count || 0;
|
|
686
|
-
}
|
|
687
|
-
} catch {}
|
|
688
|
-
atomicWriteFileSync(injectedIdsFile, JSON.stringify({
|
|
689
|
-
ids: [...new Set([...prevIds.map(String), ...dedupIds])],
|
|
690
|
-
ts: Date.now(),
|
|
691
|
-
count: prevCount + 1,
|
|
692
|
-
...(hookData.session_id ? { session: hookData.session_id } : {}),
|
|
693
|
-
}));
|
|
677
|
+
mergeInjectedMarker(injectedIdsFile, dedupIds,
|
|
678
|
+
{ sessionId: hookData.session_id, maxAgeMs: DEDUP_STALE_MS, mode: 'union' });
|
|
694
679
|
} catch {}
|
|
695
680
|
}
|
|
696
681
|
}
|
|
@@ -932,22 +917,14 @@ async function main() {
|
|
|
932
917
|
if (output) {
|
|
933
918
|
process.stdout.write(output + '\n');
|
|
934
919
|
// Write injected IDs for dedup with hook.mjs handleUserPrompt + self-dedup
|
|
920
|
+
// replace, NOT union: this leg writes the prompt's own result set wholesale, and it
|
|
921
|
+
// is the ONE writer that puts raw observation numbers (mixed with `P<id>` strings)
|
|
922
|
+
// into the marker. `mode:'replace'` writes `candidateIds` verbatim for exactly that
|
|
923
|
+
// reason — stringifying here would flip the D#213 exclude from inert to live, which
|
|
924
|
+
// is a behaviour change with its own ruler and its own decision to make.
|
|
935
925
|
try {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const prev = JSON.parse(readFileSync(injectedIdsFile, 'utf8'));
|
|
939
|
-
// M-6: same-session (or legacy) count only; atomic write (torn-write guard).
|
|
940
|
-
if (prev.ts && Date.now() - prev.ts < DEDUP_STALE_MS
|
|
941
|
-
&& !(prev.session && hookData.session_id && prev.session !== hookData.session_id)) {
|
|
942
|
-
prevCount = prev.count || 0;
|
|
943
|
-
}
|
|
944
|
-
} catch {}
|
|
945
|
-
atomicWriteFileSync(injectedIdsFile, JSON.stringify({
|
|
946
|
-
ids: candidateIds,
|
|
947
|
-
ts: Date.now(),
|
|
948
|
-
count: prevCount + 1,
|
|
949
|
-
...(hookData.session_id ? { session: hookData.session_id } : {}),
|
|
950
|
-
}));
|
|
926
|
+
mergeInjectedMarker(injectedIdsFile, candidateIds,
|
|
927
|
+
{ sessionId: hookData.session_id, maxAgeMs: DEDUP_STALE_MS, mode: 'replace' });
|
|
951
928
|
} catch {}
|
|
952
929
|
// v26 P0: bump injection_count for obs-based emits only (prompt-corpus
|
|
953
930
|
// rows have "P<id>" string IDs; skip those — they live in user_prompts).
|
package/search-scoring.mjs
CHANGED
|
@@ -308,6 +308,13 @@ export function runIdleCleanup(db) {
|
|
|
308
308
|
const marked = db.prepare(`
|
|
309
309
|
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
310
310
|
WHERE importance <= 1 AND COALESCE(access_count, 0) = 0
|
|
311
|
+
-- injection_count=0, the second half of decayAndMarkIdle's engagement guard
|
|
312
|
+
-- (audit 2026-09-02 P0-4). Since v2.56 an injected-but-never-accessed row counts
|
|
313
|
+
-- as PROVEN RELEVANT and decay protects it; this MCP sibling carried the lesson
|
|
314
|
+
-- guard (CHANGELOG "the sixth enforcement site") and not this one, so a row the
|
|
315
|
+
-- CLI/hook paths preserve was pending-purge'd 5 minutes into an idle server —
|
|
316
|
+
-- the same guard-on-one-path shape the docblock below claims was consolidated.
|
|
317
|
+
AND COALESCE(injection_count, 0) = 0
|
|
311
318
|
AND type IN (${types})
|
|
312
319
|
AND created_at_epoch < ? AND COALESCE(compressed_into, 0) = 0
|
|
313
320
|
-- Never auto-mark a lesson-bearing row for purge. This idle path is the
|
|
@@ -321,6 +328,9 @@ export function runIdleCleanup(db) {
|
|
|
321
328
|
const compressed = db.prepare(`
|
|
322
329
|
UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
|
|
323
330
|
WHERE COALESCE(compressed_into, 0) = 0 AND importance = 1
|
|
331
|
+
-- Same engagement guard as the mark-idle pass above: COMPRESSED_AUTO also hides
|
|
332
|
+
-- the row from every retrieval surface, so an injected row must not reach it.
|
|
333
|
+
AND COALESCE(injection_count, 0) = 0
|
|
324
334
|
AND type IN (${types})
|
|
325
335
|
AND created_at_epoch < ?
|
|
326
336
|
-- Same lesson guard: auto-compress (-1) hides the row from all retrieval and
|
package/server.mjs
CHANGED
|
@@ -15,13 +15,11 @@ import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from '
|
|
|
15
15
|
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
16
16
|
import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
recoverOrphanedChildren, recoverBuriedLessons, sweepDeferredWorkOrphans,
|
|
20
|
-
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
21
|
-
hardDeleteCandidateCount,
|
|
18
|
+
runMaintainOps, findDuplicates, maintenanceStats,
|
|
22
19
|
OP_CAP, STALE_AGE_MS, resolveDefaultMaintainOps, DEFAULT_MAINTAIN_OPS,
|
|
23
20
|
} from './lib/maintain-core.mjs';
|
|
24
|
-
|
|
21
|
+
// snapshotDb left with maintain-core: the pre-maintain snapshot is part of the op ORDER
|
|
22
|
+
// (it must see the pre-existing pending rows), so it moved into runMaintainOps (P1-5).
|
|
25
23
|
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
26
24
|
import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
|
|
27
25
|
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
@@ -47,10 +45,10 @@ import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
|
47
45
|
import { join, sep } from 'path';
|
|
48
46
|
import { homedir } from 'os';
|
|
49
47
|
import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
50
|
-
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources } from './lib/registry-core.mjs';
|
|
48
|
+
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichImportedResources, enrichNamedResource } from './lib/registry-core.mjs';
|
|
51
49
|
import { searchResources } from './registry-retriever.mjs';
|
|
52
50
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
53
|
-
import {
|
|
51
|
+
import { saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
|
|
54
52
|
import { applyObsUpdate } from './lib/observation-write.mjs';
|
|
55
53
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
56
54
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
@@ -59,7 +57,7 @@ import { fetchRecent } from './lib/recent-core.mjs';
|
|
|
59
57
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
60
58
|
import {
|
|
61
59
|
insertDeferred, listOpenWithOrdinal, dropDeferred, formatDropReasonHint,
|
|
62
|
-
resolveDeferredIds,
|
|
60
|
+
resolveDeferredIds,
|
|
63
61
|
getDeferredByIds, formatDeferredDetail,
|
|
64
62
|
searchDeferredWork, formatDeferredSearchTrailer,
|
|
65
63
|
formatDeferListRow, countStaleOpen, formatDeferStaleHint,
|
|
@@ -833,33 +831,23 @@ server.registerTool(
|
|
|
833
831
|
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
834
832
|
const project = args.project || inferProject();
|
|
835
833
|
|
|
836
|
-
let closesIds
|
|
837
|
-
let result;
|
|
834
|
+
let closesIds, result;
|
|
838
835
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
// already-closed items. Mirrors mem-cli.mjs cmdSave shape.
|
|
855
|
-
if (args.closes_deferred && args.closes_deferred.length > 0) {
|
|
856
|
-
// D#195: 'dropped' is closable by the close verb (kept in sync with
|
|
857
|
-
// mem-cli.mjs cmdSave — same policy, both faces).
|
|
858
|
-
closesIds = resolveDeferredIds(db, project, args.closes_deferred, { allowStatuses: ['open', 'dropped'] });
|
|
859
|
-
closeDeferredItems(db, closesIds, r.id);
|
|
860
|
-
}
|
|
861
|
-
return r;
|
|
862
|
-
})();
|
|
836
|
+
// The transaction body — dedup short-circuit BEFORE the resolver, and D#195's
|
|
837
|
+
// allowStatuses — is lib/save-observation.mjs's (audit 2026-09-02 P1-6). Both faces
|
|
838
|
+
// used to write it out and each carried a "kept in sync with the other" comment.
|
|
839
|
+
({ result, closesIds } = saveWithClosures(db, {
|
|
840
|
+
// Size ceiling applied here rather than in lib/save-observation.mjs so the
|
|
841
|
+
// shared pipeline keeps its "caller validates" contract (see its header).
|
|
842
|
+
content: clampSaveText(args.content, SAVE_TEXT_LIMITS.content),
|
|
843
|
+
title: clampSaveText(args.title, SAVE_TEXT_LIMITS.title),
|
|
844
|
+
type: args.type || 'discovery',
|
|
845
|
+
importance: args.importance,
|
|
846
|
+
project,
|
|
847
|
+
files: args.files || [],
|
|
848
|
+
lesson_learned: clampSaveText(args.lesson_learned, SAVE_TEXT_LIMITS.lesson_learned),
|
|
849
|
+
supersedes: args.supersedes,
|
|
850
|
+
}, { closesTokens: args.closes_deferred, project }));
|
|
863
851
|
} catch (e) {
|
|
864
852
|
if (args.closes_deferred && args.closes_deferred.length > 0) {
|
|
865
853
|
// Re-throw with a clearer prefix so MCP error response names the
|
|
@@ -1164,7 +1152,6 @@ server.registerTool(
|
|
|
1164
1152
|
if (args.operations && args.operations.length === 0) {
|
|
1165
1153
|
return { content: [{ type: 'text', text: `operations array is empty. Pass a non-empty list (e.g. ${JSON.stringify(DEFAULT_MAINTAIN_OPS)}) or omit operations to use the default set.` }], isError: true };
|
|
1166
1154
|
}
|
|
1167
|
-
const results = [];
|
|
1168
1155
|
const staleAge = Date.now() - STALE_AGE_MS;
|
|
1169
1156
|
const mctx = { projectFilter, baseParams, staleAge, opCap: OP_CAP };
|
|
1170
1157
|
|
|
@@ -1180,116 +1167,33 @@ server.registerTool(
|
|
|
1180
1167
|
// runs unconfirmed on either surface now or before).
|
|
1181
1168
|
const purgeConfirmed = args.confirm === true;
|
|
1182
1169
|
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const lines = [
|
|
1205
|
-
'purge_stale preview (confirm=false):',
|
|
1206
|
-
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`,
|
|
1207
|
-
];
|
|
1208
|
-
if (previewRow.candidates > 0) {
|
|
1209
|
-
lines.push(` Oldest: ${new Date(previewRow.oldest).toISOString().slice(0, 10)}`);
|
|
1210
|
-
lines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
1211
|
-
}
|
|
1212
|
-
lines.push(' Nothing was deleted. To delete, re-run with confirm=true:');
|
|
1213
|
-
lines.push(` mem_maintain(action="execute", operations=${JSON.stringify(ops)}, confirm=true${args.retain_days ? `, retain_days=${args.retain_days}` : ''}${args.project ? `, project="${args.project}"` : ''})`);
|
|
1214
|
-
results.push(lines.join('\n'));
|
|
1215
|
-
} else {
|
|
1216
|
-
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1217
|
-
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)` + (purged >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1170
|
+
// The op ORDER, the pre-transaction snapshot and the three post-transaction ops all
|
|
1171
|
+
// live in maintain-core (audit 2026-09-02 P1-5) — this face keeps only what is
|
|
1172
|
+
// genuinely MCP-shaped: how `merge_ids` is spelled in a warning, and what the purge
|
|
1173
|
+
// preview tells the caller to type next. Before the extraction this block rendered
|
|
1174
|
+
// `inj>=8` as a literal while the CLI twin interpolated PINNED_INJ_THRESHOLD.
|
|
1175
|
+
const results = runMaintainOps(db, mctx, ops, {
|
|
1176
|
+
retainDays: args.retain_days ?? 30,
|
|
1177
|
+
retainCutoff: Date.now() - (args.retain_days ?? 30) * DAY_MS,
|
|
1178
|
+
confirmed: purgeConfirmed,
|
|
1179
|
+
mergeGroups: args.merge_ids || null,
|
|
1180
|
+
mergeIdsProvided: Boolean(args.merge_ids),
|
|
1181
|
+
mergeIdsFlagName: 'merge_ids',
|
|
1182
|
+
onError: debugCatch,
|
|
1183
|
+
renderPurgePreview: (previewRow, retainDays) => {
|
|
1184
|
+
const lines = [
|
|
1185
|
+
'purge_stale preview (confirm=false):',
|
|
1186
|
+
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`,
|
|
1187
|
+
];
|
|
1188
|
+
if (previewRow.candidates > 0) {
|
|
1189
|
+
lines.push(` Oldest: ${new Date(previewRow.oldest).toISOString().slice(0, 10)}`);
|
|
1190
|
+
lines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
1218
1191
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
// Self-heal legacy orphans (keeper hard-deleted pre-recoverChildrenOf):
|
|
1225
|
-
// resurface unreachable children. Non-destructive — un-hide only, no delete.
|
|
1226
|
-
const orphans = recoverOrphanedChildren(db, mctx);
|
|
1227
|
-
if (orphans > 0) results.push(`Recovered ${orphans} orphaned compression children`);
|
|
1228
|
-
// Heal lesson rows citation-decay buried at importance 0 (pre floor=1). 0→1 on
|
|
1229
|
-
// lesson-bearing rows only; idempotent no-op once none remain.
|
|
1230
|
-
const lessonsHealed = recoverBuriedLessons(db, mctx);
|
|
1231
|
-
if (lessonsHealed > 0) results.push(`Healed ${lessonsHealed} lesson rows buried at importance 0`);
|
|
1232
|
-
// Heal deferred_work rows whose closing obs / source prompt was deleted while FK was
|
|
1233
|
-
// OFF (dangling ref foreign_key_check flags). Applies the ON DELETE SET NULL the FK would.
|
|
1234
|
-
const deferredHealed = sweepDeferredWorkOrphans(db, mctx);
|
|
1235
|
-
if (deferredHealed > 0) results.push(`Healed ${deferredHealed} deferred-work rows with dangling references`);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
if (ops.includes('decay')) {
|
|
1239
|
-
// injection_count>0 protected (maintain-core; unifies with CLI + hook —
|
|
1240
|
-
// the MCP copy previously lacked this clause and decayed/purged injected memories).
|
|
1241
|
-
const { decayed, idleMarked } = decayAndMarkIdle(db, mctx);
|
|
1242
|
-
results.push(`Decayed ${decayed} stale observations, marked ${idleMarked} idle as pending-purge` + ((decayed >= OP_CAP || idleMarked >= OP_CAP) ? ' (cap reached, re-run for more)' : ''));
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (ops.includes('boost')) {
|
|
1246
|
-
const boosted = boostAccessed(db, mctx);
|
|
1247
|
-
results.push(`Boosted ${boosted} frequently-accessed observations` + (boosted >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
if (ops.includes('demote_pinned')) {
|
|
1251
|
-
const demoted = demotePinned(db, mctx);
|
|
1252
|
-
results.push(`Demoted ${demoted} pinned-but-uncited observations (inj>=8, cited=0; no lesson → importance 1, lesson → 2)` + (demoted >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (ops.includes('dedup') && args.merge_ids) {
|
|
1256
|
-
const totalMerged = mergeDuplicates(db, args.merge_ids);
|
|
1257
|
-
results.push(`Merged ${totalMerged} duplicate observations`);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
if (!ops.includes('dedup') && args.merge_ids) {
|
|
1261
|
-
results.push('Warning: merge_ids provided but "dedup" not in operations — merge_ids ignored');
|
|
1262
|
-
}
|
|
1263
|
-
})();
|
|
1264
|
-
|
|
1265
|
-
// FTS5 optimize (outside transaction)
|
|
1266
|
-
db.exec("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
|
|
1267
|
-
results.push('FTS5 index optimized');
|
|
1268
|
-
|
|
1269
|
-
// rebuild_vectors: outside main transaction (maintain-core, shared with CLI).
|
|
1270
|
-
if (ops.includes('rebuild_vectors')) {
|
|
1271
|
-
try {
|
|
1272
|
-
const r = rebuildVectors(db);
|
|
1273
|
-
results.push(r.ok
|
|
1274
|
-
? `Vectors: rebuilt vocabulary (${r.terms} terms), updated ${r.updated}/${r.total} vectors`
|
|
1275
|
-
: `Vectors: ${r.reason}`);
|
|
1276
|
-
} catch (e) {
|
|
1277
|
-
debugCatch(e, 'rebuild_vectors');
|
|
1278
|
-
results.push(`Vectors: rebuild failed — ${e.message}`);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// vacuum: reclaim freelist dead space left by DELETEs. Whole-DB, outside any
|
|
1283
|
-
// transaction. maintain-core, shared with CLI.
|
|
1284
|
-
if (ops.includes('vacuum')) {
|
|
1285
|
-
try {
|
|
1286
|
-
const v = vacuum(db);
|
|
1287
|
-
results.push(`VACUUM: reclaimed ~${v.reclaimedMB}MB (freelist ${v.freeBefore} → ${v.freeAfter} pages)`);
|
|
1288
|
-
} catch (e) {
|
|
1289
|
-
debugCatch(e, 'vacuum');
|
|
1290
|
-
results.push(`VACUUM failed — ${e.message}`);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1192
|
+
lines.push(' Nothing was deleted. To delete, re-run with confirm=true:');
|
|
1193
|
+
lines.push(` mem_maintain(action="execute", operations=${JSON.stringify(ops)}, confirm=true${args.retain_days ? `, retain_days=${args.retain_days}` : ''}${args.project ? `, project="${args.project}"` : ''})`);
|
|
1194
|
+
return lines.join('\n');
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1293
1197
|
|
|
1294
1198
|
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
1295
1199
|
}
|
|
@@ -1500,16 +1404,8 @@ server.registerTool(
|
|
|
1500
1404
|
let enrichMsg = '';
|
|
1501
1405
|
if (args.enrich) {
|
|
1502
1406
|
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const row = rdb.prepare('SELECT local_path FROM resources WHERE id = ?').get(r.id);
|
|
1506
|
-
if (!row?.local_path) continue;
|
|
1507
|
-
try {
|
|
1508
|
-
const content = readFileSync(row.local_path, 'utf8');
|
|
1509
|
-
if (await enrichResource(rdb, r.name, r.type, content)) ok++;
|
|
1510
|
-
} catch {}
|
|
1511
|
-
}
|
|
1512
|
-
enrichMsg = `\nEnriched: ${ok}/${results.length}`;
|
|
1407
|
+
const { ok, denied } = await enrichImportedResources(rdb, results, { confineTo: DB_DIR, enrichResource });
|
|
1408
|
+
enrichMsg = `\nEnriched: ${ok}/${results.length}` + (denied ? ` (${denied} refused: path outside managed directory)` : '');
|
|
1513
1409
|
}
|
|
1514
1410
|
|
|
1515
1411
|
const lines = results.map(r => `${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
|
|
@@ -1523,25 +1419,27 @@ server.registerTool(
|
|
|
1523
1419
|
if (!args.name) {
|
|
1524
1420
|
return { content: [{ type: 'text', text: 'enrich requires a name parameter' }], isError: true };
|
|
1525
1421
|
}
|
|
1526
|
-
const row = rdb.prepare("SELECT name, type, local_path FROM resources WHERE name = ? AND status = 'active'").get(args.name);
|
|
1527
|
-
if (!row) {
|
|
1528
|
-
return { content: [{ type: 'text', text: `Resource not found: ${args.name}` }], isError: true };
|
|
1529
|
-
}
|
|
1530
|
-
if (!row.local_path) {
|
|
1531
|
-
return { content: [{ type: 'text', text: `No local_path for ${args.name}` }], isError: true };
|
|
1532
|
-
}
|
|
1533
1422
|
// Confine to the env-aware data dir (managed/ relocates with CLAUDE_MEM_DIR, D#29);
|
|
1534
|
-
// === homedir when the env is unset, so non-relocated confinement is unchanged.
|
|
1535
|
-
|
|
1536
|
-
if (!isPathConfined(row.local_path, enrichBase)) {
|
|
1537
|
-
return { content: [{ type: 'text', text: `Access denied: path outside managed directory` }], isError: true };
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1423
|
+
// === homedir when the env is unset, so non-relocated confinement is unchanged. The
|
|
1424
|
+
// gate itself now lives in lib/registry-core.mjs so all four enrichment legs share it.
|
|
1540
1425
|
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1541
1426
|
try {
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1427
|
+
const { status, error } = await enrichNamedResource(rdb, args.name, { confineTo: DB_DIR, enrichResource });
|
|
1428
|
+
if (status === 'not-found') {
|
|
1429
|
+
return { content: [{ type: 'text', text: `Resource not found: ${args.name}` }], isError: true };
|
|
1430
|
+
}
|
|
1431
|
+
if (status === 'no-path') {
|
|
1432
|
+
return { content: [{ type: 'text', text: `No local_path for ${args.name}` }], isError: true };
|
|
1433
|
+
}
|
|
1434
|
+
if (status === 'denied') {
|
|
1435
|
+
return { content: [{ type: 'text', text: `Access denied: path outside managed directory` }], isError: true };
|
|
1436
|
+
}
|
|
1437
|
+
if (status === 'unreadable') {
|
|
1438
|
+
// Pre-refactor this threw out of readFileSync into the catch below and rendered
|
|
1439
|
+
// `Enrich error: <errno>`. Keep the errno: a stale local_path is the common cause.
|
|
1440
|
+
return { content: [{ type: 'text', text: `Enrich error: ${error?.message || 'cannot read local_path'}` }], isError: true };
|
|
1441
|
+
}
|
|
1442
|
+
return { content: [{ type: 'text', text: status === 'enriched' ? `Enriched: ${args.name}` : `Enrichment failed for ${args.name}` }] };
|
|
1545
1443
|
} catch (e) {
|
|
1546
1444
|
return { content: [{ type: 'text', text: `Enrich error: ${e.message}` }], isError: true };
|
|
1547
1445
|
}
|
package/source-files.mjs
CHANGED
|
@@ -63,6 +63,8 @@ export const SOURCE_FILES = [
|
|
|
63
63
|
'lib/cite-back-hint.mjs',
|
|
64
64
|
// The one definition of the pre-recall cooldown path — shared by its writer
|
|
65
65
|
// (scripts/pre-tool-recall.js) and both readers (cite-back-hint, edge-attribution).
|
|
66
|
+
'lib/cite-recall-path.mjs',
|
|
67
|
+
'lib/frontmatter.mjs',
|
|
66
68
|
'lib/cooldown-path.mjs',
|
|
67
69
|
// v2.85: stale test-fixture sweeper. Imported by install.mjs (cleanup) + cli.mjs.
|
|
68
70
|
// Missing from manifest → tarball ships install.mjs that ERR_MODULE_NOT_FOUND on cleanup.
|