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.
Files changed (142) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +134 -45
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +85 -124
  23. package/hook-update.mjs +262 -87
  24. package/hook.mjs +1169 -606
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +49 -26
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/user-prompt-search.js +238 -97
  130. package/search-engine.mjs +329 -96
  131. package/search-scoring.mjs +75 -33
  132. package/secret-scrub.mjs +48 -12
  133. package/server/fts-check.mjs +12 -9
  134. package/server.mjs +674 -243
  135. package/skip-tools.mjs +19 -8
  136. package/source-files.mjs +54 -21
  137. package/stop-words.mjs +152 -19
  138. package/synonyms.mjs +579 -164
  139. package/tfidf.mjs +130 -40
  140. package/tier.mjs +1 -1
  141. package/tool-schemas.mjs +381 -131
  142. 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
- runMaintainOps, findDuplicates, maintenanceStats,
19
- OP_CAP, STALE_AGE_MS, resolveDefaultMaintainOps, DEFAULT_MAINTAIN_OPS,
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 { fetchObsDetail, fetchPromptDetail, fetchEventDetail, fetchSessionDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
25
- import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
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 { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
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 { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
48
- import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichImportedResources, enrichNamedResource, resourceUseHint } from './lib/registry-core.mjs';
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 { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
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, listOpenWithOrdinal, dropDeferred, formatDropReasonHint,
128
+ insertDeferred,
129
+ listOpenWithOrdinal,
130
+ dropDeferred,
131
+ formatDropReasonHint,
59
132
  resolveDeferredIds,
60
- getDeferredByIds, formatDeferredDetail,
61
- searchDeferredWork, formatDeferredSearchTrailer,
62
- formatDeferListRow, countStaleOpen, formatDeferStaleHint,
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 { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, chmodSync } from 'fs';
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(`[claude-mem-lite] Try: rm "${DB_PATH}-wal" "${DB_PATH}-shm" or reinstall with: node install.mjs install`);
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(`[claude-mem-lite] Left WAL/SHM intact (not a corruption error). If this persists, retry or reinstall: node install.mjs install`);
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) { return _resolveProjectShared(db, 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 = process.env.MEM_QUIET_HOOKS === '1'
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(paginatedResults, args, ftsQuery, totalCount, orFallbackFired = false, isDeepSearch = false) {
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('This is a recall miss (the rewrite ran), not a query-syntax issue; the memory likely has no related observations.');
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 = totalCount > paginatedResults.length
280
- ? `${paginatedResults.length} of ${totalCount}`
281
- : `${paginatedResults.length}`;
282
- const hasMixed = paginatedResults.some(r => r.source === 'session' || r.source === 'prompt' || r.source === 'event');
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(`Found ${countLabel} result(s)${qLabel}${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt, E# event)' : ''}\n`);
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(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`);
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(`S#${r.id} 📋 ${truncate(r.request || r.completed || '(no summary)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`);
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(`E#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`);
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(`\nWorkflow: mem_timeline(anchor=ID) for context | mem_get(ids=[...]) for full details · ~Nt = est. tokens to fetch full detail`);
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') throw new Error(`Invalid date_since: "${bounds.value}" (use <N><unit>, e.g. 7d, 24h, 90m, 2w)`);
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 = !args.type && !args.obs_type && !args.branch && !args.tier && !args.importance && offset === 0;
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 { /* trailer is best-effort; never break search */ }
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 (args.query && !ftsQuery && !epochFrom && !epochTo && !args.obs_type && !args.importance && deepMode === 'normal') {
373
- return { ...appendDeferredTrailer(formatSearchOutput([], args, ftsQuery, 0)), escalated: false, results: [], total: 0, variants: null };
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; // cross-source gate open; obsTypeScoped narrows it to obs+events
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, currentProject, env: process.env,
400
- searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
401
- reRankWithContext, llm, rerankLlm,
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, ftsQuery, effectiveSource: effectiveType, deepMode, rerank,
405
- limit, offset, project: args.project ?? null, obsType: args.obs_type ?? null,
406
- importance: args.importance ?? null, branch: args.branch ?? null,
407
- includeNoise: args.include_noise === true, epochFrom, epochTo,
408
- sort: args.sort || 'relevance', tier: args.tier ?? null,
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, // D#76: obs_type ⇒ obs+events (skip type-less sessions/prompts)
411
- obsTypeFallback: true, // list-recent-by-type when 0 matches
412
- crossSourceEpochSortNoFts: true, // epoch-sort cross-source with no ftsQuery
413
- rerankPolicy: 'mcp', // (ftsQuery||isDeep) gate; re-rank/re-sort on ftsQuery&&!reranked
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, // recent-listing for explicit --source with no ftsQuery
563
+ recentListingNoFts: true, // recent-listing for explicit --source with no ftsQuery
416
564
  tolerateMissingFts: false,
417
- tierPosition: 'late', // tier filter after re-rank
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) process.stderr.write(`[mem] auto-escalated to deep search (weak results: ${r.escalatedObsCount} hits)\n`);
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 += r.variants.length > 1
429
- ? `\n\n[deep search: rewrote into ${r.variants.length} variants — ${r.variants.slice(1).map(v => JSON.stringify(v)).join(', ')}]`
430
- : '\n\n[deep search: rewrite produced no usable variants; searched the original query only (== baseline)]';
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 { ...output, results: r.page, total: r.total, escalated: r.escalated, variants: r.variants, reranked: r.reranked };
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) throw new Error(`Invalid date_since: "${args.date_since}" (use <N><unit>, e.g. 7d, 24h, 90m, 2w)`);
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(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}`);
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(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}`);
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(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}${marker}`);
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 { content: [{ type: 'text', text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, S#N, E#N, or D#N.` }] };
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 = bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length + deferredIds.length;
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(`No valid fields. Unknown field(s): ${invalidFields.join(', ')}. Valid: ${OBS_FIELDS.join(', ')}`);
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)) continue;
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(`${obsFieldLabel(f)}: ${typeof display === 'string' && display.length > maxLen ? display.slice(0, maxLen) + '…' : display}`);
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(`${f}: ${typeof val === 'string' && val.length > maxLen ? val.slice(0, maxLen) + '…' : val}`);
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; // already in the header
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; // already in the header
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 = foundBySource.obs.size + foundBySource.session.size + foundBySource.prompt.size + foundBySource.event.size + deferredFound;
709
-
710
- if (totalFound === 0 && deferredIds.length > 0 && bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length === 0) {
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 { content: [{ type: 'text', text: `Deferred item(s) not found: ${deferredMissing.map(i => `D#${i}`).join(', ')}. List open items: mem_defer_list.` }] };
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(Object.entries(bySrc).filter(([, v]) => v.length > 0).map(([k]) => k));
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) hints.push(`#${probe.obs.join(', #')} (obs — use source='obs' or bare #N)`);
723
- if (probe.session.length > 0) hints.push(`S#${probe.session.join(', S#')} (session — use source='session' or S#N)`);
724
- if (probe.prompt.length > 0) hints.push(`P#${probe.prompt.join(', P#')} (prompt — use source='prompt' or P#N)`);
725
- if (probe.event.length > 0) hints.push(`E#${probe.event.join(', E#')} (event — use source='event' or E#N)`);
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 = deferredMissing.length > 0 ? ` Deferred item(s) not found: ${deferredMissing.map(i => `D#${i}`).join(', ')}.` : '';
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) msg.push(`Recovered ${result.recoveredChildren} merged/compressed child observation(s) to live.`);
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(db, {
838
- // Size ceiling applied here rather than in lib/save-observation.mjs so the
839
- // shared pipeline keeps its "caller validates" contract (see its header).
840
- content: clampSaveText(args.content, SAVE_TEXT_LIMITS.content),
841
- title: clampSaveText(args.title, SAVE_TEXT_LIMITS.title),
842
- type: args.type || 'discovery',
843
- importance: args.importance,
844
- project,
845
- files: args.files || [],
846
- lesson_learned: clampSaveText(args.lesson_learned, SAVE_TEXT_LIMITS.lesson_learned),
847
- supersedes: args.supersedes,
848
- }, { closesTokens: args.closes_deferred, project }));
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; // unwrapped — preserves original message + stack
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 = closesIds && closesIds.length > 0
868
- ? ` Closed deferred: ${closesIds.map(i => `D#${i}`).join(', ')}.`
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({ type: result.type, id: result.id, lessonCaptured: result.lessonCaptured, surface: 'mcp' });
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 = shouldQueueSaveEnrich(result) && queueSaveEnrich(result.id)
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 { content: [{ type: 'text', text:
906
- `Deferred as D#${r.id} (item ${ord ?? '?'}) in project "${project}" — surfaces in next SessionStart banner.` }] };
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, sessTotal, promptTotal, obsRecent, sessRecent,
986
- types, projects, daily, tokenEst, avgImp, lowVal, lowSignalTitle,
987
- noiseRatio, lowSignalRatio, compressedCount, supersededOnlyCount, tierMap,
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
- ...((noiseRatio > 0.6 || lowSignalRatio > 0.3) ? [' ⚠️ High noise ratio — consider running mem_compress / maintain'] : []),
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).map(([t, c]) => `${c} ${t}`).join(', ');
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 { content: [{ type: 'text', text: `Compressed ${totalCompressed} observations into ${compressableGroups.length} weekly summaries.` }] };
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(` [${keep.id}] "${truncate(keep.title, 40)}" <-> [${remove.id}] "${truncate(remove.title, 40)}" (${d.similarity})`);
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('', `Ready-to-use command:`, ` mem_maintain(action="execute", operations=["dedup"], merge_ids=${JSON.stringify(mergeIds)})`);
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(` [${d.a.id}] "${truncate(d.a.title, 40)}" <-> [${d.b.id}] "${truncate(d.b.title, 40)}" (${d.similarity})`);
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 = args.operations && args.operations.length > 0
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 { 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 };
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(` mem_maintain(action="execute", operations=${JSON.stringify(ops)}, confirm=true${args.retain_days ? `, retain_days=${args.retain_days}` : ''}${args.project ? `, project="${args.project}"` : ''})`);
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 { content: [{ type: 'text', text: `Unknown action: ${action}. Use "scan" or "execute".` }], isError: true };
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) lines.push(` #${obs.id} [${obs.type || 'change'}] ${truncate(obs.title || '(untitled)', 100)}`);
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(` #${obs.id} [${obs.type || 'change'}] (project=${obs.project || '?'}) ${truncate(obs.title || '(untitled)', 100)}`);
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) lines.push(` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`);
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 lines.push(` Normalize: ${results.normalize.processed || 0} updated, ${results.normalize.groups || 0} synonym groups`);
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) lines.push(` Cluster-merge: ${results.clusterMerge.merged || 0} merged of ${results.clusterMerge.processed || 0} clusters`);
1263
- if (results.smartCompress) lines.push(` Smart-compress: ${results.smartCompress.compressed || 0} compressed of ${results.smartCompress.processed || 0} clusters`);
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 { content: [{ type: 'text', text: 'Registry DB not available. Run install first.' }], isError: true };
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 = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
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, { home, managedPrefix: join(DB_DIR, 'managed') + sep });
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 { content: [{ type: 'text', text: `Found ${results.length} resource(s) for "${args.query}":\n\n${lines.join('\n\n')}` }] };
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, type: args.resource_type, source: args.source, fields,
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 { content: [{ type: 'text', text: removed ? `Removed: ${args.resource_type}:${args.name}` : 'Not found.' }] };
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, { confineTo: DB_DIR, enrichResource });
1391
- enrichMsg = `\nEnriched: ${ok}/${results.length}` + (denied ? ` (${denied} refused: path outside managed directory)` : '');
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 { content: [{ type: 'text', text: `Imported ${results.length} resource(s) from ${args.url}:\n${lines.join('\n')}${enrichMsg}` }] };
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, { confineTo: DB_DIR, enrichResource });
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 { content: [{ type: 'text', text: `Access denied: path outside managed directory` }], isError: true };
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 { content: [{ type: 'text', text: `Enrich error: ${error?.message || 'cannot read local_path'}` }], isError: true };
1747
+ return {
1748
+ content: [{ type: 'text', text: `Enrich error: ${error?.message || 'cannot read local_path'}` }],
1749
+ isError: true,
1750
+ };
1424
1751
  }
