claude-mem-lite 3.96.0 → 3.96.1
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/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +134 -45
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +85 -124
- package/hook-update.mjs +262 -87
- package/hook.mjs +1169 -606
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +49 -26
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- package/utils.mjs +184 -36
package/server.mjs
CHANGED
|
@@ -12,17 +12,48 @@ import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './se
|
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
14
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
15
|
-
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
16
|
-
import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
17
15
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
resolveAnchorToken,
|
|
17
|
+
formatAnchorError,
|
|
18
|
+
resolveQueryAnchor,
|
|
19
|
+
fetchRecentTimeline,
|
|
20
|
+
fetchTimelineWindow,
|
|
21
|
+
} from './lib/timeline-core.mjs';
|
|
22
|
+
import {
|
|
23
|
+
buildSearchFtsQuery,
|
|
24
|
+
parseDateBounds,
|
|
25
|
+
parseDuration,
|
|
26
|
+
coreRunSearchPipeline,
|
|
27
|
+
} from './lib/search-core.mjs';
|
|
28
|
+
import {
|
|
29
|
+
runMaintainOps,
|
|
30
|
+
findDuplicates,
|
|
31
|
+
maintenanceStats,
|
|
32
|
+
OP_CAP,
|
|
33
|
+
STALE_AGE_MS,
|
|
34
|
+
resolveDefaultMaintainOps,
|
|
35
|
+
DEFAULT_MAINTAIN_OPS,
|
|
20
36
|
} from './lib/maintain-core.mjs';
|
|
21
37
|
// snapshotDb left with maintain-core: the pre-maintain snapshot is part of the op ORDER
|
|
22
38
|
// (it must see the pre-existing pending rows), so it moved into runMaintainOps (P1-5).
|
|
23
39
|
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
24
|
-
import {
|
|
25
|
-
|
|
40
|
+
import {
|
|
41
|
+
fetchObsDetail,
|
|
42
|
+
fetchPromptDetail,
|
|
43
|
+
fetchEventDetail,
|
|
44
|
+
fetchSessionDetail,
|
|
45
|
+
OBS_FIELDS,
|
|
46
|
+
SESSION_DETAIL_FIELDS,
|
|
47
|
+
PROMPT_DETAIL_FIELDS,
|
|
48
|
+
EVENT_DETAIL_FIELDS,
|
|
49
|
+
supersededNotice,
|
|
50
|
+
} from './lib/get-core.mjs';
|
|
51
|
+
import {
|
|
52
|
+
collectBrowseTiers,
|
|
53
|
+
getActiveMemorySessionId,
|
|
54
|
+
BROWSE_TIERS,
|
|
55
|
+
BROWSE_TIER_LABELS,
|
|
56
|
+
} from './lib/browse-core.mjs';
|
|
26
57
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
27
58
|
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
28
59
|
import { buildLessonNudge } from './lib/save-nudge.mjs';
|
|
@@ -31,7 +62,29 @@ import { formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cl
|
|
|
31
62
|
// set by default — the invocation has to be the one that actually works on this install.
|
|
32
63
|
import { CLI_INVOKE } from './cli-path.mjs';
|
|
33
64
|
import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from './format-utils.mjs';
|
|
34
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
memSearchSchema,
|
|
67
|
+
memRecentSchema,
|
|
68
|
+
memTimelineSchema,
|
|
69
|
+
memGetSchema,
|
|
70
|
+
memDeleteSchema,
|
|
71
|
+
memSaveSchema,
|
|
72
|
+
memStatsSchema,
|
|
73
|
+
memCompressSchema,
|
|
74
|
+
memMaintainSchema,
|
|
75
|
+
memOptimizeSchema,
|
|
76
|
+
memUpdateSchema,
|
|
77
|
+
memExportSchema,
|
|
78
|
+
memRecallSchema,
|
|
79
|
+
memFtsCheckSchema,
|
|
80
|
+
memRegistrySchema,
|
|
81
|
+
memBrowseSchema,
|
|
82
|
+
memUseSchema,
|
|
83
|
+
memDeferSchema,
|
|
84
|
+
memDeferListSchema,
|
|
85
|
+
memDeferDropSchema,
|
|
86
|
+
tools as TOOL_DEFS,
|
|
87
|
+
} from './tool-schemas.mjs';
|
|
35
88
|
|
|
36
89
|
// Lookup helper: all user-facing tool descriptions live in tool-schemas.mjs
|
|
37
90
|
// (discouragement-style, Task 5). This keeps server.mjs from drifting.
|
|
@@ -44,10 +97,27 @@ function descriptionOf(name) {
|
|
|
44
97
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
45
98
|
import { join, sep } from 'path';
|
|
46
99
|
import { homedir } from 'os';
|
|
47
|
-
import {
|
|
48
|
-
|
|
100
|
+
import {
|
|
101
|
+
ensureRegistryDb,
|
|
102
|
+
collectRegistryStats,
|
|
103
|
+
listResourcesRanked,
|
|
104
|
+
formatRegistryListLine,
|
|
105
|
+
} from './registry.mjs';
|
|
106
|
+
import {
|
|
107
|
+
IMPORT_STRING_FIELDS,
|
|
108
|
+
importResource,
|
|
109
|
+
removeResource,
|
|
110
|
+
reindexResources,
|
|
111
|
+
enrichImportedResources,
|
|
112
|
+
enrichNamedResource,
|
|
113
|
+
resourceUseHint,
|
|
114
|
+
} from './lib/registry-core.mjs';
|
|
49
115
|
import { searchResources } from './registry-retriever.mjs';
|
|
50
|
-
import {
|
|
116
|
+
import {
|
|
117
|
+
probeOtherSources as probeIdSources,
|
|
118
|
+
bucketIdTokens,
|
|
119
|
+
splitDeferredTokens,
|
|
120
|
+
} from './lib/id-routing.mjs';
|
|
51
121
|
import { saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
|
|
52
122
|
import { applyObsUpdate } from './lib/observation-write.mjs';
|
|
53
123
|
import { EXPORT_COLUMNS_SQL, buildExportWhere } from './lib/export-columns.mjs';
|
|
@@ -55,11 +125,18 @@ import { recallByFile } from './lib/recall-core.mjs';
|
|
|
55
125
|
import { fetchRecent } from './lib/recent-core.mjs';
|
|
56
126
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
57
127
|
import {
|
|
58
|
-
insertDeferred,
|
|
128
|
+
insertDeferred,
|
|
129
|
+
listOpenWithOrdinal,
|
|
130
|
+
dropDeferred,
|
|
131
|
+
formatDropReasonHint,
|
|
59
132
|
resolveDeferredIds,
|
|
60
|
-
getDeferredByIds,
|
|
61
|
-
|
|
62
|
-
|
|
133
|
+
getDeferredByIds,
|
|
134
|
+
formatDeferredDetail,
|
|
135
|
+
searchDeferredWork,
|
|
136
|
+
formatDeferredSearchTrailer,
|
|
137
|
+
formatDeferListRow,
|
|
138
|
+
countStaleOpen,
|
|
139
|
+
formatDeferStaleHint,
|
|
63
140
|
} from './lib/deferred-work.mjs';
|
|
64
141
|
import { shouldQueueSaveEnrich, queueSaveEnrich } from './lib/save-enrich.mjs';
|
|
65
142
|
import { _resetVocabCache } from './tfidf.mjs';
|
|
@@ -70,7 +147,15 @@ const { version: PKG_VERSION } = require('./package.json');
|
|
|
70
147
|
|
|
71
148
|
// ─── Database ───────────────────────────────────────────────────────────────
|
|
72
149
|
|
|
73
|
-
import {
|
|
150
|
+
import {
|
|
151
|
+
existsSync,
|
|
152
|
+
readFileSync,
|
|
153
|
+
writeFileSync,
|
|
154
|
+
appendFileSync,
|
|
155
|
+
mkdirSync,
|
|
156
|
+
readdirSync,
|
|
157
|
+
chmodSync,
|
|
158
|
+
} from 'fs';
|
|
74
159
|
|
|
75
160
|
let db;
|
|
76
161
|
try {
|
|
@@ -84,9 +169,13 @@ try {
|
|
|
84
169
|
// Fatal: log and exit with descriptive message (Claude Code shows stderr)
|
|
85
170
|
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
|
|
86
171
|
if (err.walRecoveryAttempted) {
|
|
87
|
-
console.error(
|
|
172
|
+
console.error(
|
|
173
|
+
`[claude-mem-lite] Try: rm "${DB_PATH}-wal" "${DB_PATH}-shm" or reinstall with: node install.mjs install`,
|
|
174
|
+
);
|
|
88
175
|
} else {
|
|
89
|
-
console.error(
|
|
176
|
+
console.error(
|
|
177
|
+
`[claude-mem-lite] Left WAL/SHM intact (not a corruption error). If this persists, retry or reinstall: node install.mjs install`,
|
|
178
|
+
);
|
|
90
179
|
}
|
|
91
180
|
process.exit(1);
|
|
92
181
|
}
|
|
@@ -115,7 +204,9 @@ function getRegistryDb() {
|
|
|
115
204
|
// "projects--mem" (parent--base from CWD). resolveProject() bridges this gap.
|
|
116
205
|
// Implementation extracted to project-utils.mjs; local adapter closes over module-level `db`.
|
|
117
206
|
|
|
118
|
-
function resolveProject(name) {
|
|
207
|
+
function resolveProject(name) {
|
|
208
|
+
return _resolveProjectShared(db, name);
|
|
209
|
+
}
|
|
119
210
|
|
|
120
211
|
// ─── Scoring Model Constants ────────────────────────────────────────────────
|
|
121
212
|
//
|
|
@@ -145,9 +236,8 @@ function resolveProject(name) { return _resolveProjectShared(db, name); }
|
|
|
145
236
|
// opts out. stderr doesn't pollute the MCP stdio protocol channel.
|
|
146
237
|
const _quiet = effectiveQuiet();
|
|
147
238
|
if (process.env.CLAUDE_MEM_QUIET_TRACE !== '0') {
|
|
148
|
-
const reason =
|
|
149
|
-
? 'env:MEM_QUIET_HOOKS=1'
|
|
150
|
-
: _quiet ? 'adopted:steering' : 'none';
|
|
239
|
+
const reason =
|
|
240
|
+
process.env.MEM_QUIET_HOOKS === '1' ? 'env:MEM_QUIET_HOOKS=1' : _quiet ? 'adopted:steering' : 'none';
|
|
151
241
|
const mode = _quiet ? 'BASE' : 'BASE+VERBOSE';
|
|
152
242
|
process.stderr.write(`[mem] instructions: ${mode} reason=${reason}\n`);
|
|
153
243
|
}
|
|
@@ -207,10 +297,10 @@ function defangResult(result) {
|
|
|
207
297
|
if (!result || !Array.isArray(result.content)) return result;
|
|
208
298
|
return {
|
|
209
299
|
...result,
|
|
210
|
-
content: result.content.map(c =>
|
|
300
|
+
content: result.content.map((c) =>
|
|
211
301
|
c && c.type === 'text' && typeof c.text === 'string'
|
|
212
302
|
? { ...c, text: neutralizeContextDelimiters(c.text) }
|
|
213
|
-
: c
|
|
303
|
+
: c,
|
|
214
304
|
),
|
|
215
305
|
};
|
|
216
306
|
}
|
|
@@ -246,7 +336,14 @@ function safeHandler(fn, { verbatim = false } = {}) {
|
|
|
246
336
|
// searchObservations / searchSessions / searchPrompts were consolidated into the
|
|
247
337
|
// shared coreRunSearchPipeline (lib/search-core.mjs). This surface is now a thin
|
|
248
338
|
// adapter (runSearchPipeline below); only output formatting stays local.
|
|
249
|
-
function formatSearchOutput(
|
|
339
|
+
function formatSearchOutput(
|
|
340
|
+
paginatedResults,
|
|
341
|
+
args,
|
|
342
|
+
ftsQuery,
|
|
343
|
+
totalCount,
|
|
344
|
+
orFallbackFired = false,
|
|
345
|
+
isDeepSearch = false,
|
|
346
|
+
) {
|
|
250
347
|
if (paginatedResults.length === 0) {
|
|
251
348
|
const hint = [];
|
|
252
349
|
if (isDeepSearch) {
|
|
@@ -254,7 +351,9 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFall
|
|
|
254
351
|
// "query was filtered" hint below would be misleading — the LLM rewrite ran
|
|
255
352
|
// N variants and simply found nothing (F9).
|
|
256
353
|
hint.push('No results — deep search rewrote the query into variants and still found nothing.');
|
|
257
|
-
hint.push(
|
|
354
|
+
hint.push(
|
|
355
|
+
'This is a recall miss (the rewrite ran), not a query-syntax issue; the memory likely has no related observations.',
|
|
356
|
+
);
|
|
258
357
|
} else if (args.query && !ftsQuery) {
|
|
259
358
|
hint.push(`Query "${args.query}" was filtered (FTS5 keywords/special chars only).`);
|
|
260
359
|
hint.push('Tip: use content words instead of operators (AND, OR, NOT, NEAR).');
|
|
@@ -276,10 +375,13 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFall
|
|
|
276
375
|
// The old isCrossSource gate predated countSearchTotal: back then single-source
|
|
277
376
|
// totalCount was just results.length, so suppressing "of M" hid nothing. Now it hid
|
|
278
377
|
// the real total, diverging from the CLI (mem-cli.mjs has no such gate). (#8217)
|
|
279
|
-
const countLabel =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
378
|
+
const countLabel =
|
|
379
|
+
totalCount > paginatedResults.length
|
|
380
|
+
? `${paginatedResults.length} of ${totalCount}`
|
|
381
|
+
: `${paginatedResults.length}`;
|
|
382
|
+
const hasMixed = paginatedResults.some(
|
|
383
|
+
(r) => r.source === 'session' || r.source === 'prompt' || r.source === 'event',
|
|
384
|
+
);
|
|
283
385
|
// P2-6: empty/omitted query falls through to a "listing recent" path — label it explicitly
|
|
284
386
|
// so callers don't mistake BM25-less results for relevance-ranked ones.
|
|
285
387
|
const qLabel = args.query ? ` for "${args.query}"` : ' (no query — listing recent)';
|
|
@@ -287,27 +389,37 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFall
|
|
|
287
389
|
// query actually matched only a subset of the terms. Suppressed when the caller
|
|
288
390
|
// explicitly requested OR semantics — there's no "fallback" in that path.
|
|
289
391
|
const fallbackHint = orFallbackFired && !args.or ? ' (relaxed AND→OR)' : '';
|
|
290
|
-
lines.push(
|
|
392
|
+
lines.push(
|
|
393
|
+
`Found ${countLabel} result(s)${qLabel}${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt, E# event)' : ''}\n`,
|
|
394
|
+
);
|
|
291
395
|
|
|
292
396
|
// `~Nt` = estimated tokens to fetch this row's full body via mem_get (attachBodyTokens).
|
|
293
397
|
// Conditional so a result that skipped enrichment renders cleanly, not "~undefinedt".
|
|
294
|
-
const tok = r => (r.bodyTokens ? ` ~${r.bodyTokens}t` : '');
|
|
398
|
+
const tok = (r) => (r.bodyTokens ? ` ~${r.bodyTokens}t` : '');
|
|
295
399
|
for (const r of paginatedResults) {
|
|
296
400
|
if (r.source === 'obs') {
|
|
297
|
-
lines.push(
|
|
401
|
+
lines.push(
|
|
402
|
+
`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`,
|
|
403
|
+
);
|
|
298
404
|
if (r.snippet && r.snippet.length > 10 && r.snippet !== r.title) {
|
|
299
405
|
lines.push(` ${truncate(r.snippet, 100)}`);
|
|
300
406
|
}
|
|
301
407
|
} else if (r.source === 'session') {
|
|
302
|
-
lines.push(
|
|
408
|
+
lines.push(
|
|
409
|
+
`S#${r.id} 📋 ${truncate(r.request || r.completed || '(no summary)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`,
|
|
410
|
+
);
|
|
303
411
|
} else if (r.source === 'prompt') {
|
|
304
412
|
lines.push(`P#${r.id} 💬 ${truncate(r.text)} | ${fmtDate(r.date)}${tok(r)}`);
|
|
305
413
|
} else if (r.source === 'event') {
|
|
306
|
-
lines.push(
|
|
414
|
+
lines.push(
|
|
415
|
+
`E#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`,
|
|
416
|
+
);
|
|
307
417
|
}
|
|
308
418
|
}
|
|
309
419
|
|
|
310
|
-
lines.push(
|
|
420
|
+
lines.push(
|
|
421
|
+
`\nWorkflow: mem_timeline(anchor=ID) for context | mem_get(ids=[...]) for full details · ~Nt = est. tokens to fetch full detail`,
|
|
422
|
+
);
|
|
311
423
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
312
424
|
}
|
|
313
425
|
|
|
@@ -337,7 +449,8 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
337
449
|
|
|
338
450
|
const bounds = parseDateBounds(args.date_from, args.date_to, args.date_since);
|
|
339
451
|
if (!bounds.ok) {
|
|
340
|
-
if (bounds.bad === 'since')
|
|
452
|
+
if (bounds.bad === 'since')
|
|
453
|
+
throw new Error(`Invalid date_since: "${bounds.value}" (use <N><unit>, e.g. 7d, 24h, 90m, 2w)`);
|
|
341
454
|
throw new Error(`Invalid date_${bounds.bad}: "${bounds.value}" (use ISO 8601 or YYYY-MM-DD)`);
|
|
342
455
|
}
|
|
343
456
|
const { epochFrom, epochTo } = bounds;
|
|
@@ -352,7 +465,8 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
352
465
|
// with CLI cmdSearch's emitDeferredTrailer; unfiltered first-page searches
|
|
353
466
|
// only. Structured fields (results/total) stay untouched — the trailer is a
|
|
354
467
|
// text affordance, not a result source.
|
|
355
|
-
const wantDeferredTrailer =
|
|
468
|
+
const wantDeferredTrailer =
|
|
469
|
+
!args.type && !args.obs_type && !args.branch && !args.tier && !args.importance && offset === 0;
|
|
356
470
|
const appendDeferredTrailer = (result) => {
|
|
357
471
|
if (!wantDeferredTrailer) return result;
|
|
358
472
|
try {
|
|
@@ -361,7 +475,9 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
361
475
|
if (lines.length > 0 && result.content?.[0]?.type === 'text') {
|
|
362
476
|
result.content[0].text += `\n\n${lines.join('\n')}`;
|
|
363
477
|
}
|
|
364
|
-
} catch {
|
|
478
|
+
} catch {
|
|
479
|
+
/* trailer is best-effort; never break search */
|
|
480
|
+
}
|
|
365
481
|
return result;
|
|
366
482
|
};
|
|
367
483
|
|
|
@@ -369,8 +485,22 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
369
485
|
// keywords/special chars). Skipped for deep/auto (the LLM rewrite may still
|
|
370
486
|
// produce variants) and for filter-only listings (date/obs_type/importance).
|
|
371
487
|
// A pure "D#92" query lands here — the trailer still reaches the item.
|
|
372
|
-
if (
|
|
373
|
-
|
|
488
|
+
if (
|
|
489
|
+
args.query &&
|
|
490
|
+
!ftsQuery &&
|
|
491
|
+
!epochFrom &&
|
|
492
|
+
!epochTo &&
|
|
493
|
+
!args.obs_type &&
|
|
494
|
+
!args.importance &&
|
|
495
|
+
deepMode === 'normal'
|
|
496
|
+
) {
|
|
497
|
+
return {
|
|
498
|
+
...appendDeferredTrailer(formatSearchOutput([], args, ftsQuery, 0)),
|
|
499
|
+
escalated: false,
|
|
500
|
+
results: [],
|
|
501
|
+
total: 0,
|
|
502
|
+
variants: null,
|
|
503
|
+
};
|
|
374
504
|
}
|
|
375
505
|
|
|
376
506
|
// Source scoping. deep is observations-only (deepSearch fuses hybrid-obs lists). branch/tier are
|
|
@@ -386,7 +516,7 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
386
516
|
} else if (args.type) {
|
|
387
517
|
effectiveType = args.type;
|
|
388
518
|
} else if (args.obs_type && !args.branch && !args.tier) {
|
|
389
|
-
effectiveType = undefined;
|
|
519
|
+
effectiveType = undefined; // cross-source gate open; obsTypeScoped narrows it to obs+events
|
|
390
520
|
obsTypeScoped = true;
|
|
391
521
|
} else if (args.importance || args.branch || args.tier) {
|
|
392
522
|
effectiveType = 'observations';
|
|
@@ -396,38 +526,61 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
396
526
|
|
|
397
527
|
const r = await coreRunSearchPipeline(
|
|
398
528
|
{
|
|
399
|
-
db,
|
|
400
|
-
|
|
401
|
-
|
|
529
|
+
db,
|
|
530
|
+
currentProject,
|
|
531
|
+
env: process.env,
|
|
532
|
+
searchObservationsHybrid,
|
|
533
|
+
deepSearch,
|
|
534
|
+
shouldEscalateToDeep,
|
|
535
|
+
autoDeepLlmReady,
|
|
536
|
+
reRankWithContext,
|
|
537
|
+
llm,
|
|
538
|
+
rerankLlm,
|
|
402
539
|
},
|
|
403
540
|
{
|
|
404
|
-
query: args.query,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
541
|
+
query: args.query,
|
|
542
|
+
ftsQuery,
|
|
543
|
+
effectiveSource: effectiveType,
|
|
544
|
+
deepMode,
|
|
545
|
+
rerank,
|
|
546
|
+
limit,
|
|
547
|
+
offset,
|
|
548
|
+
project: args.project ?? null,
|
|
549
|
+
obsType: args.obs_type ?? null,
|
|
550
|
+
importance: args.importance ?? null,
|
|
551
|
+
branch: args.branch ?? null,
|
|
552
|
+
includeNoise: args.include_noise === true,
|
|
553
|
+
epochFrom,
|
|
554
|
+
epochTo,
|
|
555
|
+
sort: args.sort || 'relevance',
|
|
556
|
+
tier: args.tier ?? null,
|
|
409
557
|
// ── MCP surface policy ──
|
|
410
|
-
obsTypeScoped,
|
|
411
|
-
obsTypeFallback: true,
|
|
412
|
-
crossSourceEpochSortNoFts: true,
|
|
413
|
-
rerankPolicy: 'mcp',
|
|
558
|
+
obsTypeScoped, // D#76: obs_type ⇒ obs+events (skip type-less sessions/prompts)
|
|
559
|
+
obsTypeFallback: true, // list-recent-by-type when 0 matches
|
|
560
|
+
crossSourceEpochSortNoFts: true, // epoch-sort cross-source with no ftsQuery
|
|
561
|
+
rerankPolicy: 'mcp', // (ftsQuery||isDeep) gate; re-rank/re-sort on ftsQuery&&!reranked
|
|
414
562
|
rerankProject: currentProject,
|
|
415
|
-
recentListingNoFts: true,
|
|
563
|
+
recentListingNoFts: true, // recent-listing for explicit --source with no ftsQuery
|
|
416
564
|
tolerateMissingFts: false,
|
|
417
|
-
tierPosition: 'late',
|
|
565
|
+
tierPosition: 'late', // tier filter after re-rank
|
|
418
566
|
tierProject: args.project || currentProject,
|
|
419
|
-
}
|
|
567
|
+
},
|
|
420
568
|
);
|
|
421
569
|
|
|
422
570
|
// Observability: announce auto-escalation on stderr (parity with CLI deep note).
|
|
423
|
-
if (r.escalated)
|
|
571
|
+
if (r.escalated)
|
|
572
|
+
process.stderr.write(`[mem] auto-escalated to deep search (weak results: ${r.escalatedObsCount} hits)\n`);
|
|
424
573
|
|
|
425
574
|
const output = formatSearchOutput(r.page, args, ftsQuery, r.total, r.orFallbackFired, r.isDeep);
|
|
426
575
|
// Surface the rewrite to the calling agent (F13) + the rerank signal (D#43).
|
|
427
576
|
if (r.isDeep && r.variants && output.content?.[0]?.type === 'text') {
|
|
428
|
-
output.content[0].text +=
|
|
429
|
-
|
|
430
|
-
|
|
577
|
+
output.content[0].text +=
|
|
578
|
+
r.variants.length > 1
|
|
579
|
+
? `\n\n[deep search: rewrote into ${r.variants.length} variants — ${r.variants
|
|
580
|
+
.slice(1)
|
|
581
|
+
.map((v) => JSON.stringify(v))
|
|
582
|
+
.join(', ')}]`
|
|
583
|
+
: '\n\n[deep search: rewrite produced no usable variants; searched the original query only (== baseline)]';
|
|
431
584
|
}
|
|
432
585
|
if (r.reranked && output.content?.[0]?.type === 'text') {
|
|
433
586
|
output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
|
|
@@ -435,7 +588,14 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
435
588
|
appendDeferredTrailer(output);
|
|
436
589
|
|
|
437
590
|
// Expose structured fields for tests + the MCP content blob.
|
|
438
|
-
return {
|
|
591
|
+
return {
|
|
592
|
+
...output,
|
|
593
|
+
results: r.page,
|
|
594
|
+
total: r.total,
|
|
595
|
+
escalated: r.escalated,
|
|
596
|
+
variants: r.variants,
|
|
597
|
+
reranked: r.reranked,
|
|
598
|
+
};
|
|
439
599
|
}
|
|
440
600
|
|
|
441
601
|
server.registerTool(
|
|
@@ -447,7 +607,7 @@ server.registerTool(
|
|
|
447
607
|
safeHandler(async (args) => {
|
|
448
608
|
const result = await runSearchPipeline(db, args, {});
|
|
449
609
|
return { content: result.content };
|
|
450
|
-
})
|
|
610
|
+
}),
|
|
451
611
|
);
|
|
452
612
|
|
|
453
613
|
// ─── Tool: mem_recent ────────────────────────────────────────────────────────
|
|
@@ -472,7 +632,8 @@ async function runRecent(db, args) {
|
|
|
472
632
|
let since = null;
|
|
473
633
|
if (args.date_since !== undefined) {
|
|
474
634
|
const d = parseDuration(args.date_since);
|
|
475
|
-
if (!d.ok)
|
|
635
|
+
if (!d.ok)
|
|
636
|
+
throw new Error(`Invalid date_since: "${args.date_since}" (use <N><unit>, e.g. 7d, 24h, 90m, 2w)`);
|
|
476
637
|
since = Date.now() - d.ms;
|
|
477
638
|
}
|
|
478
639
|
|
|
@@ -486,7 +647,9 @@ async function runRecent(db, args) {
|
|
|
486
647
|
|
|
487
648
|
const lines = [`Recent observations (${project || 'all'}):\n`];
|
|
488
649
|
for (const r of rows) {
|
|
489
|
-
lines.push(
|
|
650
|
+
lines.push(
|
|
651
|
+
`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}`,
|
|
652
|
+
);
|
|
490
653
|
}
|
|
491
654
|
lines.push(`\nWorkflow: mem_get(ids=[...]) for full details | mem_timeline(anchor=ID) for context`);
|
|
492
655
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
@@ -498,7 +661,7 @@ server.registerTool(
|
|
|
498
661
|
description: descriptionOf('mem_recent'),
|
|
499
662
|
inputSchema: memRecentSchema,
|
|
500
663
|
},
|
|
501
|
-
safeHandler(async (args) => runRecent(db, args))
|
|
664
|
+
safeHandler(async (args) => runRecent(db, args)),
|
|
502
665
|
);
|
|
503
666
|
|
|
504
667
|
// ─── Tool: mem_timeline ─────────────────────────────────────────────────────
|
|
@@ -552,7 +715,9 @@ server.registerTool(
|
|
|
552
715
|
|
|
553
716
|
const lines = [`Timeline (most recent ${rows.length}):\n`];
|
|
554
717
|
for (const r of rows.reverse()) {
|
|
555
|
-
lines.push(
|
|
718
|
+
lines.push(
|
|
719
|
+
`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}`,
|
|
720
|
+
);
|
|
556
721
|
}
|
|
557
722
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
558
723
|
}
|
|
@@ -567,11 +732,13 @@ server.registerTool(
|
|
|
567
732
|
const lines = [`Timeline around #${anchorId}${anchorNote ? ' ' + anchorNote : ''}:\n`];
|
|
568
733
|
for (const r of all) {
|
|
569
734
|
const marker = r.id === anchorId ? ' ◀' : '';
|
|
570
|
-
lines.push(
|
|
735
|
+
lines.push(
|
|
736
|
+
`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}${marker}`,
|
|
737
|
+
);
|
|
571
738
|
}
|
|
572
739
|
|
|
573
740
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
574
|
-
})
|
|
741
|
+
}),
|
|
575
742
|
);
|
|
576
743
|
|
|
577
744
|
// ─── Tool: mem_get ──────────────────────────────────────────────────────────
|
|
@@ -592,9 +759,17 @@ server.registerTool(
|
|
|
592
759
|
const { bySrc, invalid } = bucketIdTokens(rest, { explicit: args.source || null, defaultSource: 'obs' });
|
|
593
760
|
if (invalid.length > 0) {
|
|
594
761
|
// Should not happen — schema regex already rejected bad tokens — but guard defensively.
|
|
595
|
-
return {
|
|
762
|
+
return {
|
|
763
|
+
content: [
|
|
764
|
+
{
|
|
765
|
+
type: 'text',
|
|
766
|
+
text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, S#N, E#N, or D#N.`,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
};
|
|
596
770
|
}
|
|
597
|
-
const totalRequested =
|
|
771
|
+
const totalRequested =
|
|
772
|
+
bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length + deferredIds.length;
|
|
598
773
|
if (totalRequested === 0) {
|
|
599
774
|
return { content: [{ type: 'text', text: 'No valid IDs provided.' }] };
|
|
600
775
|
}
|
|
@@ -604,10 +779,12 @@ server.registerTool(
|
|
|
604
779
|
let fieldsNote = '';
|
|
605
780
|
let obsFieldFilter = null;
|
|
606
781
|
if (args.fields?.length && bySrc.obs.length > 0) {
|
|
607
|
-
const invalidFields = args.fields.filter(f => !OBS_FIELDS.includes(f));
|
|
608
|
-
const validFields = args.fields.filter(f => OBS_FIELDS.includes(f));
|
|
782
|
+
const invalidFields = args.fields.filter((f) => !OBS_FIELDS.includes(f));
|
|
783
|
+
const validFields = args.fields.filter((f) => OBS_FIELDS.includes(f));
|
|
609
784
|
if (validFields.length === 0) {
|
|
610
|
-
throw new Error(
|
|
785
|
+
throw new Error(
|
|
786
|
+
`No valid fields. Unknown field(s): ${invalidFields.join(', ')}. Valid: ${OBS_FIELDS.join(', ')}`,
|
|
787
|
+
);
|
|
611
788
|
}
|
|
612
789
|
if (invalidFields.length > 0) {
|
|
613
790
|
fieldsNote = `Note: unknown field(s) dropped: ${invalidFields.join(', ')}. Valid: ${OBS_FIELDS.join(', ')}`;
|
|
@@ -632,13 +809,16 @@ server.registerTool(
|
|
|
632
809
|
for (const f of renderFields) {
|
|
633
810
|
const val = row[f];
|
|
634
811
|
if (val === null || val === undefined || val === '') continue;
|
|
635
|
-
if (f === 'text' && row.narrative && typeof val === 'string' && val.startsWith(row.narrative))
|
|
812
|
+
if (f === 'text' && row.narrative && typeof val === 'string' && val.startsWith(row.narrative))
|
|
813
|
+
continue;
|
|
636
814
|
// Shared formatter (cli/common.mjs) renders epoch-ms time fields as
|
|
637
815
|
// `<ms> (<relative>)` — parity with the CLI `get` path so an LLM reader
|
|
638
816
|
// gets a scannable hint instead of a bare millisecond integer.
|
|
639
817
|
const display = formatObsFieldValue(f, val);
|
|
640
818
|
const maxLen = f === 'narrative' ? 1000 : f === 'lesson_learned' ? 500 : f === 'text' ? 500 : 200;
|
|
641
|
-
lines.push(
|
|
819
|
+
lines.push(
|
|
820
|
+
`${obsFieldLabel(f)}: ${typeof display === 'string' && display.length > maxLen ? display.slice(0, maxLen) + '…' : display}`,
|
|
821
|
+
);
|
|
642
822
|
}
|
|
643
823
|
sections.push(lines.join('\n'));
|
|
644
824
|
}
|
|
@@ -656,7 +836,9 @@ server.registerTool(
|
|
|
656
836
|
const val = row[f];
|
|
657
837
|
if (val === null || val === undefined || val === '') continue;
|
|
658
838
|
const maxLen = 500;
|
|
659
|
-
lines.push(
|
|
839
|
+
lines.push(
|
|
840
|
+
`${f}: ${typeof val === 'string' && val.length > maxLen ? val.slice(0, maxLen) + '…' : val}`,
|
|
841
|
+
);
|
|
660
842
|
}
|
|
661
843
|
sections.push(lines.join('\n'));
|
|
662
844
|
}
|
|
@@ -667,7 +849,7 @@ server.registerTool(
|
|
|
667
849
|
foundBySource.prompt.add(row.id);
|
|
668
850
|
const lines = [`── P#${row.id} ──`];
|
|
669
851
|
for (const f of PROMPT_DETAIL_FIELDS) {
|
|
670
|
-
if (f === 'id') continue;
|
|
852
|
+
if (f === 'id') continue; // already in the header
|
|
671
853
|
const val = row[f];
|
|
672
854
|
if (val === null || val === undefined || val === '') continue;
|
|
673
855
|
lines.push(`${f}: ${typeof val === 'string' && val.length > 500 ? val.slice(0, 500) + '…' : val}`);
|
|
@@ -683,7 +865,7 @@ server.registerTool(
|
|
|
683
865
|
foundBySource.event.add(row.id);
|
|
684
866
|
const lines = [`── E#${row.id} [${row.event_type}] ──`];
|
|
685
867
|
for (const f of EVENT_DETAIL_FIELDS) {
|
|
686
|
-
if (f === 'id' || f === 'event_type') continue;
|
|
868
|
+
if (f === 'id' || f === 'event_type') continue; // already in the header
|
|
687
869
|
const val = row[f];
|
|
688
870
|
if (val === null || val === undefined || val === '') continue;
|
|
689
871
|
lines.push(`${f}: ${typeof val === 'string' && val.length > 500 ? val.slice(0, 500) + '…' : val}`);
|
|
@@ -699,33 +881,59 @@ server.registerTool(
|
|
|
699
881
|
let deferredMissing = [];
|
|
700
882
|
if (deferredIds.length > 0) {
|
|
701
883
|
const dRows = getDeferredByIds(db, deferredIds);
|
|
702
|
-
const found = new Set(dRows.map(r => r.id));
|
|
703
|
-
deferredMissing = deferredIds.filter(id => !found.has(id));
|
|
884
|
+
const found = new Set(dRows.map((r) => r.id));
|
|
885
|
+
deferredMissing = deferredIds.filter((id) => !found.has(id));
|
|
704
886
|
deferredSections = dRows.map(formatDeferredDetail);
|
|
705
887
|
deferredFound = dRows.length;
|
|
706
888
|
}
|
|
707
889
|
|
|
708
|
-
const totalFound =
|
|
709
|
-
|
|
710
|
-
|
|
890
|
+
const totalFound =
|
|
891
|
+
foundBySource.obs.size +
|
|
892
|
+
foundBySource.session.size +
|
|
893
|
+
foundBySource.prompt.size +
|
|
894
|
+
foundBySource.event.size +
|
|
895
|
+
deferredFound;
|
|
896
|
+
|
|
897
|
+
if (
|
|
898
|
+
totalFound === 0 &&
|
|
899
|
+
deferredIds.length > 0 &&
|
|
900
|
+
bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length === 0
|
|
901
|
+
) {
|
|
711
902
|
// Deferred-only request, nothing found — the source-probe below is about
|
|
712
903
|
// obs/session/prompt/event and would render an empty source list.
|
|
713
|
-
return {
|
|
904
|
+
return {
|
|
905
|
+
content: [
|
|
906
|
+
{
|
|
907
|
+
type: 'text',
|
|
908
|
+
text: `Deferred item(s) not found: ${deferredMissing.map((i) => `D#${i}`).join(', ')}. List open items: mem_defer_list.`,
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
};
|
|
714
912
|
}
|
|
715
913
|
|
|
716
914
|
if (totalFound === 0) {
|
|
717
915
|
// Probe other sources so callers can retry with the right prefix/source override.
|
|
718
|
-
const queried = new Set(
|
|
916
|
+
const queried = new Set(
|
|
917
|
+
Object.entries(bySrc)
|
|
918
|
+
.filter(([, v]) => v.length > 0)
|
|
919
|
+
.map(([k]) => k),
|
|
920
|
+
);
|
|
719
921
|
const allNumericIds = [...bySrc.obs, ...bySrc.session, ...bySrc.prompt, ...bySrc.event];
|
|
720
922
|
const probe = probeIdSources(db, allNumericIds, queried);
|
|
721
923
|
const hints = [];
|
|
722
|
-
if (probe.obs.length > 0)
|
|
723
|
-
if (probe.session.length > 0)
|
|
724
|
-
|
|
725
|
-
if (probe.
|
|
924
|
+
if (probe.obs.length > 0) hints.push(`#${probe.obs.join(', #')} (obs — use source='obs' or bare #N)`);
|
|
925
|
+
if (probe.session.length > 0)
|
|
926
|
+
hints.push(`S#${probe.session.join(', S#')} (session — use source='session' or S#N)`);
|
|
927
|
+
if (probe.prompt.length > 0)
|
|
928
|
+
hints.push(`P#${probe.prompt.join(', P#')} (prompt — use source='prompt' or P#N)`);
|
|
929
|
+
if (probe.event.length > 0)
|
|
930
|
+
hints.push(`E#${probe.event.join(', E#')} (event — use source='event' or E#N)`);
|
|
726
931
|
const hint = hints.length > 0 ? ` Try: ${hints.join('; ')}.` : '';
|
|
727
932
|
const queriedList = [...queried].join(', ');
|
|
728
|
-
const deferredNote =
|
|
933
|
+
const deferredNote =
|
|
934
|
+
deferredMissing.length > 0
|
|
935
|
+
? ` Deferred item(s) not found: ${deferredMissing.map((i) => `D#${i}`).join(', ')}.`
|
|
936
|
+
: '';
|
|
729
937
|
const msg = `No records found in source(s) [${queriedList}] for the given ID(s).${deferredNote}${hint}`;
|
|
730
938
|
return { content: [{ type: 'text', text: fieldsNote ? `${msg}\n\n${fieldsNote}` : msg }] };
|
|
731
939
|
}
|
|
@@ -733,12 +941,12 @@ server.registerTool(
|
|
|
733
941
|
// Missing-ID note per bucket (mirrors mem_delete). Show missing IDs with their bucket prefix
|
|
734
942
|
// so callers can tell which source returned nothing.
|
|
735
943
|
const missingHints = [];
|
|
736
|
-
const miss = (arr, found, prefix) => arr.filter(id => !found.has(id)).map(id => `${prefix}${id}`);
|
|
944
|
+
const miss = (arr, found, prefix) => arr.filter((id) => !found.has(id)).map((id) => `${prefix}${id}`);
|
|
737
945
|
missingHints.push(...miss(bySrc.obs, foundBySource.obs, '#'));
|
|
738
946
|
missingHints.push(...miss(bySrc.session, foundBySource.session, 'S#'));
|
|
739
947
|
missingHints.push(...miss(bySrc.prompt, foundBySource.prompt, 'P#'));
|
|
740
948
|
missingHints.push(...miss(bySrc.event, foundBySource.event, 'E#'));
|
|
741
|
-
missingHints.push(...deferredMissing.map(id => `D#${id}`));
|
|
949
|
+
missingHints.push(...deferredMissing.map((id) => `D#${id}`));
|
|
742
950
|
|
|
743
951
|
const parts = [];
|
|
744
952
|
if (fieldsNote) parts.push(fieldsNote);
|
|
@@ -748,7 +956,7 @@ server.registerTool(
|
|
|
748
956
|
}
|
|
749
957
|
|
|
750
958
|
return { content: [{ type: 'text', text: parts.join('\n\n') }] };
|
|
751
|
-
})
|
|
959
|
+
}),
|
|
752
960
|
);
|
|
753
961
|
|
|
754
962
|
// ─── Tool: mem_delete ────────────────────────────────────────────────────────
|
|
@@ -778,12 +986,13 @@ server.registerTool(
|
|
|
778
986
|
// with the CLI `delete` path (was inlined + kept in sync by parity comments).
|
|
779
987
|
const result = deleteObservations(db, args.ids);
|
|
780
988
|
|
|
781
|
-
const missing = args.ids.filter(id => !rows.some(r => r.id === id));
|
|
989
|
+
const missing = args.ids.filter((id) => !rows.some((r) => r.id === id));
|
|
782
990
|
const msg = [`Deleted ${result.deleted} observation(s).`];
|
|
783
|
-
if (result.recoveredChildren > 0)
|
|
991
|
+
if (result.recoveredChildren > 0)
|
|
992
|
+
msg.push(`Recovered ${result.recoveredChildren} merged/compressed child observation(s) to live.`);
|
|
784
993
|
if (missing.length > 0) msg.push(`Note: ID(s) ${missing.join(', ')} not found.`);
|
|
785
994
|
return { content: [{ type: 'text', text: msg.join(' ') }] };
|
|
786
|
-
})
|
|
995
|
+
}),
|
|
787
996
|
);
|
|
788
997
|
|
|
789
998
|
// ─── Tool: mem_save ─────────────────────────────────────────────────────────
|
|
@@ -834,18 +1043,22 @@ server.registerTool(
|
|
|
834
1043
|
// The transaction body — dedup short-circuit BEFORE the resolver, and D#195's
|
|
835
1044
|
// allowStatuses — is lib/save-observation.mjs's (audit 2026-09-02 P1-6). Both faces
|
|
836
1045
|
// used to write it out and each carried a "kept in sync with the other" comment.
|
|
837
|
-
({ result, closesIds } = saveWithClosures(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1046
|
+
({ result, closesIds } = saveWithClosures(
|
|
1047
|
+
db,
|
|
1048
|
+
{
|
|
1049
|
+
// Size ceiling applied here rather than in lib/save-observation.mjs so the
|
|
1050
|
+
// shared pipeline keeps its "caller validates" contract (see its header).
|
|
1051
|
+
content: clampSaveText(args.content, SAVE_TEXT_LIMITS.content),
|
|
1052
|
+
title: clampSaveText(args.title, SAVE_TEXT_LIMITS.title),
|
|
1053
|
+
type: args.type || 'discovery',
|
|
1054
|
+
importance: args.importance,
|
|
1055
|
+
project,
|
|
1056
|
+
files: args.files || [],
|
|
1057
|
+
lesson_learned: clampSaveText(args.lesson_learned, SAVE_TEXT_LIMITS.lesson_learned),
|
|
1058
|
+
supersedes: args.supersedes,
|
|
1059
|
+
},
|
|
1060
|
+
{ closesTokens: args.closes_deferred, project },
|
|
1061
|
+
));
|
|
849
1062
|
} catch (e) {
|
|
850
1063
|
if (args.closes_deferred && args.closes_deferred.length > 0) {
|
|
851
1064
|
// Re-throw with a clearer prefix so MCP error response names the
|
|
@@ -853,7 +1066,7 @@ server.registerTool(
|
|
|
853
1066
|
// closesIds is closure-scoped and may not have been assigned before throw.
|
|
854
1067
|
throw new Error(`mem_save with closes_deferred failed: ${e.message}`, { cause: e });
|
|
855
1068
|
}
|
|
856
|
-
throw e;
|
|
1069
|
+
throw e; // unwrapped — preserves original message + stack
|
|
857
1070
|
}
|
|
858
1071
|
|
|
859
1072
|
if (result.kind === 'duplicate') {
|
|
@@ -864,20 +1077,26 @@ server.registerTool(
|
|
|
864
1077
|
}
|
|
865
1078
|
|
|
866
1079
|
const lessonNote = result.lessonCaptured ? ` 💡lesson captured` : '';
|
|
867
|
-
const closedNote =
|
|
868
|
-
|
|
869
|
-
|
|
1080
|
+
const closedNote =
|
|
1081
|
+
closesIds && closesIds.length > 0
|
|
1082
|
+
? ` Closed deferred: ${closesIds.map((i) => `D#${i}`).join(', ')}.`
|
|
1083
|
+
: '';
|
|
870
1084
|
const supersededNote = formatSupersededNote(result);
|
|
871
|
-
const nudge = buildLessonNudge({
|
|
1085
|
+
const nudge = buildLessonNudge({
|
|
1086
|
+
type: result.type,
|
|
1087
|
+
id: result.id,
|
|
1088
|
+
lessonCaptured: result.lessonCaptured,
|
|
1089
|
+
surface: 'mcp',
|
|
1090
|
+
});
|
|
872
1091
|
// G1+G2: detached backfill worker (lesson for obligated types + aliases for
|
|
873
1092
|
// every save) — fill-only-empty, so an agent acting on the nudge still wins.
|
|
874
|
-
const enrichNote =
|
|
875
|
-
? ' (background enrichment queued)' : '';
|
|
1093
|
+
const enrichNote =
|
|
1094
|
+
shouldQueueSaveEnrich(result) && queueSaveEnrich(result.id) ? ' (background enrichment queued)' : '';
|
|
876
1095
|
// D#201: on its own line rather than inside the success sentence.
|
|
877
1096
|
const skipNote = formatSupersedeSkipped(result.supersedeSkipped);
|
|
878
1097
|
const savedText = `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}${enrichNote}${nudge}`;
|
|
879
1098
|
return { content: [{ type: 'text', text: skipNote ? `${savedText}\n${skipNote}` : savedText }] };
|
|
880
|
-
})
|
|
1099
|
+
}),
|
|
881
1100
|
);
|
|
882
1101
|
|
|
883
1102
|
// ─── Tool: mem_defer ────────────────────────────────────────────────────────
|
|
@@ -901,10 +1120,16 @@ server.registerTool(
|
|
|
901
1120
|
// Compute the ordinal for the freshly-inserted row so the response is
|
|
902
1121
|
// immediately actionable ("ok, I deferred this as item 1").
|
|
903
1122
|
const open = listOpenWithOrdinal(db, project, 50);
|
|
904
|
-
const ord = open.find(o => o.id === r.id)?.ordinal ?? null;
|
|
905
|
-
return {
|
|
906
|
-
|
|
907
|
-
|
|
1123
|
+
const ord = open.find((o) => o.id === r.id)?.ordinal ?? null;
|
|
1124
|
+
return {
|
|
1125
|
+
content: [
|
|
1126
|
+
{
|
|
1127
|
+
type: 'text',
|
|
1128
|
+
text: `Deferred as D#${r.id} (item ${ord ?? '?'}) in project "${project}" — surfaces in next SessionStart banner.`,
|
|
1129
|
+
},
|
|
1130
|
+
],
|
|
1131
|
+
};
|
|
1132
|
+
}),
|
|
908
1133
|
);
|
|
909
1134
|
|
|
910
1135
|
// ─── Tool: mem_defer_list ───────────────────────────────────────────────────
|
|
@@ -931,7 +1156,7 @@ server.registerTool(
|
|
|
931
1156
|
// Affordance for the detail field — list stays title-only by design.
|
|
932
1157
|
lines.push(`Full detail: mem_get ids=["D#<id>"]`);
|
|
933
1158
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
934
|
-
})
|
|
1159
|
+
}),
|
|
935
1160
|
);
|
|
936
1161
|
|
|
937
1162
|
// ─── Tool: mem_defer_drop ───────────────────────────────────────────────────
|
|
@@ -957,7 +1182,7 @@ server.registerTool(
|
|
|
957
1182
|
const hint = formatDropReasonHint(args.reason);
|
|
958
1183
|
const dropText = `Dropped D#${realId} in project "${project}". Reason: ${args.reason}`;
|
|
959
1184
|
return { content: [{ type: 'text', text: hint ? `${dropText}\n${hint}` : dropText }] };
|
|
960
|
-
})
|
|
1185
|
+
}),
|
|
961
1186
|
);
|
|
962
1187
|
|
|
963
1188
|
// ─── Tool: mem_stats ────────────────────────────────────────────────────────
|
|
@@ -982,9 +1207,23 @@ server.registerTool(
|
|
|
982
1207
|
}
|
|
983
1208
|
|
|
984
1209
|
const {
|
|
985
|
-
obsTotal,
|
|
986
|
-
|
|
987
|
-
|
|
1210
|
+
obsTotal,
|
|
1211
|
+
sessTotal,
|
|
1212
|
+
promptTotal,
|
|
1213
|
+
obsRecent,
|
|
1214
|
+
sessRecent,
|
|
1215
|
+
types,
|
|
1216
|
+
projects,
|
|
1217
|
+
daily,
|
|
1218
|
+
tokenEst,
|
|
1219
|
+
avgImp,
|
|
1220
|
+
lowVal,
|
|
1221
|
+
lowSignalTitle,
|
|
1222
|
+
noiseRatio,
|
|
1223
|
+
lowSignalRatio,
|
|
1224
|
+
compressedCount,
|
|
1225
|
+
supersededOnlyCount,
|
|
1226
|
+
tierMap,
|
|
988
1227
|
} = computeStatsFeed(db, { project: args.project || null, days });
|
|
989
1228
|
|
|
990
1229
|
const lines = [
|
|
@@ -996,12 +1235,12 @@ server.registerTool(
|
|
|
996
1235
|
`Last ${days}d: ${obsRecent.c} observations | ${sessRecent.c} sessions`,
|
|
997
1236
|
'',
|
|
998
1237
|
'Type distribution (recent):',
|
|
999
|
-
...types.map(t => ` ${typeIcon(t.type)} ${t.type}: ${t.c}`),
|
|
1238
|
+
...types.map((t) => ` ${typeIcon(t.type)} ${t.type}: ${t.c}`),
|
|
1000
1239
|
'',
|
|
1001
|
-
...(projects.length ? ['Top projects:', ...projects.map(p => ` ${p.project}: ${p.c}`)] : []),
|
|
1240
|
+
...(projects.length ? ['Top projects:', ...projects.map((p) => ` ${p.project}: ${p.c}`)] : []),
|
|
1002
1241
|
'',
|
|
1003
1242
|
'Daily activity (last 7d):',
|
|
1004
|
-
...daily.map(d => ` ${d.day}: ${d.c} observations`),
|
|
1243
|
+
...daily.map((d) => ` ${d.day}: ${d.c} observations`),
|
|
1005
1244
|
'',
|
|
1006
1245
|
'Data Health:',
|
|
1007
1246
|
` Est. tokens: ${tokenEst.t ?? 0}`,
|
|
@@ -1009,7 +1248,9 @@ server.registerTool(
|
|
|
1009
1248
|
` Low-value (imp≤1, never used, >30d): ${lowVal.c} (${(noiseRatio * 100).toFixed(1)}% noise)`,
|
|
1010
1249
|
` Low-signal titles (Modified/Error/Worked on…): ${lowSignalTitle.c} (${(lowSignalRatio * 100).toFixed(1)}%)`,
|
|
1011
1250
|
` Compressed: ${compressedCount.c}`,
|
|
1012
|
-
...(
|
|
1251
|
+
...(noiseRatio > 0.6 || lowSignalRatio > 0.3
|
|
1252
|
+
? [' ⚠️ High noise ratio — consider running mem_compress / maintain']
|
|
1253
|
+
: []),
|
|
1013
1254
|
'',
|
|
1014
1255
|
// Tier counts only live (uncompressed, non-superseded) observations — surface
|
|
1015
1256
|
// the full decomposition so live + compressed + superseded = Total adds up cleanly.
|
|
@@ -1018,7 +1259,7 @@ server.registerTool(
|
|
|
1018
1259
|
];
|
|
1019
1260
|
|
|
1020
1261
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1021
|
-
})
|
|
1262
|
+
}),
|
|
1022
1263
|
);
|
|
1023
1264
|
|
|
1024
1265
|
// ─── Tool: mem_compress ──────────────────────────────────────────────────────
|
|
@@ -1055,7 +1296,9 @@ server.registerTool(
|
|
|
1055
1296
|
const [proj, week] = key.split('::');
|
|
1056
1297
|
const types = {};
|
|
1057
1298
|
for (const o of obs) types[o.type] = (types[o.type] || 0) + 1;
|
|
1058
|
-
const typeStr = Object.entries(types)
|
|
1299
|
+
const typeStr = Object.entries(types)
|
|
1300
|
+
.map(([t, c]) => `${c} ${t}`)
|
|
1301
|
+
.join(', ');
|
|
1059
1302
|
return ` ${proj} ${week}: ${obs.length} obs (${typeStr})`;
|
|
1060
1303
|
}),
|
|
1061
1304
|
'',
|
|
@@ -1074,8 +1317,15 @@ server.registerTool(
|
|
|
1074
1317
|
});
|
|
1075
1318
|
compress();
|
|
1076
1319
|
|
|
1077
|
-
return {
|
|
1078
|
-
|
|
1320
|
+
return {
|
|
1321
|
+
content: [
|
|
1322
|
+
{
|
|
1323
|
+
type: 'text',
|
|
1324
|
+
text: `Compressed ${totalCompressed} observations into ${compressableGroups.length} weekly summaries.`,
|
|
1325
|
+
},
|
|
1326
|
+
],
|
|
1327
|
+
};
|
|
1328
|
+
}),
|
|
1079
1329
|
);
|
|
1080
1330
|
|
|
1081
1331
|
// ─── Tool: mem_maintain ──────────────────────────────────────────────────────
|
|
@@ -1111,8 +1361,8 @@ server.registerTool(
|
|
|
1111
1361
|
formatPendingPurgeLine(stats.pendingPurge),
|
|
1112
1362
|
];
|
|
1113
1363
|
if (duplicates.length > 0) {
|
|
1114
|
-
const autoMergeable = duplicates.filter(d => parseFloat(d.similarity) >= AUTO_MERGE_THRESHOLD);
|
|
1115
|
-
const manualReview = duplicates.filter(d => parseFloat(d.similarity) < AUTO_MERGE_THRESHOLD);
|
|
1364
|
+
const autoMergeable = duplicates.filter((d) => parseFloat(d.similarity) >= AUTO_MERGE_THRESHOLD);
|
|
1365
|
+
const manualReview = duplicates.filter((d) => parseFloat(d.similarity) < AUTO_MERGE_THRESHOLD);
|
|
1116
1366
|
|
|
1117
1367
|
if (autoMergeable.length > 0) {
|
|
1118
1368
|
lines.push('', `Auto-mergeable pairs (similarity >= ${AUTO_MERGE_THRESHOLD}):`);
|
|
@@ -1120,21 +1370,29 @@ server.registerTool(
|
|
|
1120
1370
|
// Keep the higher-importance or newer observation
|
|
1121
1371
|
const keep = d.a.importance >= d.b.importance ? d.a : d.b;
|
|
1122
1372
|
const remove = keep === d.a ? d.b : d.a;
|
|
1123
|
-
lines.push(
|
|
1373
|
+
lines.push(
|
|
1374
|
+
` [${keep.id}] "${truncate(keep.title, 40)}" <-> [${remove.id}] "${truncate(remove.title, 40)}" (${d.similarity})`,
|
|
1375
|
+
);
|
|
1124
1376
|
}
|
|
1125
1377
|
// Build ready-to-use merge_ids for auto-mergeable pairs
|
|
1126
|
-
const mergeIds = autoMergeable.map(d => {
|
|
1378
|
+
const mergeIds = autoMergeable.map((d) => {
|
|
1127
1379
|
const keep = d.a.importance >= d.b.importance ? d.a : d.b;
|
|
1128
1380
|
const remove = keep === d.a ? d.b : d.a;
|
|
1129
1381
|
return [keep.id, remove.id];
|
|
1130
1382
|
});
|
|
1131
|
-
lines.push(
|
|
1383
|
+
lines.push(
|
|
1384
|
+
'',
|
|
1385
|
+
`Ready-to-use command:`,
|
|
1386
|
+
` mem_maintain(action="execute", operations=["dedup"], merge_ids=${JSON.stringify(mergeIds)})`,
|
|
1387
|
+
);
|
|
1132
1388
|
}
|
|
1133
1389
|
|
|
1134
1390
|
if (manualReview.length > 0) {
|
|
1135
1391
|
lines.push('', 'Needs review:');
|
|
1136
1392
|
for (const d of manualReview.slice(0, DUPLICATE_DISPLAY)) {
|
|
1137
|
-
lines.push(
|
|
1393
|
+
lines.push(
|
|
1394
|
+
` [${d.a.id}] "${truncate(d.a.title, 40)}" <-> [${d.b.id}] "${truncate(d.b.title, 40)}" (${d.similarity})`,
|
|
1395
|
+
);
|
|
1138
1396
|
}
|
|
1139
1397
|
}
|
|
1140
1398
|
}
|
|
@@ -1142,13 +1400,20 @@ server.registerTool(
|
|
|
1142
1400
|
}
|
|
1143
1401
|
|
|
1144
1402
|
if (action === 'execute') {
|
|
1145
|
-
const ops =
|
|
1146
|
-
? args.operations
|
|
1147
|
-
: resolveDefaultMaintainOps();
|
|
1403
|
+
const ops =
|
|
1404
|
+
args.operations && args.operations.length > 0 ? args.operations : resolveDefaultMaintainOps();
|
|
1148
1405
|
// T2-P1-A: reject explicit empty array (vs. omitted → defaults above). Empty-array
|
|
1149
1406
|
// callers are almost always mistakes; silently running only FTS5 optimize hides the error.
|
|
1150
1407
|
if (args.operations && args.operations.length === 0) {
|
|
1151
|
-
return {
|
|
1408
|
+
return {
|
|
1409
|
+
content: [
|
|
1410
|
+
{
|
|
1411
|
+
type: 'text',
|
|
1412
|
+
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.`,
|
|
1413
|
+
},
|
|
1414
|
+
],
|
|
1415
|
+
isError: true,
|
|
1416
|
+
};
|
|
1152
1417
|
}
|
|
1153
1418
|
const staleAge = Date.now() - STALE_AGE_MS;
|
|
1154
1419
|
const mctx = { projectFilter, baseParams, staleAge, opCap: OP_CAP };
|
|
@@ -1188,7 +1453,9 @@ server.registerTool(
|
|
|
1188
1453
|
lines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
1189
1454
|
}
|
|
1190
1455
|
lines.push(' Nothing was deleted. To delete, re-run with confirm=true:');
|
|
1191
|
-
lines.push(
|
|
1456
|
+
lines.push(
|
|
1457
|
+
` mem_maintain(action="execute", operations=${JSON.stringify(ops)}, confirm=true${args.retain_days ? `, retain_days=${args.retain_days}` : ''}${args.project ? `, project="${args.project}"` : ''})`,
|
|
1458
|
+
);
|
|
1192
1459
|
return lines.join('\n');
|
|
1193
1460
|
},
|
|
1194
1461
|
});
|
|
@@ -1196,8 +1463,11 @@ server.registerTool(
|
|
|
1196
1463
|
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
1197
1464
|
}
|
|
1198
1465
|
|
|
1199
|
-
return {
|
|
1200
|
-
|
|
1466
|
+
return {
|
|
1467
|
+
content: [{ type: 'text', text: `Unknown action: ${action}. Use "scan" or "execute".` }],
|
|
1468
|
+
isError: true,
|
|
1469
|
+
};
|
|
1470
|
+
}),
|
|
1201
1471
|
);
|
|
1202
1472
|
|
|
1203
1473
|
// ─── Tool: mem_optimize ────────────────────────────────────────────────────
|
|
@@ -1213,9 +1483,7 @@ server.registerTool(
|
|
|
1213
1483
|
|
|
1214
1484
|
if (action === 'preview') {
|
|
1215
1485
|
const preview = optimizePreview(db, { project: args.project, detail: args.detail === true });
|
|
1216
|
-
const lines = [
|
|
1217
|
-
`🔍 LLM Optimization Preview:`,
|
|
1218
|
-
];
|
|
1486
|
+
const lines = [`🔍 LLM Optimization Preview:`];
|
|
1219
1487
|
if (args.project) lines.push(` Project filter: ${args.project}`);
|
|
1220
1488
|
lines.push(
|
|
1221
1489
|
` Re-enrich candidates: ${preview.reenrich}`,
|
|
@@ -1229,13 +1497,18 @@ server.registerTool(
|
|
|
1229
1497
|
lines.push('', '─── Cluster-merge details ───');
|
|
1230
1498
|
for (const [i, cluster] of preview.mergeClusters.entries()) {
|
|
1231
1499
|
lines.push(` Cluster ${i + 1} (${cluster.length} obs, project=${cluster[0]?.project || '?'}):`);
|
|
1232
|
-
for (const obs of cluster)
|
|
1500
|
+
for (const obs of cluster)
|
|
1501
|
+
lines.push(
|
|
1502
|
+
` #${obs.id} [${obs.type || 'change'}] ${truncate(obs.title || '(untitled)', 100)}`,
|
|
1503
|
+
);
|
|
1233
1504
|
}
|
|
1234
1505
|
}
|
|
1235
1506
|
if (preview.reenrichSamples && preview.reenrichSamples.length > 0) {
|
|
1236
1507
|
lines.push('', '─── Re-enrich sample (first 20) ───');
|
|
1237
1508
|
for (const obs of preview.reenrichSamples) {
|
|
1238
|
-
lines.push(
|
|
1509
|
+
lines.push(
|
|
1510
|
+
` #${obs.id} [${obs.type || 'change'}] (project=${obs.project || '?'}) ${truncate(obs.title || '(untitled)', 100)}`,
|
|
1511
|
+
);
|
|
1239
1512
|
}
|
|
1240
1513
|
}
|
|
1241
1514
|
}
|
|
@@ -1254,16 +1527,28 @@ server.registerTool(
|
|
|
1254
1527
|
});
|
|
1255
1528
|
|
|
1256
1529
|
const lines = ['🔧 LLM Optimization Results:'];
|
|
1257
|
-
if (results.reenrich)
|
|
1530
|
+
if (results.reenrich)
|
|
1531
|
+
lines.push(
|
|
1532
|
+
` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`,
|
|
1533
|
+
);
|
|
1258
1534
|
if (results.normalize) {
|
|
1259
1535
|
if (results.normalize.skipped) lines.push(` Normalize: skipped (${results.normalize.reason})`);
|
|
1260
|
-
else
|
|
1536
|
+
else
|
|
1537
|
+
lines.push(
|
|
1538
|
+
` Normalize: ${results.normalize.processed || 0} updated, ${results.normalize.groups || 0} synonym groups`,
|
|
1539
|
+
);
|
|
1261
1540
|
}
|
|
1262
|
-
if (results.clusterMerge)
|
|
1263
|
-
|
|
1541
|
+
if (results.clusterMerge)
|
|
1542
|
+
lines.push(
|
|
1543
|
+
` Cluster-merge: ${results.clusterMerge.merged || 0} merged of ${results.clusterMerge.processed || 0} clusters`,
|
|
1544
|
+
);
|
|
1545
|
+
if (results.smartCompress)
|
|
1546
|
+
lines.push(
|
|
1547
|
+
` Smart-compress: ${results.smartCompress.compressed || 0} compressed of ${results.smartCompress.processed || 0} clusters`,
|
|
1548
|
+
);
|
|
1264
1549
|
|
|
1265
1550
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1266
|
-
})
|
|
1551
|
+
}),
|
|
1267
1552
|
);
|
|
1268
1553
|
|
|
1269
1554
|
// ─── Tool: mem_registry ─────────────────────────────────────────────────────
|
|
@@ -1277,7 +1562,10 @@ server.registerTool(
|
|
|
1277
1562
|
safeHandler(async (args) => {
|
|
1278
1563
|
const rdb = getRegistryDb();
|
|
1279
1564
|
if (!rdb) {
|
|
1280
|
-
return {
|
|
1565
|
+
return {
|
|
1566
|
+
content: [{ type: 'text', text: 'Registry DB not available. Run install first.' }],
|
|
1567
|
+
isError: true,
|
|
1568
|
+
};
|
|
1281
1569
|
}
|
|
1282
1570
|
|
|
1283
1571
|
const action = args.action;
|
|
@@ -1291,8 +1579,8 @@ server.registerTool(
|
|
|
1291
1579
|
limit: args.category || args.quality ? 20 : 10, // fetch more for post-filtering
|
|
1292
1580
|
});
|
|
1293
1581
|
// Apply category/quality filters if provided
|
|
1294
|
-
if (args.category) results = results.filter(r => r.category === args.category);
|
|
1295
|
-
if (args.quality) results = results.filter(r => r.quality_tier === args.quality);
|
|
1582
|
+
if (args.category) results = results.filter((r) => r.category === args.category);
|
|
1583
|
+
if (args.quality) results = results.filter((r) => r.quality_tier === args.quality);
|
|
1296
1584
|
// Prioritize directly invocable resources (with invocation_name) over community resources
|
|
1297
1585
|
results.sort((a, b) => {
|
|
1298
1586
|
const aInvocable = a.invocation_name ? 1 : 0;
|
|
@@ -1305,16 +1593,27 @@ server.registerTool(
|
|
|
1305
1593
|
return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };
|
|
1306
1594
|
}
|
|
1307
1595
|
const home = homedir();
|
|
1308
|
-
const lines = results.map(r => {
|
|
1309
|
-
const qualityBadge =
|
|
1596
|
+
const lines = results.map((r) => {
|
|
1597
|
+
const qualityBadge =
|
|
1598
|
+
r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
|
|
1310
1599
|
const categoryLabel = r.category ? ` [${r.category}]` : '';
|
|
1311
1600
|
// P2-6: the invocation rule is shared; only the Markdown bold and the two-space
|
|
1312
1601
|
// indent below are this face's.
|
|
1313
|
-
const { portablePath, howToUse } = resourceUseHint(r, {
|
|
1602
|
+
const { portablePath, howToUse } = resourceUseHint(r, {
|
|
1603
|
+
home,
|
|
1604
|
+
managedPrefix: join(DB_DIR, 'managed') + sep,
|
|
1605
|
+
});
|
|
1314
1606
|
const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
|
|
1315
1607
|
return `${qualityBadge} ${r.type === 'skill' ? 'S' : 'A'} **${r.name}**${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`;
|
|
1316
1608
|
});
|
|
1317
|
-
return {
|
|
1609
|
+
return {
|
|
1610
|
+
content: [
|
|
1611
|
+
{
|
|
1612
|
+
type: 'text',
|
|
1613
|
+
text: `Found ${results.length} resource(s) for "${args.query}":\n\n${lines.join('\n\n')}`,
|
|
1614
|
+
},
|
|
1615
|
+
],
|
|
1616
|
+
};
|
|
1318
1617
|
}
|
|
1319
1618
|
|
|
1320
1619
|
if (action === 'list') {
|
|
@@ -1332,7 +1631,7 @@ server.registerTool(
|
|
|
1332
1631
|
const lines = [
|
|
1333
1632
|
`Registry Stats:`,
|
|
1334
1633
|
` Total active: ${s.total}`,
|
|
1335
|
-
...s.byType.map(t => ` ${t.type}: ${t.c}`),
|
|
1634
|
+
...s.byType.map((t) => ` ${t.type}: ${t.c}`),
|
|
1336
1635
|
` User-added: ${s.userAdded}`,
|
|
1337
1636
|
` Zero adoption (recommended but never adopted): ${s.zeroAdopt}`,
|
|
1338
1637
|
];
|
|
@@ -1355,7 +1654,10 @@ server.registerTool(
|
|
|
1355
1654
|
const fields = {};
|
|
1356
1655
|
for (const f of IMPORT_STRING_FIELDS) fields[f] = args[f] || '';
|
|
1357
1656
|
const { id } = importResource(rdb, {
|
|
1358
|
-
name: args.name,
|
|
1657
|
+
name: args.name,
|
|
1658
|
+
type: args.resource_type,
|
|
1659
|
+
source: args.source,
|
|
1660
|
+
fields,
|
|
1359
1661
|
});
|
|
1360
1662
|
return { content: [{ type: 'text', text: `Imported: ${args.resource_type}:${args.name} (id=${id})` }] };
|
|
1361
1663
|
}
|
|
@@ -1365,7 +1667,11 @@ server.registerTool(
|
|
|
1365
1667
|
return { content: [{ type: 'text', text: 'remove requires name and resource_type' }], isError: true };
|
|
1366
1668
|
}
|
|
1367
1669
|
const { removed } = removeResource(rdb, { name: args.name, type: args.resource_type });
|
|
1368
|
-
return {
|
|
1670
|
+
return {
|
|
1671
|
+
content: [
|
|
1672
|
+
{ type: 'text', text: removed ? `Removed: ${args.resource_type}:${args.name}` : 'Not found.' },
|
|
1673
|
+
],
|
|
1674
|
+
};
|
|
1369
1675
|
}
|
|
1370
1676
|
|
|
1371
1677
|
if (action === 'reindex') {
|
|
@@ -1387,12 +1693,24 @@ server.registerTool(
|
|
|
1387
1693
|
let enrichMsg = '';
|
|
1388
1694
|
if (args.enrich) {
|
|
1389
1695
|
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1390
|
-
const { ok, denied } = await enrichImportedResources(rdb, results, {
|
|
1391
|
-
|
|
1696
|
+
const { ok, denied } = await enrichImportedResources(rdb, results, {
|
|
1697
|
+
confineTo: DB_DIR,
|
|
1698
|
+
enrichResource,
|
|
1699
|
+
});
|
|
1700
|
+
enrichMsg =
|
|
1701
|
+
`\nEnriched: ${ok}/${results.length}` +
|
|
1702
|
+
(denied ? ` (${denied} refused: path outside managed directory)` : '');
|
|
1392
1703
|
}
|
|
1393
1704
|
|
|
1394
|
-
const lines = results.map(r => `${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
|
|
1395
|
-
return {
|
|
1705
|
+
const lines = results.map((r) => `${r.type === 'skill' ? 'S' : 'A'} ${r.name} (id=${r.id})`);
|
|
1706
|
+
return {
|
|
1707
|
+
content: [
|
|
1708
|
+
{
|
|
1709
|
+
type: 'text',
|
|
1710
|
+
text: `Imported ${results.length} resource(s) from ${args.url}:\n${lines.join('\n')}${enrichMsg}`,
|
|
1711
|
+
},
|
|
1712
|
+
],
|
|
1713
|
+
};
|
|
1396
1714
|
} catch (e) {
|
|
1397
1715
|
return { content: [{ type: 'text', text: `Import failed: ${e.message}` }], isError: true };
|
|
1398
1716
|
}
|
|
@@ -1407,7 +1725,10 @@ server.registerTool(
|
|
|
1407
1725
|
// gate itself now lives in lib/registry-core.mjs so all four enrichment legs share it.
|
|
1408
1726
|
const { enrichResource } = await import('./registry-enricher.mjs');
|
|
1409
1727
|
try {
|
|
1410
|
-
const { status, error } = await enrichNamedResource(rdb, args.name, {
|
|
1728
|
+
const { status, error } = await enrichNamedResource(rdb, args.name, {
|
|
1729
|
+
confineTo: DB_DIR,
|
|
1730
|
+
enrichResource,
|
|
1731
|
+
});
|
|
1411
1732
|
if (status === 'not-found') {
|
|
1412
1733
|
return { content: [{ type: 'text', text: `Resource not found: ${args.name}` }], isError: true };
|
|
1413
1734
|
}
|
|
@@ -1415,21 +1736,42 @@ server.registerTool(
|
|
|
1415
1736
|
return { content: [{ type: 'text', text: `No local_path for ${args.name}` }], isError: true };
|
|
1416
1737
|
}
|
|
1417
1738
|
if (status === 'denied') {
|
|
1418
|
-
return {
|
|
1739
|
+
return {
|
|
1740
|
+
content: [{ type: 'text', text: `Access denied: path outside managed directory` }],
|
|
1741
|
+
isError: true,
|
|
1742
|
+
};
|
|
1419
1743
|
}
|
|
1420
1744
|
if (status === 'unreadable') {
|
|
1421
1745
|
// Pre-refactor this threw out of readFileSync into the catch below and rendered
|
|
1422
1746
|
// `Enrich error: <errno>`. Keep the errno: a stale local_path is the common cause.
|
|
1423
|
-
return {
|
|
1747
|
+
return {
|
|
1748
|
+
content: [{ type: 'text', text: `Enrich error: ${error?.message || 'cannot read local_path'}` }],
|
|
1749
|
+
isError: true,
|
|
1750
|
+
};
|
|
1424
1751
|
}
|
|
1425
|
-
return {
|
|
1752
|
+
return {
|
|
1753
|
+
content: [
|
|
1754
|
+
{
|
|
1755
|
+
type: 'text',
|
|
1756
|
+
text: status === 'enriched' ? `Enriched: ${args.name}` : `Enrichment failed for ${args.name}`,
|
|
1757
|
+
},
|
|
1758
|
+
],
|
|
1759
|
+
};
|
|
1426
1760
|
} catch (e) {
|
|
1427
1761
|
return { content: [{ type: 'text', text: `Enrich error: ${e.message}` }], isError: true };
|
|
1428
1762
|
}
|
|
1429
1763
|
}
|
|
1430
1764
|
|
|
1431
|
-
return {
|
|
1432
|
-
|
|
1765
|
+
return {
|
|
1766
|
+
content: [
|
|
1767
|
+
{
|
|
1768
|
+
type: 'text',
|
|
1769
|
+
text: `Unknown action: ${action}. Valid: search, list, stats, import, remove, reindex, import_url, enrich`,
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
isError: true,
|
|
1773
|
+
};
|
|
1774
|
+
}),
|
|
1433
1775
|
);
|
|
1434
1776
|
|
|
1435
1777
|
// ─── Tool: mem_use ──────────────────────────────────────────────────────────
|
|
@@ -1455,13 +1797,17 @@ server.registerTool(
|
|
|
1455
1797
|
const type = args.type || 'skill';
|
|
1456
1798
|
|
|
1457
1799
|
// 1. Exact match by name or invocation_name — the ONLY path that loads content.
|
|
1458
|
-
const row = rdb
|
|
1800
|
+
const row = rdb
|
|
1801
|
+
.prepare(
|
|
1802
|
+
`
|
|
1459
1803
|
SELECT id, name, type, local_path, invocation_name, capability_summary
|
|
1460
1804
|
FROM resources
|
|
1461
1805
|
WHERE status = 'active' AND type = ?
|
|
1462
1806
|
AND (name = ? OR invocation_name = ?)
|
|
1463
1807
|
LIMIT 1
|
|
1464
|
-
|
|
1808
|
+
`,
|
|
1809
|
+
)
|
|
1810
|
+
.get(type, name, name);
|
|
1465
1811
|
|
|
1466
1812
|
// 2. Name miss → SUGGEST, never substitute. The FTS5 search still runs (it is what
|
|
1467
1813
|
// produces the candidate list), but its result is only ever rendered as names: loading
|
|
@@ -1473,8 +1819,13 @@ server.registerTool(
|
|
|
1473
1819
|
// exact-name decision the caller makes.
|
|
1474
1820
|
if (!row) {
|
|
1475
1821
|
let candidates = [];
|
|
1476
|
-
try {
|
|
1477
|
-
|
|
1822
|
+
try {
|
|
1823
|
+
candidates = searchResources(rdb, name, { type, limit: 5 })
|
|
1824
|
+
.map((r) => r.name)
|
|
1825
|
+
.filter(Boolean);
|
|
1826
|
+
} catch {
|
|
1827
|
+
/* a suggestion is best-effort; the miss message below still stands */
|
|
1828
|
+
}
|
|
1478
1829
|
// Every echo of the caller's own name below is bounded + delimiter-inert (audit F7):
|
|
1479
1830
|
// raw interpolation let a crafted `name` forge a <skill-loaded> block and the execute
|
|
1480
1831
|
// imperative inside this message, and the handler-wide defangResult cannot catch it —
|
|
@@ -1490,19 +1841,26 @@ server.registerTool(
|
|
|
1490
1841
|
return { content: [{ type: 'text', text: `${head} Try ${browse} to browse.` }] };
|
|
1491
1842
|
}
|
|
1492
1843
|
const list = echoedCandidates.map((n) => ` - ${n}`).join('\n');
|
|
1493
|
-
return {
|
|
1494
|
-
|
|
1495
|
-
|
|
1844
|
+
return {
|
|
1845
|
+
content: [
|
|
1846
|
+
{
|
|
1847
|
+
type: 'text',
|
|
1848
|
+
text:
|
|
1849
|
+
`${head} Closest ${type}s by search (NOT loaded — none matched the name you asked for):\n${list}\n\n` +
|
|
1850
|
+
`Load one deliberately with its exact name, e.g. mem_use(name="${echoedCandidates[0]}"${type === 'skill' ? '' : `, type="${type}"`}), or browse with ${browse}.`,
|
|
1851
|
+
},
|
|
1852
|
+
],
|
|
1853
|
+
};
|
|
1496
1854
|
}
|
|
1497
1855
|
|
|
1498
1856
|
// 3. Resolve path: directory skills → SKILL.md (agents always have full .md paths)
|
|
1499
1857
|
let skillPath = row.local_path || '';
|
|
1500
1858
|
if (skillPath && !skillPath.endsWith('.md')) {
|
|
1501
|
-
for (const candidate of [
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1859
|
+
for (const candidate of [join(skillPath, 'SKILL.md'), join(skillPath, `skills/${row.name}/SKILL.md`)]) {
|
|
1860
|
+
if (existsSync(candidate)) {
|
|
1861
|
+
skillPath = candidate;
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1506
1864
|
}
|
|
1507
1865
|
}
|
|
1508
1866
|
|
|
@@ -1511,7 +1869,10 @@ server.registerTool(
|
|
|
1511
1869
|
// equals homedir when unset, so this does not weaken the non-relocated confinement.
|
|
1512
1870
|
const managedBase = DB_DIR;
|
|
1513
1871
|
if (skillPath && !isPathConfined(skillPath, managedBase)) {
|
|
1514
|
-
return {
|
|
1872
|
+
return {
|
|
1873
|
+
content: [{ type: 'text', text: `Access denied: path "${skillPath}" is outside managed directory` }],
|
|
1874
|
+
isError: true,
|
|
1875
|
+
};
|
|
1515
1876
|
}
|
|
1516
1877
|
|
|
1517
1878
|
// 5. Read content
|
|
@@ -1527,17 +1888,31 @@ server.registerTool(
|
|
|
1527
1888
|
|
|
1528
1889
|
// 5. Record invocation
|
|
1529
1890
|
try {
|
|
1530
|
-
rdb
|
|
1891
|
+
rdb
|
|
1892
|
+
.prepare(
|
|
1893
|
+
`
|
|
1531
1894
|
INSERT INTO invocations (resource_id, session_id, trigger, adopted, outcome)
|
|
1532
1895
|
VALUES (?, ?, 'user_explicit', 1, 'success')
|
|
1533
|
-
|
|
1534
|
-
|
|
1896
|
+
`,
|
|
1897
|
+
)
|
|
1898
|
+
.run(row.id, process.env.CLAUDE_SESSION_ID || 'unknown');
|
|
1899
|
+
} catch {
|
|
1900
|
+
/* non-critical */
|
|
1901
|
+
}
|
|
1535
1902
|
|
|
1536
1903
|
const _home = homedir();
|
|
1537
|
-
const portablePath =
|
|
1904
|
+
const portablePath =
|
|
1905
|
+
skillPath && skillPath.startsWith(_home) ? '~' + skillPath.slice(_home.length) : skillPath || '';
|
|
1538
1906
|
const pathAttr = portablePath ? ` path="${portablePath}"` : '';
|
|
1539
1907
|
const reloadHint = portablePath ? ` Reload: Read("${portablePath}")` : '';
|
|
1540
|
-
return {
|
|
1908
|
+
return {
|
|
1909
|
+
content: [
|
|
1910
|
+
{
|
|
1911
|
+
type: 'text',
|
|
1912
|
+
text: `<skill-loaded name="${row.name}" type="${row.type}"${pathAttr}>\n${content}\n</skill-loaded>\n\nFollow the instructions above to execute this ${row.type}.${reloadHint}`,
|
|
1913
|
+
},
|
|
1914
|
+
],
|
|
1915
|
+
};
|
|
1541
1916
|
}),
|
|
1542
1917
|
);
|
|
1543
1918
|
|
|
@@ -1553,18 +1928,26 @@ server.registerTool(
|
|
|
1553
1928
|
// `obs_type` → `type`, same alias mem_save takes — see the schema comment.
|
|
1554
1929
|
args = applyArgAliases(args, { obs_type: 'type' });
|
|
1555
1930
|
const obs = db.prepare('SELECT id, title FROM observations WHERE id = ?').get(args.id);
|
|
1556
|
-
if (!obs)
|
|
1931
|
+
if (!obs)
|
|
1932
|
+
return { content: [{ type: 'text', text: `Observation #${args.id} not found` }], isError: true };
|
|
1557
1933
|
|
|
1558
1934
|
// Shared mutation (lib/observation-write applyObsUpdate, P2-12): scrub + UPDATE +
|
|
1559
1935
|
// derived-column rebuild in one transaction — single source with CLI cmdUpdate.
|
|
1560
1936
|
const updatedCols = applyObsUpdate(db, args.id, {
|
|
1561
|
-
title: args.title,
|
|
1562
|
-
|
|
1937
|
+
title: args.title,
|
|
1938
|
+
narrative: args.narrative,
|
|
1939
|
+
type: args.type,
|
|
1940
|
+
importance: args.importance,
|
|
1941
|
+
lesson_learned: args.lesson_learned,
|
|
1942
|
+
concepts: args.concepts,
|
|
1563
1943
|
});
|
|
1564
|
-
if (updatedCols.length === 0)
|
|
1944
|
+
if (updatedCols.length === 0)
|
|
1945
|
+
return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
|
|
1565
1946
|
|
|
1566
|
-
return {
|
|
1567
|
-
|
|
1947
|
+
return {
|
|
1948
|
+
content: [{ type: 'text', text: `Updated observation #${args.id}: ${updatedCols.join(', ')}` }],
|
|
1949
|
+
};
|
|
1950
|
+
}),
|
|
1568
1951
|
);
|
|
1569
1952
|
|
|
1570
1953
|
// ─── Tool: mem_export ────────────────────────────────────────────────────────
|
|
@@ -1589,7 +1972,8 @@ async function runExport(db, args) {
|
|
|
1589
1972
|
let toEpoch = null;
|
|
1590
1973
|
if (args.date_from) {
|
|
1591
1974
|
fromEpoch = new Date(args.date_from).getTime();
|
|
1592
|
-
if (isNaN(fromEpoch))
|
|
1975
|
+
if (isNaN(fromEpoch))
|
|
1976
|
+
throw new Error(`Invalid date_from: "${args.date_from}" (use ISO 8601 or YYYY-MM-DD)`);
|
|
1593
1977
|
}
|
|
1594
1978
|
if (args.date_to) {
|
|
1595
1979
|
const d = args.date_to.length === 10 ? args.date_to + 'T23:59:59.999Z' : args.date_to;
|
|
@@ -1615,7 +1999,8 @@ async function runExport(db, args) {
|
|
|
1615
1999
|
includeCompressed: Boolean(args.include_compressed),
|
|
1616
2000
|
project: args.project ? _resolveProjectShared(db, args.project) : null,
|
|
1617
2001
|
type: args.type || null,
|
|
1618
|
-
fromEpoch,
|
|
2002
|
+
fromEpoch,
|
|
2003
|
+
toEpoch,
|
|
1619
2004
|
});
|
|
1620
2005
|
// No clamp (audit 2026-08-14 A2): `Math.min(args.limit ?? 200, 1000)` made an MCP-driven
|
|
1621
2006
|
// backup of a >1000-row store impossible, on the tool whose own description says "USE
|
|
@@ -1629,15 +2014,17 @@ async function runExport(db, args) {
|
|
|
1629
2014
|
// EXPORT_COLUMNS_SQL: shared with CLI cmdExport — the full round-trippable set restore
|
|
1630
2015
|
// reads back (v3.42 HIGH-2: this handler used to carry a narrower 16-col SELECT, silently
|
|
1631
2016
|
// dropping text/aliases/citation-signals on the advertised MCP backup→restore flow).
|
|
1632
|
-
const probed = db
|
|
2017
|
+
const probed = db
|
|
2018
|
+
.prepare(`SELECT ${EXPORT_COLUMNS_SQL} FROM observations ${where} ORDER BY created_at_epoch DESC LIMIT ?`)
|
|
2019
|
+
.all(...params, exportLimit + 1);
|
|
1633
2020
|
const rows = probed.slice(0, exportLimit);
|
|
1634
2021
|
const moreAvailable = probed.length > exportLimit;
|
|
1635
2022
|
|
|
1636
|
-
if (rows.length === 0)
|
|
2023
|
+
if (rows.length === 0)
|
|
2024
|
+
return { content: [{ type: 'text', text: 'No observations found matching the criteria.' }] };
|
|
1637
2025
|
|
|
1638
|
-
const output =
|
|
1639
|
-
? rows.map(r => JSON.stringify(r)).join('\n')
|
|
1640
|
-
: JSON.stringify(rows, null, 2);
|
|
2026
|
+
const output =
|
|
2027
|
+
args.format === 'jsonl' ? rows.map((r) => JSON.stringify(r)).join('\n') : JSON.stringify(rows, null, 2);
|
|
1641
2028
|
|
|
1642
2029
|
// A truncated backup is the failure mode this tool must never produce quietly: the old
|
|
1643
2030
|
// note ("Results capped at N … increase limit (max 1000)") never said how much was
|
|
@@ -1656,7 +2043,8 @@ async function runExport(db, args) {
|
|
|
1656
2043
|
let cap = '';
|
|
1657
2044
|
if (moreAvailable) {
|
|
1658
2045
|
const total = db.prepare(`SELECT COUNT(*) AS c FROM observations ${where}`).get(...params).c;
|
|
1659
|
-
cap =
|
|
2046
|
+
cap =
|
|
2047
|
+
`\nWARNING — PARTIAL EXPORT, NOT A COMPLETE BACKUP: capped at ${exportLimit} of ${total} matching observations; ${total - exportLimit} rows are missing from this payload and restoring it would lose them.` +
|
|
1660
2048
|
`\nFor a complete backup, write it to a FILE instead of pulling it through this conversation: \`${CLI_INVOKE} export --format jsonl > backup.jsonl\` (the CLI exports the complete set by default). Narrowing with date_from/date_to also works.` +
|
|
1661
2049
|
`\nRaising \`limit\` here is the last resort, not the first: this result is model context, so all ${total} rows would be loaded into the transcript.`;
|
|
1662
2050
|
}
|
|
@@ -1671,10 +2059,9 @@ server.registerTool(
|
|
|
1671
2059
|
},
|
|
1672
2060
|
// verbatim: the export payload feeds `restore` — defanging it would silently
|
|
1673
2061
|
// rewrite backed-up rows whose text legitimately contains these tags.
|
|
1674
|
-
safeHandler(
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
)
|
|
2062
|
+
safeHandler(async (args) => runExport(db, applyArgAliases(args, { from: 'date_from', to: 'date_to' })), {
|
|
2063
|
+
verbatim: true,
|
|
2064
|
+
}),
|
|
1678
2065
|
);
|
|
1679
2066
|
|
|
1680
2067
|
// ─── Tool: mem_recall ────────────────────────────────────────────────────────
|
|
@@ -1693,17 +2080,23 @@ server.registerTool(
|
|
|
1693
2080
|
});
|
|
1694
2081
|
|
|
1695
2082
|
if (rows.length === 0) {
|
|
1696
|
-
return {
|
|
2083
|
+
return {
|
|
2084
|
+
content: [
|
|
2085
|
+
{ type: 'text', text: `No history for "${filename}". This file hasn't been observed yet.` },
|
|
2086
|
+
],
|
|
2087
|
+
};
|
|
1697
2088
|
}
|
|
1698
2089
|
|
|
1699
2090
|
const lines = [`History for ${filename} (${rows.length} observation${rows.length !== 1 ? 's' : ''}):\n`];
|
|
1700
2091
|
for (const r of rows) {
|
|
1701
2092
|
const lesson = r.lesson_learned ? `\n Lesson: ${truncate(r.lesson_learned, 100)}` : '';
|
|
1702
|
-
lines.push(
|
|
2093
|
+
lines.push(
|
|
2094
|
+
`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}${lesson}`,
|
|
2095
|
+
);
|
|
1703
2096
|
}
|
|
1704
2097
|
lines.push(`\nWorkflow: mem_get(ids=[...]) for full details`);
|
|
1705
2098
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1706
|
-
})
|
|
2099
|
+
}),
|
|
1707
2100
|
);
|
|
1708
2101
|
|
|
1709
2102
|
// ─── Tool: mem_fts_check ─────────────────────────────────────────────────────
|
|
@@ -1717,7 +2110,7 @@ server.registerTool(
|
|
|
1717
2110
|
description: descriptionOf('mem_fts_check'),
|
|
1718
2111
|
inputSchema: memFtsCheckSchema,
|
|
1719
2112
|
},
|
|
1720
|
-
safeHandler(async (args) => handleMemFtsCheck(db, args))
|
|
2113
|
+
safeHandler(async (args) => handleMemFtsCheck(db, args)),
|
|
1721
2114
|
);
|
|
1722
2115
|
|
|
1723
2116
|
// ─── Tool: mem_browse ────────────────────────────────────────────────────────
|
|
@@ -1737,7 +2130,10 @@ server.registerTool(
|
|
|
1737
2130
|
|
|
1738
2131
|
// Shared collection (lib/browse-core, P2-12) — single source with CLI browse.
|
|
1739
2132
|
const { showTiers, tierData, tierCounts, grandTotal } = collectBrowseTiers(db, {
|
|
1740
|
-
project,
|
|
2133
|
+
project,
|
|
2134
|
+
tierFilter,
|
|
2135
|
+
limit,
|
|
2136
|
+
now,
|
|
1741
2137
|
currentSessionId: getActiveMemorySessionId(db, project),
|
|
1742
2138
|
});
|
|
1743
2139
|
const tiers = BROWSE_TIERS;
|
|
@@ -1754,26 +2150,33 @@ server.registerTool(
|
|
|
1754
2150
|
continue;
|
|
1755
2151
|
}
|
|
1756
2152
|
|
|
1757
|
-
if (count === 0) {
|
|
2153
|
+
if (count === 0) {
|
|
2154
|
+
lines.push('');
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
1758
2157
|
|
|
1759
2158
|
for (const r of rows) {
|
|
1760
|
-
lines.push(
|
|
2159
|
+
lines.push(
|
|
2160
|
+
` #${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)', 80)} | ${fmtDate(r.created_at)}`,
|
|
2161
|
+
);
|
|
1761
2162
|
}
|
|
1762
2163
|
if (count > rows.length) lines.push(` ... and ${count - rows.length} more`);
|
|
1763
2164
|
lines.push('');
|
|
1764
2165
|
}
|
|
1765
2166
|
|
|
1766
2167
|
if (grandTotal === 0) {
|
|
1767
|
-
return {
|
|
2168
|
+
return {
|
|
2169
|
+
content: [{ type: 'text', text: 'No observations found. Start a coding session to build memory.' }],
|
|
2170
|
+
};
|
|
1768
2171
|
}
|
|
1769
2172
|
|
|
1770
2173
|
if (!tierFilter) {
|
|
1771
|
-
const parts = tiers.map(t => `${t[0].toUpperCase() + t.slice(1)}: ${tierCounts[t] ?? 0}`);
|
|
2174
|
+
const parts = tiers.map((t) => `${t[0].toUpperCase() + t.slice(1)}: ${tierCounts[t] ?? 0}`);
|
|
1772
2175
|
lines.push(`Totals: ${grandTotal} observations | ${parts.join(' | ')}`);
|
|
1773
2176
|
}
|
|
1774
2177
|
|
|
1775
2178
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1776
|
-
})
|
|
2179
|
+
}),
|
|
1777
2180
|
);
|
|
1778
2181
|
|
|
1779
2182
|
// ─── Hidden tool filter ─────────────────────────────────────────────────────
|
|
@@ -1793,9 +2196,7 @@ server.registerTool(
|
|
|
1793
2196
|
// - `enabled` stays true, so `tools/call` keeps routing normally — per
|
|
1794
2197
|
// mcp.js line 106, a `disabled` tool would reject calls too.
|
|
1795
2198
|
|
|
1796
|
-
const HIDDEN_TOOL_NAMES = new Set(
|
|
1797
|
-
TOOL_DEFS.filter((t) => t.hidden === true).map((t) => t.name),
|
|
1798
|
-
);
|
|
2199
|
+
const HIDDEN_TOOL_NAMES = new Set(TOOL_DEFS.filter((t) => t.hidden === true).map((t) => t.name));
|
|
1799
2200
|
|
|
1800
2201
|
// Opt-out: setting CLAUDE_MEM_ALL_TOOLS=1 restores pre-v2.34.0 behavior where
|
|
1801
2202
|
// every registered tool is visible in `tools/list`. Users who relied on Claude
|
|
@@ -1833,7 +2234,11 @@ if (!effectiveQuiet()) {
|
|
|
1833
2234
|
// Checkpoint WAL every 5 minutes to prevent unbounded growth
|
|
1834
2235
|
const WAL_CHECKPOINT_INTERVAL = 5 * 60 * 1000;
|
|
1835
2236
|
const walTimer = setInterval(() => {
|
|
1836
|
-
try {
|
|
2237
|
+
try {
|
|
2238
|
+
db.pragma('wal_checkpoint(PASSIVE)');
|
|
2239
|
+
} catch (e) {
|
|
2240
|
+
debugCatch(e, 'walCheckpoint');
|
|
2241
|
+
}
|
|
1837
2242
|
}, WAL_CHECKPOINT_INTERVAL);
|
|
1838
2243
|
walTimer.unref(); // Don't keep process alive just for checkpoints
|
|
1839
2244
|
|
|
@@ -1868,15 +2273,27 @@ idleTimer.unref();
|
|
|
1868
2273
|
function shutdown(exitCode = 0) {
|
|
1869
2274
|
clearInterval(walTimer);
|
|
1870
2275
|
clearInterval(idleTimer);
|
|
1871
|
-
try {
|
|
1872
|
-
|
|
1873
|
-
|
|
2276
|
+
try {
|
|
2277
|
+
if (db) db.pragma('wal_checkpoint(TRUNCATE)');
|
|
2278
|
+
} catch {}
|
|
2279
|
+
try {
|
|
2280
|
+
if (db) db.close();
|
|
2281
|
+
} catch {}
|
|
2282
|
+
try {
|
|
2283
|
+
if (registryDb) registryDb.close();
|
|
2284
|
+
} catch {}
|
|
1874
2285
|
process.exit(exitCode);
|
|
1875
2286
|
}
|
|
1876
2287
|
process.on('SIGINT', () => shutdown(0));
|
|
1877
2288
|
process.on('SIGTERM', () => shutdown(0));
|
|
1878
|
-
process.on('uncaughtException', (err) => {
|
|
1879
|
-
|
|
2289
|
+
process.on('uncaughtException', (err) => {
|
|
2290
|
+
debugCatch(err, 'uncaughtException');
|
|
2291
|
+
shutdown(1);
|
|
2292
|
+
});
|
|
2293
|
+
process.on('unhandledRejection', (err) => {
|
|
2294
|
+
debugCatch(err, 'unhandledRejection');
|
|
2295
|
+
shutdown(1);
|
|
2296
|
+
});
|
|
1880
2297
|
|
|
1881
2298
|
// ─── Runtime Dir Retention + Permissions ────────────────────────────────────
|
|
1882
2299
|
|
|
@@ -1908,13 +2325,17 @@ export function pruneSpawnLog(path, nowMs = Date.now()) {
|
|
|
1908
2325
|
try {
|
|
1909
2326
|
const { ts } = JSON.parse(l);
|
|
1910
2327
|
return typeof ts === 'string' && ts >= cutoffIso;
|
|
1911
|
-
} catch {
|
|
2328
|
+
} catch {
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
1912
2331
|
});
|
|
1913
2332
|
if (kept.length > SPAWN_LOG_MAX_LINES) kept = kept.slice(-SPAWN_LOG_MAX_LINES);
|
|
1914
2333
|
if (kept.length === lines.length) return 0;
|
|
1915
2334
|
writeFileSync(path, kept.length ? kept.join('\n') + '\n' : '', { mode: 0o600 });
|
|
1916
2335
|
return lines.length - kept.length;
|
|
1917
|
-
} catch {
|
|
2336
|
+
} catch {
|
|
2337
|
+
return 0;
|
|
2338
|
+
}
|
|
1918
2339
|
}
|
|
1919
2340
|
|
|
1920
2341
|
/**
|
|
@@ -1935,10 +2356,17 @@ export function hardenRuntimeFiles(dir) {
|
|
|
1935
2356
|
let touched = 0;
|
|
1936
2357
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1937
2358
|
if (!entry.isFile()) continue;
|
|
1938
|
-
try {
|
|
2359
|
+
try {
|
|
2360
|
+
chmodSync(join(dir, entry.name), 0o600);
|
|
2361
|
+
touched++;
|
|
2362
|
+
} catch {
|
|
2363
|
+
/* per-entry, silent */
|
|
2364
|
+
}
|
|
1939
2365
|
}
|
|
1940
2366
|
return touched;
|
|
1941
|
-
} catch {
|
|
2367
|
+
} catch {
|
|
2368
|
+
return 0;
|
|
2369
|
+
}
|
|
1942
2370
|
}
|
|
1943
2371
|
|
|
1944
2372
|
// ─── Start Server ───────────────────────────────────────────────────────────
|
|
@@ -1956,19 +2384,22 @@ export function hardenRuntimeFiles(dir) {
|
|
|
1956
2384
|
if (process.env.MEM_DISABLE_SPAWN_LOG !== '1') {
|
|
1957
2385
|
try {
|
|
1958
2386
|
if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true, mode: 0o700 });
|
|
1959
|
-
const line =
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2387
|
+
const line =
|
|
2388
|
+
JSON.stringify({
|
|
2389
|
+
ts: new Date().toISOString(),
|
|
2390
|
+
pid: process.pid,
|
|
2391
|
+
ppid: process.ppid,
|
|
2392
|
+
argv1: process.argv[1] || '',
|
|
2393
|
+
version: PKG_VERSION,
|
|
2394
|
+
}) + '\n';
|
|
1966
2395
|
const spawnLog = join(RUNTIME_DIR, 'mcp-spawns.log');
|
|
1967
2396
|
appendFileSync(spawnLog, line, { mode: 0o600 });
|
|
1968
2397
|
// Lazy GC on append, mirroring lib/hook-telemetry.pruneOldShards. Single-file
|
|
1969
2398
|
// sink (not day-sharded) so retention is applied line-wise instead.
|
|
1970
2399
|
pruneSpawnLog(spawnLog);
|
|
1971
|
-
} catch {
|
|
2400
|
+
} catch {
|
|
2401
|
+
/* never block startup on telemetry failure */
|
|
2402
|
+
}
|
|
1972
2403
|
}
|
|
1973
2404
|
|
|
1974
2405
|
// Owner-only for the whole runtime dir. Sibling aux files carry captured file
|