@danielsimonjr/memory-mcp 12.2.3 → 12.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,15 +8,15 @@
8
8
  * @module server/toolHandlers
9
9
  */
10
10
  import path from 'node:path';
11
- import { formatToolResponse, formatTextResponse, formatRawResponse, validateWithSchema, validateFilePath, BatchCreateEntitiesSchema, BatchCreateRelationsSchema, EntityNamesSchema, DeleteRelationsSchema, AddObservationsInputSchema, DeleteObservationsInputSchema, ArchiveCriteriaSchema, SavedSearchInputSchema, SavedSearchUpdateSchema, ImportFormatSchema, ExtendedExportFormatSchema, MergeStrategySchema, ExportFilterSchema, SearchQuerySchema, HybridSearchManager, QueryAnalyzer, QueryPlanner, ReflectionManager, ObservationNormalizer, RefIndex, AuditLog, GovernanceManager, FreshnessManager, ArtifactManager, CollaborativeSynthesis, FailureDistillation, CognitiveLoadAnalyzer, ConsolidationScheduler, DreamEngine, DistillationPipeline, DefaultDistillationPolicy, NoOpDistillationPolicy, computeEntropy, passesEntropyFilter, EntropyFilterStage, getRoleProfile, listRoleProfiles, QueryCostEstimator, ContradictionDetector, PiiRedactor, } from '@danielsimonjr/memoryjs';
11
+ import { formatToolResponse, formatTextResponse, formatRawResponse, validateWithSchema, validateFilePath, BatchCreateEntitiesSchema, BatchCreateRelationsSchema, EntityNamesSchema, DeleteRelationsSchema, AddObservationsInputSchema, DeleteObservationsInputSchema, ArchiveCriteriaSchema, SavedSearchInputSchema, SavedSearchUpdateSchema, ImportFormatSchema, ExtendedExportFormatSchema, MergeStrategySchema, ExportFilterSchema, SearchQuerySchema, HybridSearchManager, QueryAnalyzer, QueryPlanner, ReflectionManager, ObservationNormalizer, RefIndex, FreshnessManager, ArtifactManager, CollaborativeSynthesis, FailureDistillation, CognitiveLoadAnalyzer, ConsolidationScheduler, DreamEngine, DistillationPipeline, DefaultDistillationPolicy, NoOpDistillationPolicy, computeEntropy, passesEntropyFilter, EntropyFilterStage, getRoleProfile, listRoleProfiles, QueryCostEstimator, ContradictionDetector, PiiRedactor, DecisionManager, RankedSearch, RelationConsolidator, clearAllSearchCaches, getAllCacheStats, } from '@danielsimonjr/memoryjs';
12
+ import { promises as fs } from 'node:fs';
13
+ import { performance } from 'node:perf_hooks';
12
14
  import { z } from 'zod';
13
15
  import { maybeCompressResponse } from './responseCompressor.js';
14
16
  // ==================== SINGLETON INFRASTRUCTURE ====================
15
17
  // WeakMap-based singletons keyed on ManagerContext to avoid re-instantiation per request.
16
18
  // These managers are not on ManagerContext directly, so we wire them up once per ctx.
17
19
  const refIndexMap = new WeakMap();
18
- const auditLogMap = new WeakMap();
19
- const governanceMap = new WeakMap();
20
20
  const freshnessMap = new WeakMap();
21
21
  const artifactManagerMap = new WeakMap();
22
22
  const distillationPipelineMap = new WeakMap();
@@ -24,9 +24,29 @@ const failureDistillationMap = new WeakMap();
24
24
  const consolidationSchedulerMap = new WeakMap();
25
25
  const dreamEngineMap = new WeakMap();
26
26
  const queryCostEstimatorMap = new WeakMap();
27
+ // Active project scope (Phase 13 deferred item; ctx.defaultProjectId is readonly,
28
+ // so we keep mutable scope state in a WeakMap keyed on the context. Set via the
29
+ // `set_project_scope` tool; handlers may consult `getActiveProjectScope(ctx)`
30
+ // when they want to auto-apply the scope.
31
+ const projectScopeMap = new WeakMap();
32
+ export function getActiveProjectScope(ctx) {
33
+ return projectScopeMap.get(ctx);
34
+ }
27
35
  function getStorageFilePath(ctx) {
28
- // GraphStorage exposes filePath publicly; fall back to cwd-relative default
29
- return ctx.storage.filePath ?? 'memory.jsonl';
36
+ // GraphStorage stores the path on `memoryFilePath` (private but reachable via
37
+ // type-cast). Older versions of this helper looked for `filePath` and
38
+ // silently fell back to 'memory.jsonl' — which made diag / size / reindex
39
+ // reports point at the wrong file. Try both field names defensively.
40
+ const storage = ctx.storage;
41
+ return storage.memoryFilePath ?? storage.filePath ?? 'memory.jsonl';
42
+ }
43
+ // Sidecar path for the serialized Cue–Tag–Content graph, following the
44
+ // library's `<basename>-<suffix>` convention (memory.jsonl → memory-reconstructive.json).
45
+ function reconstructiveSidecarPath(ctx) {
46
+ const storagePath = getStorageFilePath(ctx);
47
+ const ext = path.extname(storagePath);
48
+ const base = ext ? storagePath.slice(0, -ext.length) : storagePath;
49
+ return `${base}-reconstructive.json`;
30
50
  }