1425
- return { content: [{ type: 'text', text: status === 'enriched' ? `Enriched: ${args.name}` : `Enrichment failed for ${args.name}` }] };
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 { content: [{ type: 'text', text: `Unknown action: ${action}. Valid: search, list, stats, import, remove, reindex, import_url, enrich` }], isError: true };
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.prepare(`
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
- `).get(type, name, name);
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 { candidates = searchResources(rdb, name, { type, limit: 5 }).map((r) => r.name).filter(Boolean); }
1477
- catch { /* a suggestion is best-effort; the miss message below still stands */ }
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 { content: [{ type: 'text', text:
1494
- `${head} Closest ${type}s by search (NOT loaded — none matched the name you asked for):\n${list}\n\n` +
1495
- `Load one deliberately with its exact name, e.g. mem_use(name="${echoedCandidates[0]}"${type === 'skill' ? '' : `, type="${type}"`}), or browse with ${browse}.` }] };
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
- join(skillPath, 'SKILL.md'),
1503
- join(skillPath, `skills/${row.name}/SKILL.md`),
1504
- ]) {
1505
- if (existsSync(candidate)) { skillPath = candidate; break; }
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 { content: [{ type: 'text', text: `Access denied: path "${skillPath}" is outside managed directory` }], isError: true };
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.prepare(`
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
- `).run(row.id, process.env.CLAUDE_SESSION_ID || 'unknown');
1534
- } catch { /* non-critical */ }
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 = skillPath && skillPath.startsWith(_home) ? '~' + skillPath.slice(_home.length) : (skillPath || '');
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 { content: [{ type: 'text', 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}` }] };
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) return { content: [{ type: 'text', text: `Observation #${args.id} not found` }], isError: true };
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, narrative: args.narrative, type: args.type,
1562
- importance: args.importance, lesson_learned: args.lesson_learned, concepts: args.concepts,
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) return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
1944
+ if (updatedCols.length === 0)
1945
+ return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
1565
1946
 
1566
- return { content: [{ type: 'text', text: `Updated observation #${args.id}: ${updatedCols.join(', ')}` }] };
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)) throw new Error(`Invalid date_from: "${args.date_from}" (use ISO 8601 or YYYY-MM-DD)`);
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, toEpoch,
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.prepare(`SELECT ${EXPORT_COLUMNS_SQL} FROM observations ${where} ORDER BY created_at_epoch DESC LIMIT ?`).all(...params, exportLimit + 1);
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) return { content: [{ type: 'text', text: 'No observations found matching the criteria.' }] };
2023
+ if (rows.length === 0)
2024
+ return { content: [{ type: 'text', text: 'No observations found matching the criteria.' }] };
1637
2025
 
1638
- const output = args.format === 'jsonl'
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 = `\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.` +
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
- async (args) => runExport(db, applyArgAliases(args, { from: 'date_from', to: 'date_to' })),
1676
- { verbatim: true },
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 { content: [{ type: 'text', text: `No history for "${filename}". This file hasn't been observed yet.` }] };
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(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}${lesson}`);
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, tierFilter, limit, now,
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) { lines.push(''); continue; }
2153
+ if (count === 0) {
2154
+ lines.push('');
2155
+ continue;
2156
+ }
1758
2157
 
1759
2158
  for (const r of rows) {
1760
- lines.push(` #${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)', 80)} | ${fmtDate(r.created_at)}`);
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 { content: [{ type: 'text', text: 'No observations found. Start a coding session to build memory.' }] };
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 { db.pragma('wal_checkpoint(PASSIVE)'); } catch (e) { debugCatch(e, 'walCheckpoint'); }
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 { if (db) db.pragma('wal_checkpoint(TRUNCATE)'); } catch {}
1872
- try { if (db) db.close(); } catch {}
1873
- try { if (registryDb) registryDb.close(); } catch {}
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) => { debugCatch(err, 'uncaughtException'); shutdown(1); });
1879
- process.on('unhandledRejection', (err) => { debugCatch(err, 'unhandledRejection'); shutdown(1); });
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 { return false; }
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 { return 0; }
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 { chmodSync(join(dir, entry.name), 0o600); touched++; } catch { /* per-entry, silent */ }
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 { return 0; }
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 = JSON.stringify({
1960
- ts: new Date().toISOString(),
1961
- pid: process.pid,
1962
- ppid: process.ppid,
1963
- argv1: process.argv[1] || '',
1964
- version: PKG_VERSION,
1965
- }) + '\n';
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 { /* never block startup on telemetry failure */ }
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