31
51
  function getRefIndex(ctx) {
32
52
  if (!refIndexMap.has(ctx)) {
@@ -36,22 +56,6 @@ function getRefIndex(ctx) {
36
56
  }
37
57
  return refIndexMap.get(ctx);
38
58
  }
39
- function getAuditLog(ctx) {
40
- if (!auditLogMap.has(ctx)) {
41
- const storagePath = getStorageFilePath(ctx);
42
- const dir = path.dirname(storagePath);
43
- auditLogMap.set(ctx, new AuditLog(path.join(dir, 'memory-audit.jsonl')));
44
- }
45
- return auditLogMap.get(ctx);
46
- }
47
- function getGovernanceManager(ctx) {
48
- if (!governanceMap.has(ctx)) {
49
- // GovernanceManager constructor accepts GraphStorage; ctx.storage is GraphStorage
50
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
- governanceMap.set(ctx, new GovernanceManager(ctx.storage, getAuditLog(ctx)));
52
- }
53
- return governanceMap.get(ctx);
54
- }
55
59
  function getFreshnessManager(ctx) {
56
60
  if (!freshnessMap.has(ctx)) {
57
61
  freshnessMap.set(ctx, new FreshnessManager(ctx.storage));
@@ -125,11 +129,43 @@ async function withCompression(handler) {
125
129
  * Filtered/limited search tools (search_nodes_ranked, fuzzy_search, etc.) are not
126
130
  * wrapped because their results are bounded by query specificity or limit params.
127
131
  */
132
+ /**
133
+ * v2.1.0 — Parse `find_duplicate_observations` / `find_jaccard_duplicate_observations`
134
+ * filter args into the manager's `ObservationDedupFilter` shape. Validates each
135
+ * field independently so a stray garbage value falls back to the manager default
136
+ * rather than throwing — REST-like leniency since these are diagnostic tools.
137
+ */
138
+ function parseObservationDedupFilter(args) {
139
+ const out = {};
140
+ if (typeof args.entityType === 'string') {
141
+ out.entityType = args.entityType;
142
+ }
143
+ else if (Array.isArray(args.entityType) && args.entityType.every((s) => typeof s === 'string')) {
144
+ out.entityType = args.entityType;
145
+ }
146
+ if (typeof args.projectId === 'string')
147
+ out.projectId = args.projectId;
148
+ if (typeof args.sessionId === 'string')
149
+ out.sessionId = args.sessionId;
150
+ if (typeof args.minOccurrences === 'number' && Number.isFinite(args.minOccurrences) && args.minOccurrences >= 2) {
151
+ out.minOccurrences = args.minOccurrences;
152
+ }
153
+ if (typeof args.maxGroups === 'number' && Number.isFinite(args.maxGroups) && args.maxGroups >= 1) {
154
+ out.maxGroups = args.maxGroups;
155
+ }
156
+ return out;
157
+ }
128
158
  export const toolHandlers = {
129
159
  // ==================== ENTITY HANDLERS ====================
130
160
  create_entities: async (ctx, args) => {
131
161
  const entities = validateWithSchema(args.entities, BatchCreateEntitiesSchema, 'Invalid entities data');
132
- return formatToolResponse(await ctx.entityManager.createEntities(entities));
162
+ // Phase 13 (v12.4.0): auto-stamp active project scope on entities lacking
163
+ // an explicit projectId. Set via set_project_scope; explicit projectId wins.
164
+ const activeScope = getActiveProjectScope(ctx);
165
+ const scoped = activeScope
166
+ ? entities.map((e) => (e.projectId === undefined ? { ...e, projectId: activeScope } : e))
167
+ : entities;
168
+ return formatToolResponse(await ctx.entityManager.createEntities(scoped));
133
169
  },
134
170
  delete_entities: async (ctx, args) => {
135
171
  const entityNames = validateWithSchema(args.entityNames, EntityNamesSchema, 'Invalid entity names');
@@ -148,6 +184,19 @@ export const toolHandlers = {
148
184
  const projects = await ctx.entityManager.listProjects();
149
185
  return formatToolResponse({ projects, count: projects.length });
150
186
  },
187
+ set_project_scope: async (ctx, args) => {
188
+ const projectId = validateWithSchema(args.projectId, z.string(), 'Invalid projectId');
189
+ if (projectId === '') {
190
+ projectScopeMap.delete(ctx);
191
+ return formatToolResponse({ projectId: null });
192
+ }
193
+ projectScopeMap.set(ctx, projectId);
194
+ return formatToolResponse({ projectId });
195
+ },
196
+ get_project_scope: async (ctx, _args) => {
197
+ const projectId = projectScopeMap.get(ctx) ?? null;
198
+ return formatToolResponse({ projectId });
199
+ },
151
200
  get_entity_versions: async (ctx, args) => {
152
201
  const entityName = validateWithSchema(args.entityName, z.string().min(1), 'Invalid entityName');
153
202
  const latest = await ctx.entityManager.getLatestVersion(entityName);
@@ -242,10 +291,20 @@ export const toolHandlers = {
242
291
  const entityName = args.entityName !== undefined
243
292
  ? validateWithSchema(args.entityName, z.string().min(1), 'Invalid entity name')
244
293
  : undefined;
245
- const options = args.options ?? {};
294
+ const options = args.options !== undefined
295
+ ? validateWithSchema(args.options, z.object({
296
+ resolveCoreferences: z.boolean().optional(),
297
+ anchorTimestamps: z.boolean().optional(),
298
+ extractKeywords: z.boolean().optional(),
299
+ }).strict(), 'Invalid options')
300
+ : {};
246
301
  const persist = args.persist === true;
247
302
  const normalizer = new ObservationNormalizer();
248
- const graph = await ctx.storage.loadGraph();
303
+ // Use getGraphForMutation() to acquire the storage write lock — prevents
304
+ // concurrent calls from overwriting each other's persisted normalizations.
305
+ const graph = persist
306
+ ? await ctx.storage.getGraphForMutation()
307
+ : await ctx.storage.loadGraph();
249
308
  const entities = entityName
250
309
  ? graph.entities.filter(e => e.name === entityName)
251
310
  : graph.entities;
@@ -350,12 +409,52 @@ export const toolHandlers = {
350
409
  // Phase 11 Sprint 2: Hybrid search
351
410
  hybrid_search: async (ctx, args) => {
352
411
  const query = validateWithSchema(args.query, SearchQuerySchema, 'Invalid search query');
353
- const weights = args.weights;
354
- const filters = args.filters;
412
+ const weights = args.weights !== undefined
413
+ ? validateWithSchema(args.weights, z.object({
414
+ semantic: z.number().optional(),
415
+ lexical: z.number().optional(),
416
+ symbolic: z.number().optional(),
417
+ }).strict(), 'Invalid weights')
418
+ : undefined;
419
+ const filters = args.filters !== undefined
420
+ ? validateWithSchema(args.filters, z.object({
421
+ tags: z.array(z.string()).optional(),
422
+ entityTypes: z.array(z.string()).optional(),
423
+ dateRange: z.object({ start: z.string(), end: z.string() }).strict().optional(),
424
+ minImportance: z.number().optional(),
425
+ maxImportance: z.number().optional(),
426
+ }).strict(), 'Invalid filters')
427
+ : undefined;
355
428
  const limit = args.limit !== undefined
356
429
  ? validateWithSchema(args.limit, z.number().int().positive().max(200), 'Invalid limit')
357
430
  : 10;
358
- const hybridSearch = new HybridSearchManager(ctx.semanticSearch, ctx.rankedSearch);
431
+ const graphWeight = args.graphWeight !== undefined
432
+ ? validateWithSchema(args.graphWeight, z.number().min(0).max(1), 'Invalid graphWeight')
433
+ : undefined;
434
+ const expandNeighborsArgs = args.expandNeighbors !== undefined
435
+ ? validateWithSchema(args.expandNeighbors, z.object({
436
+ topK: z.number().int().positive().max(100).optional(),
437
+ damping: z.number().min(0).max(1).optional(),
438
+ }).strict(), 'Invalid expandNeighbors')
439
+ : undefined;
440
+ // Only 1-hop expansion is supported by memoryjs, so `hops` is fixed here
441
+ // rather than exposed in the tool schema.
442
+ const expandNeighbors = expandNeighborsArgs !== undefined
443
+ ? { hops: 1, ...expandNeighborsArgs }
444
+ : undefined;
445
+ const explain = args.explain !== undefined
446
+ ? validateWithSchema(args.explain, z.boolean(), 'Invalid explain flag')
447
+ : undefined;
448
+ const lookFor = args.lookFor !== undefined
449
+ ? validateWithSchema(args.lookFor, z.string().min(1), 'Invalid lookFor')
450
+ : undefined;
451
+ // ctx.hybridSearchManager only attaches the GraphRankPrior when
452
+ // MEMORY_HYBRID_GRAPH_WEIGHT is set; per-call graph options need the prior
453
+ // wired explicitly or they would be silently inert.
454
+ const wantsGraphChannel = (graphWeight !== undefined && graphWeight > 0) || expandNeighbors !== undefined;
455
+ const hybridSearch = wantsGraphChannel
456
+ ? new HybridSearchManager(ctx.semanticSearch, ctx.rankedSearch, ctx.graphRankPrior)
457
+ : ctx.hybridSearchManager;
359
458
  const graph = await ctx.storage.loadGraph();
360
459
  const results = await hybridSearch.searchWithEntities(graph, query, {
361
460
  semanticWeight: weights?.semantic ?? 0.5,
@@ -373,6 +472,10 @@ export const toolHandlers = {
373
472
  }
374
473
  : undefined,
375
474
  limit,
475
+ graphWeight,
476
+ expandNeighbors,
477
+ explain,
478
+ lookFor,
376
479
  });
377
480
  return formatToolResponse({
378
481
  query,
@@ -380,6 +483,7 @@ export const toolHandlers = {
380
483
  semantic: weights?.semantic ?? 0.5,
381
484
  lexical: weights?.lexical ?? 0.3,
382
485
  symbolic: weights?.symbolic ?? 0.2,
486
+ ...(graphWeight !== undefined ? { graph: graphWeight } : {}),
383
487
  },
384
488
  resultCount: results.length,
385
489
  results: results.map((r) => ({
@@ -389,6 +493,9 @@ export const toolHandlers = {
389
493
  matchedLayers: r.matchedLayers,
390
494
  observations: r.entity.observations.slice(0, 3),
391
495
  tags: r.entity.tags,
496
+ ...(r.evidencePaths !== undefined ? { evidencePaths: r.evidencePaths } : {}),
497
+ ...(r.evidenceTruncated !== undefined ? { evidenceTruncated: r.evidenceTruncated } : {}),
498
+ ...(r.lookForScore !== undefined ? { lookForScore: r.lookForScore } : {}),
392
499
  })),
393
500
  });
394
501
  },
@@ -429,7 +536,7 @@ export const toolHandlers = {
429
536
  const planner = new QueryPlanner();
430
537
  plan = planner.createPlan(query, analysis);
431
538
  }
432
- const hybridSearch = new HybridSearchManager(ctx.semanticSearch, ctx.rankedSearch);
539
+ const hybridSearch = ctx.hybridSearchManager;
433
540
  const reflection = new ReflectionManager(hybridSearch, analyzer);
434
541
  const graph = await ctx.storage.loadGraph();
435
542
  const result = await reflection.retrieveWithReflection(graph, query, {
@@ -863,7 +970,7 @@ export const toolHandlers = {
863
970
  const entityName = args.entityName !== undefined
864
971
  ? validateWithSchema(args.entityName, z.string().min(1), 'Invalid entityName')
865
972
  : undefined;
866
- const refs = await getRefIndex(ctx).listRefs(entityName ? { entityName } : undefined);
973
+ const refs = await getRefIndex(ctx).listRefs(entityName);
867
974
  return formatToolResponse({ refs, count: refs.length });
868
975
  },
869
976
  // ==================== ARTIFACT HANDLERS ====================
@@ -993,7 +1100,7 @@ export const toolHandlers = {
993
1100
  },
994
1101
  // ==================== GOVERNANCE HANDLERS ====================
995
1102
  set_governance_policy: async (ctx, args) => {
996
- const gm = getGovernanceManager(ctx);
1103
+ const gm = ctx.governanceManager;
997
1104
  // GovernancePolicy uses function callbacks; we translate boolean args into allow/deny functions
998
1105
  const allowCreate = args.canCreate !== undefined
999
1106
  ? validateWithSchema(args.canCreate, z.boolean(), 'Invalid canCreate')
@@ -1032,20 +1139,20 @@ export const toolHandlers = {
1032
1139
  const limit = args.limit !== undefined
1033
1140
  ? validateWithSchema(args.limit, z.number().int().min(1).max(1000), 'Invalid limit')
1034
1141
  : 50;
1035
- const al = getAuditLog(ctx);
1142
+ const al = ctx.governanceManager.auditLog;
1036
1143
  let entries = await al.query(filter);
1037
1144
  entries = entries.slice(0, limit);
1038
1145
  return formatToolResponse({ entries, count: entries.length });
1039
1146
  },
1040
1147
  audit_history: async (ctx, args) => {
1041
1148
  const entityName = validateWithSchema(args.entityName, z.string().min(1), 'Invalid entityName');
1042
- const al = getAuditLog(ctx);
1149
+ const al = ctx.governanceManager.auditLog;
1043
1150
  const entries = await al.getHistory(entityName);
1044
1151
  return formatToolResponse({ entityName, entries, count: entries.length });
1045
1152
  },
1046
1153
  rollback_operation: async (ctx, args) => {
1047
1154
  const auditEntryId = validateWithSchema(args.auditEntryId, z.string().min(1), 'Invalid auditEntryId');
1048
- const gm = getGovernanceManager(ctx);
1155
+ const gm = ctx.governanceManager;
1049
1156
  await gm.rollback(auditEntryId);
1050
1157
  return formatTextResponse(`Operation "${auditEntryId}" rolled back successfully`);
1051
1158
  },
@@ -1274,7 +1381,9 @@ export const toolHandlers = {
1274
1381
  const distillFailures = args.distillFailures !== undefined
1275
1382
  ? validateWithSchema(args.distillFailures, z.boolean(), 'Invalid distillFailures')
1276
1383
  : true;
1277
- const graph = await ctx.storage.loadGraph();
1384
+ // Use getGraphForMutation() to acquire the storage write lock — prevents
1385
+ // concurrent end_session calls from clobbering each other's outcome writes.
1386
+ const graph = await ctx.storage.getGraphForMutation();
1278
1387
  const sessionEntity = graph.entities.find(e => e.name === sessionId);
1279
1388
  if (!sessionEntity) {
1280
1389
  return formatTextResponse(`Session "${sessionId}" not found`);
@@ -1718,7 +1827,7 @@ export const toolHandlers = {
1718
1827
  // ==================== η.5.5.c OCC HANDLER ====================
1719
1828
  update_entity: async (ctx, args) => {
1720
1829
  const name = validateWithSchema(args.name, z.string().min(1), 'Invalid name');
1721
- const updates = validateWithSchema(args.updates, z.record(z.unknown()), 'Invalid updates');
1830
+ const updates = validateWithSchema(args.updates, z.record(z.string(), z.unknown()), 'Invalid updates');
1722
1831
  const expectedVersion = args.expectedVersion !== undefined
1723
1832
  ? validateWithSchema(args.expectedVersion, z.number().int().positive(), 'Invalid expectedVersion')
1724
1833
  : undefined;
@@ -1897,6 +2006,961 @@ export const toolHandlers = {
1897
2006
  const chains = await ctx.worldModelManager.predictOutcome(action, candidates);
1898
2007
  return formatToolResponse({ action, candidates, chains, count: chains.length });
1899
2008
  },
2009
+ // ==================== v2.1.0 TOOL AFFORDANCE ====================
2010
+ record_tool_outcome: async (ctx, args) => {
2011
+ const toolName = validateWithSchema(args.toolName, z.string().min(1), 'Invalid toolName');
2012
+ const outcome = validateWithSchema(args.outcome, z.enum(['success', 'failure', 'partial']), 'Invalid outcome');
2013
+ const errorMessage = args.errorMessage === undefined
2014
+ ? undefined
2015
+ : validateWithSchema(args.errorMessage, z.string(), 'Invalid errorMessage');
2016
+ const durationMs = args.durationMs === undefined
2017
+ ? undefined
2018
+ : validateWithSchema(args.durationMs, z.number().min(0), 'Invalid durationMs');
2019
+ const record = await ctx.toolAffordanceManager.recordOutcome(toolName, {
2020
+ outcome,
2021
+ errorMessage,
2022
+ durationMs,
2023
+ });
2024
+ return formatToolResponse({ record });
2025
+ },
2026
+ get_tool_affordance_stats: async (ctx, args) => {
2027
+ const toolName = validateWithSchema(args.toolName, z.string().min(1), 'Invalid toolName');
2028
+ const stats = ctx.toolAffordanceManager.rollingStats(toolName);
2029
+ return formatToolResponse({ toolName, stats: stats ?? null });
2030
+ },
2031
+ suggest_tool: async (ctx, args) => {
2032
+ const taskHint = validateWithSchema(args.taskHint, z.string().min(1), 'Invalid taskHint');
2033
+ const limit = args.limit === undefined
2034
+ ? undefined
2035
+ : validateWithSchema(args.limit, z.number().int().min(1), 'Invalid limit');
2036
+ const minScore = args.minScore === undefined
2037
+ ? undefined
2038
+ : validateWithSchema(args.minScore, z.number().min(0).max(1), 'Invalid minScore');
2039
+ const suggestions = await ctx.toolAffordanceManager.suggestTool(taskHint, { limit, minScore });
2040
+ return formatToolResponse({ taskHint, suggestions, count: suggestions.length });
2041
+ },
2042
+ list_tool_affordances: async (ctx) => {
2043
+ const records = await ctx.toolAffordanceManager.list();
2044
+ return formatToolResponse({ records, count: records.length });
2045
+ },
2046
+ remove_tool_affordance: async (ctx, args) => {
2047
+ const toolName = validateWithSchema(args.toolName, z.string().min(1), 'Invalid toolName');
2048
+ const removed = await ctx.toolAffordanceManager.remove(toolName);
2049
+ return formatToolResponse({ toolName, removed });
2050
+ },
2051
+ observe_tool_start: (ctx, args) => {
2052
+ const toolName = validateWithSchema(args.toolName, z.string().min(1), 'Invalid toolName');
2053
+ const argsField = args.args === undefined
2054
+ ? undefined
2055
+ : validateWithSchema(args.args, z.record(z.string(), z.unknown()), 'Invalid args (must be an object)');
2056
+ const callId = ctx.toolCallObserver.observeStart(toolName, argsField);
2057
+ return Promise.resolve(formatToolResponse({ callId, toolName }));
2058
+ },
2059
+ observe_tool_complete: async (ctx, args) => {
2060
+ const callId = validateWithSchema(args.callId, z.string().min(1), 'Invalid callId');
2061
+ const result = args.result === undefined
2062
+ ? undefined
2063
+ : validateWithSchema(args.result, z.string(), 'Invalid result');
2064
+ await ctx.toolCallObserver.observeComplete(callId, result === undefined ? undefined : { result });
2065
+ return formatToolResponse({ callId, recorded: 'success' });
2066
+ },
2067
+ observe_tool_error: async (ctx, args) => {
2068
+ const callId = validateWithSchema(args.callId, z.string().min(1), 'Invalid callId');
2069
+ const errorMessage = validateWithSchema(args.errorMessage, z.string().min(1), 'Invalid errorMessage');
2070
+ await ctx.toolCallObserver.observeError(callId, errorMessage);
2071
+ return formatToolResponse({ callId, recorded: 'failure', errorMessage });
2072
+ },
2073
+ observe_tool_partial: async (ctx, args) => {
2074
+ const callId = validateWithSchema(args.callId, z.string().min(1), 'Invalid callId');
2075
+ const reason = validateWithSchema(args.reason, z.string().min(1), 'Invalid reason');
2076
+ await ctx.toolCallObserver.observePartial(callId, reason);
2077
+ return formatToolResponse({ callId, recorded: 'partial', reason });
2078
+ },
2079
+ observe_tool_cancel: (ctx, args) => {
2080
+ const callId = validateWithSchema(args.callId, z.string().min(1), 'Invalid callId');
2081
+ ctx.toolCallObserver.cancel(callId);
2082
+ return Promise.resolve(formatToolResponse({ callId, cancelled: true }));
2083
+ },
2084
+ tool_observer_in_flight_count: (ctx) => {
2085
+ const count = ctx.toolCallObserver.inFlightCount();
2086
+ return Promise.resolve(formatToolResponse({ inFlightCount: count }));
2087
+ },
2088
+ // ==================== v2.1.0 HEURISTIC GUIDELINES ====================
2089
+ add_heuristic: async (ctx, args) => {
2090
+ const condition = validateWithSchema(args.condition, z.string().min(1), 'Invalid condition');
2091
+ const action = validateWithSchema(args.action, z.string().min(1), 'Invalid action');
2092
+ const priority = args.priority === undefined
2093
+ ? undefined
2094
+ : validateWithSchema(args.priority, z.number(), 'Invalid priority');
2095
+ const initialConfidence = args.initialConfidence === undefined
2096
+ ? undefined
2097
+ : validateWithSchema(args.initialConfidence, z.number().min(0).max(1), 'Invalid initialConfidence');
2098
+ const importance = args.importance === undefined
2099
+ ? undefined
2100
+ : validateWithSchema(args.importance, z.number().min(0).max(10), 'Invalid importance');
2101
+ const agentId = args.agentId === undefined
2102
+ ? undefined
2103
+ : validateWithSchema(args.agentId, z.string().min(1), 'Invalid agentId');
2104
+ const id = args.id === undefined
2105
+ ? undefined
2106
+ : validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2107
+ const heuristicId = await ctx.heuristicManager.add({
2108
+ condition,
2109
+ action,
2110
+ priority,
2111
+ initialConfidence,
2112
+ importance,
2113
+ agentId,
2114
+ id: id,
2115
+ });
2116
+ return formatToolResponse({ id: heuristicId });
2117
+ },
2118
+ get_heuristic: async (ctx, args) => {
2119
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2120
+ const heuristic = ctx.heuristicManager.get(id);
2121
+ return formatToolResponse({ id, heuristic: heuristic ?? null });
2122
+ },
2123
+ list_heuristics: async (ctx) => {
2124
+ const heuristics = await ctx.heuristicManager.list();
2125
+ return formatToolResponse({ heuristics, count: heuristics.length });
2126
+ },
2127
+ heuristic_count: async (ctx) => {
2128
+ const size = await ctx.heuristicManager.size();
2129
+ return formatToolResponse({ count: size });
2130
+ },
2131
+ match_heuristics: async (ctx, args) => {
2132
+ const input = validateWithSchema(args.input, z.string().min(1), 'Invalid input');
2133
+ const limit = args.limit === undefined
2134
+ ? undefined
2135
+ : validateWithSchema(args.limit, z.number().int().min(1), 'Invalid limit');
2136
+ const minScore = args.minScore === undefined
2137
+ ? undefined
2138
+ : validateWithSchema(args.minScore, z.number().min(0).max(1), 'Invalid minScore');
2139
+ const matches = await ctx.heuristicManager.match(input, { limit, minScore });
2140
+ return formatToolResponse({ input, matches, count: matches.length });
2141
+ },
2142
+ reinforce_heuristic: async (ctx, args) => {
2143
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2144
+ const result = await ctx.heuristicManager.reinforce(id);
2145
+ return formatToolResponse({ id, result });
2146
+ },
2147
+ record_heuristic_contradiction: async (ctx, args) => {
2148
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2149
+ const result = await ctx.heuristicManager.recordContradiction(id);
2150
+ return formatToolResponse({ id, result });
2151
+ },
2152
+ detect_heuristic_conflicts: async (ctx) => {
2153
+ const conflicts = await ctx.heuristicManager.detectConflicts();
2154
+ return formatToolResponse({ conflicts, count: conflicts.length });
2155
+ },
2156
+ remove_heuristic: async (ctx, args) => {
2157
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2158
+ const removed = await ctx.heuristicManager.remove(id);
2159
+ return formatToolResponse({ id, removed });
2160
+ },
2161
+ clear_heuristics: async (ctx) => {
2162
+ await ctx.heuristicManager.clear();
2163
+ return formatToolResponse({ cleared: true });
2164
+ },
2165
+ // ==================== v2.1.0 PROJECT CONTEXT ====================
2166
+ upsert_project_context: async (ctx, args) => {
2167
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2168
+ const facts = args.facts === undefined
2169
+ ? undefined
2170
+ : validateWithSchema(args.facts, z.array(z.string()), 'Invalid facts');
2171
+ const conventions = args.conventions === undefined
2172
+ ? undefined
2173
+ : validateWithSchema(args.conventions, z.array(z.string()), 'Invalid conventions');
2174
+ const commands = args.commands === undefined
2175
+ ? undefined
2176
+ : validateWithSchema(args.commands, z.array(z.object({
2177
+ name: z.string().min(1),
2178
+ command: z.string().min(1),
2179
+ purpose: z.string().min(1),
2180
+ })), 'Invalid commands');
2181
+ const glossary = args.glossary === undefined
2182
+ ? undefined
2183
+ : validateWithSchema(args.glossary, z.array(z.object({
2184
+ term: z.string().min(1),
2185
+ definition: z.string().min(1),
2186
+ })), 'Invalid glossary');
2187
+ const record = await ctx.projectContextManager.upsert(projectId, {
2188
+ facts, conventions, commands, glossary,
2189
+ });
2190
+ return formatToolResponse({ record });
2191
+ },
2192
+ get_project_context: async (ctx, args) => {
2193
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2194
+ const record = ctx.projectContextManager.get(projectId);
2195
+ return formatToolResponse({ projectId, record: record ?? null });
2196
+ },
2197
+ append_project_fact: async (ctx, args) => {
2198
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2199
+ const fact = validateWithSchema(args.fact, z.string().min(1), 'Invalid fact');
2200
+ const record = await ctx.projectContextManager.appendFact(projectId, fact);
2201
+ return formatToolResponse({ record });
2202
+ },
2203
+ append_project_convention: async (ctx, args) => {
2204
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2205
+ const convention = validateWithSchema(args.convention, z.string().min(1), 'Invalid convention');
2206
+ const record = await ctx.projectContextManager.appendConvention(projectId, convention);
2207
+ return formatToolResponse({ record });
2208
+ },
2209
+ append_project_command: async (ctx, args) => {
2210
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2211
+ const name = validateWithSchema(args.name, z.string().min(1), 'Invalid name');
2212
+ const command = validateWithSchema(args.command, z.string().min(1), 'Invalid command');
2213
+ const purpose = validateWithSchema(args.purpose, z.string().min(1), 'Invalid purpose');
2214
+ const record = await ctx.projectContextManager.appendCommand(projectId, { name, command, purpose });
2215
+ return formatToolResponse({ record });
2216
+ },
2217
+ append_project_glossary_term: async (ctx, args) => {
2218
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2219
+ const term = validateWithSchema(args.term, z.string().min(1), 'Invalid term');
2220
+ const definition = validateWithSchema(args.definition, z.string().min(1), 'Invalid definition');
2221
+ const record = await ctx.projectContextManager.appendGlossaryTerm(projectId, { term, definition });
2222
+ return formatToolResponse({ record });
2223
+ },
2224
+ remove_project_fact: async (ctx, args) => {
2225
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2226
+ const fact = validateWithSchema(args.fact, z.string().min(1), 'Invalid fact');
2227
+ const removed = await ctx.projectContextManager.removeFact(projectId, fact);
2228
+ return formatToolResponse({ projectId, fact, removed });
2229
+ },
2230
+ remove_project_convention: async (ctx, args) => {
2231
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2232
+ const convention = validateWithSchema(args.convention, z.string().min(1), 'Invalid convention');
2233
+ const removed = await ctx.projectContextManager.removeConvention(projectId, convention);
2234
+ return formatToolResponse({ projectId, convention, removed });
2235
+ },
2236
+ remove_project_command: async (ctx, args) => {
2237
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2238
+ const commandName = validateWithSchema(args.commandName, z.string().min(1), 'Invalid commandName');
2239
+ const removed = await ctx.projectContextManager.removeCommand(projectId, commandName);
2240
+ return formatToolResponse({ projectId, commandName, removed });
2241
+ },
2242
+ remove_project_glossary_term: async (ctx, args) => {
2243
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2244
+ const term = validateWithSchema(args.term, z.string().min(1), 'Invalid term');
2245
+ const removed = await ctx.projectContextManager.removeGlossaryTerm(projectId, term);
2246
+ return formatToolResponse({ projectId, term, removed });
2247
+ },
2248
+ clear_project_context: async (ctx, args) => {
2249
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2250
+ const cleared = await ctx.projectContextManager.clear(projectId);
2251
+ return formatToolResponse({ projectId, cleared });
2252
+ },
2253
+ format_project_context_for_llm: async (ctx, args) => {
2254
+ const projectId = validateWithSchema(args.projectId, z.string().min(1), 'Invalid projectId');
2255
+ const budgetChars = args.budgetChars === undefined
2256
+ ? undefined
2257
+ : validateWithSchema(args.budgetChars, z.number().int().min(1), 'Invalid budgetChars');
2258
+ const prose = await ctx.projectContextManager.forContext(projectId, { budgetChars });
2259
+ return formatTextResponse(prose);
2260
+ },
2261
+ // ==================== v2.1.0 DECISION RATIONALE ====================
2262
+ propose_decision: async (ctx, args) => {
2263
+ const context = validateWithSchema(args.context, z.string().min(1), 'Invalid context');
2264
+ const decision = validateWithSchema(args.decision, z.string().min(1), 'Invalid decision');
2265
+ const alternatives = args.alternatives === undefined
2266
+ ? []
2267
+ : validateWithSchema(args.alternatives, z.array(z.string()), 'Invalid alternatives');
2268
+ const consequences = args.consequences === undefined
2269
+ ? []
2270
+ : validateWithSchema(args.consequences, z.array(z.string()), 'Invalid consequences');
2271
+ const relatedFiles = args.relatedFiles === undefined
2272
+ ? undefined
2273
+ : validateWithSchema(args.relatedFiles, z.array(z.string()), 'Invalid relatedFiles');
2274
+ const supersedes = args.supersedes === undefined
2275
+ ? undefined
2276
+ : validateWithSchema(args.supersedes, z.string().min(1), 'Invalid supersedes');
2277
+ const sourceSessionId = args.sourceSessionId === undefined
2278
+ ? undefined
2279
+ : validateWithSchema(args.sourceSessionId, z.string().min(1), 'Invalid sourceSessionId');
2280
+ const sourceProjectId = args.sourceProjectId === undefined
2281
+ ? undefined
2282
+ : validateWithSchema(args.sourceProjectId, z.string().min(1), 'Invalid sourceProjectId');
2283
+ const importance = args.importance === undefined
2284
+ ? undefined
2285
+ : validateWithSchema(args.importance, z.number().min(0).max(10), 'Invalid importance');
2286
+ const agentId = args.agentId === undefined
2287
+ ? undefined
2288
+ : validateWithSchema(args.agentId, z.string().min(1), 'Invalid agentId');
2289
+ // `supersedes` is a branded DecisionId at the type level; the
2290
+ // manager casts internally so a string is fine here.
2291
+ const record = await ctx.decisionManager.propose({
2292
+ context,
2293
+ decision,
2294
+ alternatives,
2295
+ consequences,
2296
+ relatedFiles,
2297
+ supersedes: supersedes,
2298
+ sourceSessionId,
2299
+ sourceProjectId,
2300
+ }, { importance, agentId });
2301
+ return formatToolResponse({ record });
2302
+ },
2303
+ accept_decision: async (ctx, args) => {
2304
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2305
+ const result = await ctx.decisionManager.accept(id);
2306
+ return formatToolResponse({ id, result });
2307
+ },
2308
+ reject_decision: async (ctx, args) => {
2309
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2310
+ const reason = validateWithSchema(args.reason, z.string().min(1), 'Invalid reason');
2311
+ const result = await ctx.decisionManager.reject(id, reason);
2312
+ return formatToolResponse({ id, reason, result });
2313
+ },
2314
+ supersede_decision: async (ctx, args) => {
2315
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2316
+ const by = validateWithSchema(args.by, z.string().min(1), 'Invalid by');
2317
+ const result = await ctx.decisionManager.supersede(id, by);
2318
+ return formatToolResponse({ id, by, result });
2319
+ },
2320
+ find_decisions_by_context: async (ctx, args) => {
2321
+ const query = validateWithSchema(args.query, z.string().min(1), 'Invalid query');
2322
+ const records = await ctx.decisionManager.findByContext(query);
2323
+ return formatToolResponse({ query, records, count: records.length });
2324
+ },
2325
+ get_decision_chain: async (ctx, args) => {
2326
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2327
+ const chain = await ctx.decisionManager.getChain(id);
2328
+ return formatToolResponse({ id, chain, length: chain.length });
2329
+ },
2330
+ list_decisions: async (ctx, args) => {
2331
+ const status = args.status === undefined
2332
+ ? undefined
2333
+ : validateWithSchema(args.status, z.enum(['proposed', 'accepted', 'superseded', 'rejected']), 'Invalid status');
2334
+ const sourceSessionId = args.sourceSessionId === undefined
2335
+ ? undefined
2336
+ : validateWithSchema(args.sourceSessionId, z.string().min(1), 'Invalid sourceSessionId');
2337
+ const sourceProjectId = args.sourceProjectId === undefined
2338
+ ? undefined
2339
+ : validateWithSchema(args.sourceProjectId, z.string().min(1), 'Invalid sourceProjectId');
2340
+ const limit = args.limit === undefined
2341
+ ? undefined
2342
+ : validateWithSchema(args.limit, z.number().int().min(1), 'Invalid limit');
2343
+ const records = await ctx.decisionManager.list({ status, sourceSessionId, sourceProjectId, limit });
2344
+ return formatToolResponse({ records, count: records.length });
2345
+ },
2346
+ get_decision: async (ctx, args) => {
2347
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2348
+ const record = ctx.decisionManager.get(id);
2349
+ return formatToolResponse({ id, record: record ?? null });
2350
+ },
2351
+ export_decision_as_adr_markdown: async (ctx, args) => {
2352
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2353
+ const markdown = ctx.decisionManager.exportAsAdrMarkdown(id);
2354
+ return formatTextResponse(markdown);
2355
+ },
2356
+ parse_adr_markdown: async (_ctx, args) => {
2357
+ const text = validateWithSchema(args.text, z.string().min(1), 'Invalid text');
2358
+ const parsed = DecisionManager.parseAdrMarkdown(text);
2359
+ return formatToolResponse({ parsed });
2360
+ },
2361
+ // ==================== v2.1.0 do_not_remember (Exclusion) ====================
2362
+ add_exclusion_rule: async (ctx, args) => {
2363
+ const pattern = validateWithSchema(args.pattern, z.string().min(1), 'Invalid pattern');
2364
+ const scope = args.scope === undefined
2365
+ ? undefined
2366
+ : validateWithSchema(args.scope, z.enum(['future-only', 'past-only', 'both']), 'Invalid scope');
2367
+ const entityType = args.entityType === undefined
2368
+ ? undefined
2369
+ : validateWithSchema(args.entityType, z.string().min(1), 'Invalid entityType');
2370
+ const reason = args.reason === undefined
2371
+ ? undefined
2372
+ : validateWithSchema(args.reason, z.string().min(1), 'Invalid reason');
2373
+ const rule = await ctx.exclusionManager.add({ pattern, scope, entityType, reason });
2374
+ return formatToolResponse({ rule });
2375
+ },
2376
+ list_exclusion_rules: async (ctx) => {
2377
+ const rules = await ctx.exclusionManager.list();
2378
+ return formatToolResponse({ rules, count: rules.length });
2379
+ },
2380
+ remove_exclusion_rule: async (ctx, args) => {
2381
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid id');
2382
+ const removed = await ctx.exclusionManager.remove(id);
2383
+ return formatToolResponse({ id, removed });
2384
+ },
2385
+ check_exclusion: async (ctx, args) => {
2386
+ const content = validateWithSchema(args.content, z.string(), 'Invalid content');
2387
+ const entityType = args.entityType === undefined
2388
+ ? undefined
2389
+ : validateWithSchema(args.entityType, z.string().min(1), 'Invalid entityType');
2390
+ const verdict = await ctx.exclusionManager.check(content, entityType);
2391
+ return formatToolResponse(verdict);
2392
+ },
2393
+ find_matching_memories_for_rule: async (ctx, args) => {
2394
+ const pattern = validateWithSchema(args.pattern, z.string().min(1), 'Invalid pattern');
2395
+ const entityType = args.entityType === undefined
2396
+ ? undefined
2397
+ : validateWithSchema(args.entityType, z.string().min(1), 'Invalid entityType');
2398
+ const matches = await ctx.exclusionManager.findMatchingMemories({ pattern, entityType });
2399
+ return formatToolResponse({
2400
+ pattern,
2401
+ entityType,
2402
+ matches: matches.map((m) => ({ name: m.name, entityType: m.entityType })),
2403
+ count: matches.length,
2404
+ });
2405
+ },
2406
+ // ==================== v2.1.0 OBSERVATION DEDUP ====================
2407
+ find_duplicate_observations: async (ctx, args) => {
2408
+ const filter = parseObservationDedupFilter(args);
2409
+ const groups = await ctx.observationDedupManager.findDuplicateObservations(filter);
2410
+ return formatToolResponse({ filter, groups, count: groups.length });
2411
+ },
2412
+ find_jaccard_duplicate_observations: async (ctx, args) => {
2413
+ const filter = parseObservationDedupFilter(args);
2414
+ const groups = await ctx.observationDedupManager.findJaccardDuplicates(filter);
2415
+ return formatToolResponse({ filter, groups, count: groups.length });
2416
+ },
2417
+ // ==================== v2.1.0 SPELL CORRECTION ====================
2418
+ spell_suggest: async (ctx, args) => {
2419
+ const query = validateWithSchema(args.query, z.string().min(1), 'Invalid query');
2420
+ const limit = args.limit === undefined
2421
+ ? undefined
2422
+ : validateWithSchema(args.limit, z.number().int().min(1), 'Invalid limit');
2423
+ const minScore = args.minScore === undefined
2424
+ ? undefined
2425
+ : validateWithSchema(args.minScore, z.number().min(0).max(1), 'Invalid minScore');
2426
+ const maxDistance = args.maxDistance === undefined
2427
+ ? undefined
2428
+ : validateWithSchema(args.maxDistance, z.number().min(0), 'Invalid maxDistance');
2429
+ const suggestions = await ctx.spellChecker.suggest(query, { limit, minScore, maxDistance });
2430
+ return formatToolResponse({ query, suggestions, count: suggestions.length });
2431
+ },
2432
+ spell_rebuild_vocabulary: async (ctx) => {
2433
+ await ctx.spellChecker.rebuild();
2434
+ return formatToolResponse({ rebuilt: true, vocabularySize: ctx.spellChecker.vocabularySize() });
2435
+ },
2436
+ spell_vocabulary_size: async (ctx) => {
2437
+ const size = ctx.spellChecker.vocabularySize();
2438
+ return formatToolResponse({ vocabularySize: size });
2439
+ },
2440
+ // ==================== ENGINEERING / DIAGNOSTIC TOOLS (v12.5.0) ====================
2441
+ // Parallel to the `memory diag` / `memory check` / etc. CLI surface in memoryjs v2.2+.
2442
+ // Useful when the MCP server is up but the graph state is suspect.
2443
+ diag: async (ctx) => {
2444
+ const storagePath = getStorageFilePath(ctx);
2445
+ let exists = false;
2446
+ let sizeBytes = 0;
2447
+ try {
2448
+ const stat = await fs.stat(storagePath);
2449
+ exists = true;
2450
+ sizeBytes = stat.size;
2451
+ }
2452
+ catch { /* file may not exist yet */ }
2453
+ const stats = await ctx.analyticsManager.getGraphStats();
2454
+ return formatToolResponse({
2455
+ runtime: { node: process.version, platform: process.platform, arch: process.arch, pid: process.pid },
2456
+ storage: {
2457
+ path: storagePath,
2458
+ type: process.env.MEMORY_STORAGE_TYPE ?? 'jsonl',
2459
+ exists,
2460
+ sizeBytes,
2461
+ entities: stats.totalEntities,
2462
+ relations: stats.totalRelations,
2463
+ },
2464
+ loadedAt: new Date().toISOString(),
2465
+ });
2466
+ },
2467
+ health: async (ctx) => {
2468
+ const checks = [];
2469
+ const t1 = performance.now();
2470
+ let graph;
2471
+ try {
2472
+ graph = await ctx.storage.loadGraph();
2473
+ checks.push({ name: 'storage:loadGraph', ok: true, durationMs: performance.now() - t1 });
2474
+ }
2475
+ catch (e) {
2476
+ checks.push({
2477
+ name: 'storage:loadGraph',
2478
+ ok: false,
2479
+ durationMs: performance.now() - t1,
2480
+ detail: e instanceof Error ? e.message : String(e),
2481
+ });
2482
+ return formatToolResponse({ ok: false, failed: 1, totalChecks: 1, totalMs: performance.now() - t1, checks });
2483
+ }
2484
+ const t2 = performance.now();
2485
+ const names = new Set();
2486
+ const dupes = [];
2487
+ for (const e of graph.entities) {
2488
+ if (names.has(e.name))
2489
+ dupes.push(e.name);
2490
+ else
2491
+ names.add(e.name);
2492
+ }
2493
+ checks.push({
2494
+ name: 'entities:distinct-names',
2495
+ ok: dupes.length === 0,
2496
+ durationMs: performance.now() - t2,
2497
+ detail: dupes.length === 0 ? undefined : `${dupes.length} duplicate(s)`,
2498
+ });
2499
+ const t3 = performance.now();
2500
+ const orphans = [];
2501
+ for (const r of graph.relations) {
2502
+ if (!names.has(r.from) || !names.has(r.to)) {
2503
+ orphans.push(`${r.from} → ${r.to}`);
2504
+ }
2505
+ }
2506
+ checks.push({
2507
+ name: 'relations:no-orphans',
2508
+ ok: orphans.length === 0,
2509
+ durationMs: performance.now() - t3,
2510
+ detail: orphans.length === 0 ? undefined : `${orphans.length} orphan(s)`,
2511
+ });
2512
+ const t4 = performance.now();
2513
+ const byName = new Map();
2514
+ for (const e of graph.entities)
2515
+ byName.set(e.name, e);
2516
+ const cycleIssues = [];
2517
+ for (const e of graph.entities) {
2518
+ if (!e.parentId)
2519
+ continue;
2520
+ if (!byName.has(e.parentId)) {
2521
+ cycleIssues.push(`${e.name}.parentId='${e.parentId}' missing`);
2522
+ continue;
2523
+ }
2524
+ const visited = new Set();
2525
+ let cur = byName.get(e.parentId);
2526
+ while (cur && cur.parentId) {
2527
+ if (visited.has(cur.name)) {
2528
+ cycleIssues.push(`cycle through ${cur.name}`);
2529
+ break;
2530
+ }
2531
+ visited.add(cur.name);
2532
+ cur = byName.get(cur.parentId);
2533
+ }
2534
+ }
2535
+ checks.push({
2536
+ name: 'hierarchy:no-cycles-no-missing-parents',
2537
+ ok: cycleIssues.length === 0,
2538
+ durationMs: performance.now() - t4,
2539
+ detail: cycleIssues.length === 0 ? undefined : cycleIssues.slice(0, 3).join('; '),
2540
+ });
2541
+ const failed = checks.filter((c) => !c.ok).length;
2542
+ return formatToolResponse({
2543
+ ok: failed === 0,
2544
+ failed,
2545
+ totalChecks: checks.length,
2546
+ totalMs: Number(checks.reduce((a, c) => a + c.durationMs, 0).toFixed(2)),
2547
+ checks,
2548
+ });
2549
+ },
2550
+ check_graph: async (ctx, args) => {
2551
+ const apply = args.apply === undefined
2552
+ ? false
2553
+ : validateWithSchema(args.apply, z.boolean(), 'Invalid apply');
2554
+ const graph = await ctx.storage.loadGraph();
2555
+ const names = new Set(graph.entities.map((e) => e.name));
2556
+ const orphans = [];
2557
+ for (const r of graph.relations) {
2558
+ const fromMissing = !names.has(r.from);
2559
+ const toMissing = !names.has(r.to);
2560
+ if (fromMissing || toMissing) {
2561
+ orphans.push({
2562
+ from: r.from, to: r.to, relationType: r.relationType,
2563
+ reason: fromMissing && toMissing ? 'both-missing' : fromMissing ? 'from-missing' : 'to-missing',
2564
+ });
2565
+ }
2566
+ }
2567
+ const byName = new Map();
2568
+ for (const e of graph.entities)
2569
+ byName.set(e.name, { name: e.name, parentId: e.parentId });
2570
+ const missing = [];
2571
+ const cycles = [];
2572
+ for (const e of graph.entities) {
2573
+ if (!e.parentId)
2574
+ continue;
2575
+ if (!byName.has(e.parentId)) {
2576
+ missing.push({ entity: e.name, parentId: e.parentId });
2577
+ continue;
2578
+ }
2579
+ const visited = new Set([e.name]);
2580
+ let cur = byName.get(e.parentId);
2581
+ while (cur && cur.parentId) {
2582
+ if (visited.has(cur.name)) {
2583
+ cycles.push({ entityInCycle: e.name, cycleThrough: cur.name });
2584
+ break;
2585
+ }
2586
+ visited.add(cur.name);
2587
+ cur = byName.get(cur.parentId);
2588
+ }
2589
+ }
2590
+ const ok = orphans.length === 0 && missing.length === 0 && cycles.length === 0;
2591
+ const result = {
2592
+ ok, applied: apply, orphanRelations: orphans, missingParents: missing, hierarchyCycles: cycles,
2593
+ };
2594
+ if (apply && (orphans.length > 0 || missing.length > 0)) {
2595
+ let deleted = 0;
2596
+ let cleared = 0;
2597
+ if (orphans.length > 0) {
2598
+ await ctx.relationManager.deleteRelations(orphans.map((o) => ({ from: o.from, to: o.to, relationType: o.relationType })));
2599
+ deleted = orphans.length;
2600
+ }
2601
+ for (const m of missing) {
2602
+ try {
2603
+ await ctx.hierarchyManager.setEntityParent(m.entity, null);
2604
+ cleared += 1;
2605
+ }
2606
+ catch { /* skip; entity may have vanished */ }
2607
+ }
2608
+ result.actions = { orphanRelationsDeleted: deleted, missingParentsCleared: cleared };
2609
+ }
2610
+ return formatToolResponse(result);
2611
+ },
2612
+ reindex: async (ctx, args) => {
2613
+ const ranked = args.ranked === undefined
2614
+ ? true
2615
+ : validateWithSchema(args.ranked, z.boolean(), 'Invalid ranked');
2616
+ const spell = args.spell === undefined
2617
+ ? true
2618
+ : validateWithSchema(args.spell, z.boolean(), 'Invalid spell');
2619
+ if (!ranked && !spell) {
2620
+ return formatToolResponse({ ok: true, failed: 0, result: {} });
2621
+ }
2622
+ const result = {};
2623
+ if (ranked) {
2624
+ const t = performance.now();
2625
+ try {
2626
+ // Same workaround as the CLI: default ctx.rankedSearch is constructed
2627
+ // without a storageDir so buildIndex() refuses. Construct an ad-hoc one
2628
+ // alongside the JSONL.
2629
+ const storageDir = path.dirname(getStorageFilePath(ctx));
2630
+ const r = new RankedSearch(ctx.storage, storageDir);
2631
+ await r.buildIndex();
2632
+ result.ranked = { ok: true, durationMs: performance.now() - t };
2633
+ }
2634
+ catch (e) {
2635
+ result.ranked = {
2636
+ ok: false,
2637
+ durationMs: performance.now() - t,
2638
+ detail: e instanceof Error ? e.message : String(e),
2639
+ };
2640
+ }
2641
+ }
2642
+ if (spell) {
2643
+ const t = performance.now();
2644
+ try {
2645
+ await ctx.spellChecker.rebuild();
2646
+ result.spell = { ok: true, durationMs: performance.now() - t };
2647
+ }
2648
+ catch (e) {
2649
+ result.spell = {
2650
+ ok: false,
2651
+ durationMs: performance.now() - t,
2652
+ detail: e instanceof Error ? e.message : String(e),
2653
+ };
2654
+ }
2655
+ }
2656
+ const failed = Object.values(result).filter((r) => !r.ok).length;
2657
+ return formatToolResponse({ ok: failed === 0, failed, result });
2658
+ },
2659
+ cache_stats: async (_ctx) => {
2660
+ return formatToolResponse({ stats: getAllCacheStats() });
2661
+ },
2662
+ cache_clear: async (_ctx) => {
2663
+ clearAllSearchCaches();
2664
+ return formatToolResponse({ cleared: true, caches: ['basic', 'ranked', 'boolean', 'fuzzy'] });
2665
+ },
2666
+ graph_size: async (ctx) => {
2667
+ const storagePath = getStorageFilePath(ctx);
2668
+ const graph = await ctx.storage.loadGraph();
2669
+ let observationCount = 0;
2670
+ const tagSet = new Set();
2671
+ for (const e of graph.entities) {
2672
+ observationCount += e.observations.length;
2673
+ if (e.tags)
2674
+ for (const t of e.tags)
2675
+ tagSet.add(t);
2676
+ }
2677
+ let exists = false;
2678
+ let sizeBytes = 0;
2679
+ let lineCount = 0;
2680
+ try {
2681
+ const stat = await fs.stat(storagePath);
2682
+ exists = true;
2683
+ sizeBytes = stat.size;
2684
+ if (sizeBytes > 0) {
2685
+ const content = await fs.readFile(storagePath, 'utf8');
2686
+ lineCount = content.split('\n').filter((l) => l.length > 0).length;
2687
+ }
2688
+ }
2689
+ catch { /* file may not exist */ }
2690
+ return formatToolResponse({
2691
+ graph: {
2692
+ entities: graph.entities.length,
2693
+ relations: graph.relations.length,
2694
+ observations: observationCount,
2695
+ distinctTags: tagSet.size,
2696
+ avgObservationsPerEntity: graph.entities.length === 0
2697
+ ? 0
2698
+ : Number((observationCount / graph.entities.length).toFixed(2)),
2699
+ },
2700
+ storage: { path: storagePath, exists, sizeBytes, lineCount },
2701
+ });
2702
+ },
2703
+ inspect_entity: async (ctx, args) => {
2704
+ const name = validateWithSchema(args.name, z.string().min(1), 'Invalid name');
2705
+ // getEntityByName reads an in-memory nameIndex hydrated by loadGraph.
2706
+ const graph = await ctx.storage.loadGraph();
2707
+ const entity = ctx.storage.getEntityByName(name);
2708
+ if (!entity)
2709
+ throw new Error(`entity not found: ${name}`);
2710
+ const observations = await ctx.observationManager.getObservationsFor(name);
2711
+ const outgoing = graph.relations
2712
+ .filter((r) => r.from === name)
2713
+ .map((r) => ({ to: r.to, type: r.relationType }));
2714
+ const incoming = graph.relations
2715
+ .filter((r) => r.to === name)
2716
+ .map((r) => ({ from: r.from, type: r.relationType }));
2717
+ const children = (await ctx.hierarchyManager.getChildren(name)).map((c) => c.name);
2718
+ const ancestors = (await ctx.hierarchyManager.getAncestors(name)).map((a) => a.name);
2719
+ return formatToolResponse({
2720
+ name: entity.name,
2721
+ entityType: entity.entityType,
2722
+ observations,
2723
+ tags: entity.tags,
2724
+ importance: entity.importance,
2725
+ createdAt: entity.createdAt,
2726
+ lastModified: entity.lastModified,
2727
+ parentId: entity.parentId,
2728
+ children,
2729
+ ancestors,
2730
+ relations: { outgoing, incoming },
2731
+ });
2732
+ },
2733
+ hierarchy_tree: async (ctx, args) => {
2734
+ const root = args.root === undefined
2735
+ ? undefined
2736
+ : validateWithSchema(args.root, z.string().min(1), 'Invalid root');
2737
+ await ctx.storage.loadGraph();
2738
+ async function walk(n) {
2739
+ const e = ctx.storage.getEntityByName(n);
2740
+ const kids = await ctx.hierarchyManager.getChildren(n);
2741
+ const childNodes = [];
2742
+ for (const c of kids)
2743
+ childNodes.push(await walk(c.name));
2744
+ return { name: n, entityType: e?.entityType ?? 'unknown', children: childNodes };
2745
+ }
2746
+ if (root) {
2747
+ const e = ctx.storage.getEntityByName(root);
2748
+ if (!e)
2749
+ throw new Error(`entity not found: ${root}`);
2750
+ return formatToolResponse({ trees: [await walk(root)] });
2751
+ }
2752
+ const roots = await ctx.hierarchyManager.getRootEntities();
2753
+ const trees = await Promise.all(roots.map((r) => walk(r.name)));
2754
+ return formatToolResponse({ trees });
2755
+ },
2756
+ entity_neighbors: async (ctx, args) => {
2757
+ const name = validateWithSchema(args.name, z.string().min(1), 'Invalid name');
2758
+ const graph = await ctx.storage.loadGraph();
2759
+ const entity = ctx.storage.getEntityByName(name);
2760
+ if (!entity)
2761
+ throw new Error(`entity not found: ${name}`);
2762
+ const outgoing = graph.relations.filter((r) => r.from === name).map((r) => ({ to: r.to, type: r.relationType }));
2763
+ const incoming = graph.relations.filter((r) => r.to === name).map((r) => ({ from: r.from, type: r.relationType }));
2764
+ return formatToolResponse({
2765
+ entity: name,
2766
+ outgoing,
2767
+ incoming,
2768
+ outDegree: outgoing.length,
2769
+ inDegree: incoming.length,
2770
+ });
2771
+ },
2772
+ // ==================== EVENT MEMORY HANDLERS (memoryjs v3.0.0) ====================
2773
+ record_event: async (ctx, args) => {
2774
+ const input = validateWithSchema(args, z.object({
2775
+ action: z.string().min(1),
2776
+ actor: z.string().min(1),
2777
+ target: z.string().min(1).optional(),
2778
+ context: z.string().min(1).optional(),
2779
+ participants: z.array(z.string().min(1)).optional(),
2780
+ occurredAt: z.string().min(1).optional(),
2781
+ flowKey: z.string().min(1).optional(),
2782
+ detail: z.array(z.string()).optional(),
2783
+ importance: z.number().optional(),
2784
+ }).strict(), 'Invalid event input');
2785
+ const event = await ctx.eventManager.recordEvent(input);
2786
+ return formatToolResponse({ event });
2787
+ },
2788
+ get_event: async (ctx, args) => {
2789
+ const name = validateWithSchema(args.name, z.string().min(1), 'Invalid event name');
2790
+ const event = await ctx.eventManager.getEvent(name);
2791
+ if (!event) {
2792
+ return formatTextResponse(`Event "${name}" not found`);
2793
+ }
2794
+ return formatToolResponse({ event });
2795
+ },
2796
+ query_events: async (ctx, args) => {
2797
+ const filter = validateWithSchema(args, z.object({
2798
+ actor: z.string().min(1).optional(),
2799
+ target: z.string().min(1).optional(),
2800
+ action: z.string().min(1).optional(),
2801
+ flowKey: z.string().min(1).optional(),
2802
+ timeRange: z.object({
2803
+ start: z.string().min(1).optional(),
2804
+ end: z.string().min(1).optional(),
2805
+ }).strict().optional(),
2806
+ limit: z.number().int().positive().max(1000).optional(),
2807
+ }).strict(), 'Invalid event filter');
2808
+ const events = await ctx.eventManager.queryEvents(filter);
2809
+ return formatToolResponse({ events, count: events.length });
2810
+ },
2811
+ get_event_flow: async (ctx, args) => {
2812
+ const flowKey = validateWithSchema(args.flowKey, z.string().min(1), 'Invalid flowKey');
2813
+ const events = await ctx.eventManager.getFlow(flowKey);
2814
+ return formatToolResponse({ flowKey, events, count: events.length });
2815
+ },
2816
+ who_did_what: async (ctx, args) => {
2817
+ const filter = validateWithSchema(args, z.object({
2818
+ target: z.string().min(1).optional(),
2819
+ context: z.string().min(1).optional(),
2820
+ timeRange: z.object({
2821
+ start: z.string().min(1).optional(),
2822
+ end: z.string().min(1).optional(),
2823
+ }).strict().optional(),
2824
+ limit: z.number().int().positive().max(1000).optional(),
2825
+ }).strict(), 'Invalid filter');
2826
+ const entries = await ctx.eventManager.whoDidWhat(filter);
2827
+ return formatToolResponse({ entries, count: entries.length });
2828
+ },
2829
+ // ==================== RECONSTRUCTIVE MEMORY HANDLERS (memoryjs v3.0.0) ====================
2830
+ ingest_dialogue: async (ctx, args) => {
2831
+ const turns = validateWithSchema(args.turns, z.array(z.object({
2832
+ id: z.string().min(1),
2833
+ speaker: z.string().optional(),
2834
+ text: z.string().min(1),
2835
+ timestamp: z.string().optional(),
2836
+ }).strict()).min(1), 'Invalid dialogue turns');
2837
+ const rm = ctx.reconstructiveMemory();
2838
+ const result = await rm.ingest(turns);
2839
+ return formatToolResponse({
2840
+ distillation: result,
2841
+ persisted: rm.lastPersistResult,
2842
+ stats: rm.stats(),
2843
+ });
2844
+ },
2845
+ reconstruct_memory: async (ctx, args) => {
2846
+ const query = validateWithSchema(args.query, z.string().min(1), 'Invalid query');
2847
+ const options = validateWithSchema({
2848
+ maxSteps: args.maxSteps,
2849
+ perStepBudget: args.perStepBudget,
2850
+ evidenceTarget: args.evidenceTarget,
2851
+ }, z.object({
2852
+ maxSteps: z.number().int().positive().max(32).optional(),
2853
+ perStepBudget: z.number().int().positive().max(100).optional(),
2854
+ evidenceTarget: z.number().int().positive().max(100).optional(),
2855
+ }), 'Invalid reconstruction options');
2856
+ const result = await ctx.reconstructiveMemory().reconstruct(query, options);
2857
+ return formatToolResponse(result);
2858
+ },
2859
+ reconstructive_memory_stats: async (ctx) => {
2860
+ return formatToolResponse(ctx.reconstructiveMemory().stats());
2861
+ },
2862
+ // ==================== RELATION CONSOLIDATION HANDLERS (memoryjs v3.0.0) ====================
2863
+ analyze_relation_duplicates: async (ctx) => {
2864
+ const consolidator = new RelationConsolidator(ctx.relationManager, ctx.entityManager, {
2865
+ embedding: ctx.semanticSearch?.getEmbeddingService(),
2866
+ });
2867
+ const report = await consolidator.analyze();
2868
+ return formatToolResponse(report);
2869
+ },
2870
+ consolidate_relations: async (ctx, args) => {
2871
+ const apply = args.apply !== undefined
2872
+ ? validateWithSchema(args.apply, z.boolean(), 'Invalid apply flag')
2873
+ : false;
2874
+ const consolidator = new RelationConsolidator(ctx.relationManager, ctx.entityManager, {
2875
+ embedding: ctx.semanticSearch?.getEmbeddingService(),
2876
+ });
2877
+ const result = await consolidator.consolidate({ apply });
2878
+ return formatToolResponse(result);
2879
+ },
2880
+ // ==================== AGENT REFLECTION HANDLERS (memoryjs v3.0.0) ====================
2881
+ create_reflection: async (ctx, args) => {
2882
+ const parsed = validateWithSchema(args, z.object({
2883
+ scope: z.enum(['session', 'project', 'global']),
2884
+ summary: z.string().min(1),
2885
+ evidence: z.array(z.string().min(1)).min(1),
2886
+ generalizationConfidence: z.number().min(0).max(1),
2887
+ keyInsights: z.array(z.string().min(1)).max(5).optional(),
2888
+ experienceType: z.string().min(1).optional(),
2889
+ sourceSessionId: z.string().min(1).optional(),
2890
+ sourceProjectId: z.string().min(1).optional(),
2891
+ importance: z.number().optional(),
2892
+ agentId: z.string().min(1).optional(),
2893
+ }).strict(), 'Invalid reflection input');
2894
+ const reflection = await ctx.reflectionManager.create({
2895
+ scope: parsed.scope,
2896
+ summary: parsed.summary,
2897
+ evidence: parsed.evidence,
2898
+ generalization_confidence: parsed.generalizationConfidence,
2899
+ keyInsights: parsed.keyInsights,
2900
+ experienceType: parsed.experienceType,
2901
+ sourceSessionId: parsed.sourceSessionId,
2902
+ sourceProjectId: parsed.sourceProjectId,
2903
+ }, { importance: parsed.importance, agentId: parsed.agentId });
2904
+ return formatToolResponse({ reflection });
2905
+ },
2906
+ list_reflections: async (ctx, args) => {
2907
+ const options = validateWithSchema(args, z.object({
2908
+ scope: z.enum(['session', 'project', 'global']).optional(),
2909
+ sourceSessionId: z.string().min(1).optional(),
2910
+ sourceProjectId: z.string().min(1).optional(),
2911
+ minConfidence: z.number().min(0).max(1).optional(),
2912
+ includeArchived: z.boolean().optional(),
2913
+ limit: z.number().int().positive().max(1000).optional(),
2914
+ }).strict(), 'Invalid list options');
2915
+ const reflections = await ctx.reflectionManager.list(options);
2916
+ return formatToolResponse({ reflections, count: reflections.length });
2917
+ },
2918
+ get_relevant_reflections: async (ctx, args) => {
2919
+ const sessionId = validateWithSchema(args.sessionId, z.string().min(1), 'Invalid sessionId');
2920
+ const options = validateWithSchema({
2921
+ sessionEntityNames: args.sessionEntityNames,
2922
+ minConfidence: args.minConfidence,
2923
+ limit: args.limit,
2924
+ }, z.object({
2925
+ sessionEntityNames: z.array(z.string().min(1)).optional(),
2926
+ minConfidence: z.number().min(0).max(1).optional(),
2927
+ limit: z.number().int().positive().max(1000).optional(),
2928
+ }), 'Invalid relevance options');
2929
+ const reflections = await ctx.reflectionManager.getRelevantForSession(sessionId, options);
2930
+ return formatToolResponse({ sessionId, reflections, count: reflections.length });
2931
+ },
2932
+ archive_reflection: async (ctx, args) => {
2933
+ const id = validateWithSchema(args.id, z.string().min(1), 'Invalid reflection id');
2934
+ const result = await ctx.reflectionManager.archive(id);
2935
+ return formatToolResponse(result);
2936
+ },
2937
+ // ==================== RECONSTRUCTIVE MEMORY PERSISTENCE HANDLERS (memoryjs v3.0.0) ====================
2938
+ save_reconstructive_memory: async (ctx) => {
2939
+ const rm = ctx.reconstructiveMemory();
2940
+ const snapshot = rm.toSnapshot();
2941
+ const sidecarPath = reconstructiveSidecarPath(ctx);
2942
+ await fs.writeFile(sidecarPath, JSON.stringify(snapshot), 'utf-8');
2943
+ return formatToolResponse({ path: sidecarPath, stats: rm.stats() });
2944
+ },
2945
+ load_reconstructive_memory: async (ctx) => {
2946
+ const sidecarPath = reconstructiveSidecarPath(ctx);
2947
+ let raw;
2948
+ try {
2949
+ raw = await fs.readFile(sidecarPath, 'utf-8');
2950
+ }
2951
+ catch {
2952
+ throw new Error(`No reconstructive memory snapshot at "${sidecarPath}" — run save_reconstructive_memory first`);
2953
+ }
2954
+ const snapshot = validateWithSchema(JSON.parse(raw), z.object({
2955
+ cues: z.array(z.unknown()),
2956
+ tags: z.array(z.unknown()),
2957
+ contents: z.array(z.unknown()),
2958
+ triples: z.array(z.unknown()),
2959
+ }), `Invalid reconstructive memory snapshot at "${sidecarPath}"`);
2960
+ const rm = ctx.reconstructiveMemory();
2961
+ rm.loadSnapshot(snapshot);
2962
+ return formatToolResponse({ path: sidecarPath, stats: rm.stats() });
2963
+ },
1900
2964
  };
1901
2965
  /**
1902
2966
  * Handle a tool call by dispatching to the appropriate handler.