@shadowforge0/aquifer-memory 1.9.0 → 1.9.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 (43) hide show
  1. package/README.md +33 -4
  2. package/README_CN.md +9 -1
  3. package/README_TW.md +5 -2
  4. package/consumers/cli.js +55 -34
  5. package/consumers/codex-active-checkpoint.js +3 -1
  6. package/consumers/codex-current-memory.js +10 -6
  7. package/consumers/codex.js +5 -2
  8. package/consumers/default/daily-entries.js +2 -2
  9. package/consumers/default/index.js +40 -30
  10. package/consumers/default/prompts/summary.js +2 -2
  11. package/consumers/mcp.js +56 -49
  12. package/consumers/openclaw-ext/index.js +1 -1
  13. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  14. package/consumers/openclaw-ext/package.json +1 -1
  15. package/consumers/openclaw-plugin.js +66 -23
  16. package/consumers/shared/compat-recall.js +101 -0
  17. package/consumers/shared/openclaw-product-tools.js +130 -0
  18. package/consumers/shared/recall-format.js +2 -2
  19. package/core/aquifer.js +385 -20
  20. package/core/backends/local.js +60 -1
  21. package/core/finalization-review.js +88 -42
  22. package/core/interface.js +629 -0
  23. package/core/mcp-manifest.js +11 -3
  24. package/core/memory-bootstrap.js +25 -27
  25. package/core/memory-consolidation.js +564 -42
  26. package/core/memory-explain.js +20 -51
  27. package/core/memory-promotion.js +392 -55
  28. package/core/memory-recall.js +26 -48
  29. package/core/memory-records.js +91 -103
  30. package/core/memory-type-policy.js +298 -0
  31. package/core/postgres-migrations.js +9 -0
  32. package/core/session-checkpoint-producer.js +3 -1
  33. package/core/session-checkpoints.js +1 -1
  34. package/core/session-finalization.js +2 -2
  35. package/docs/getting-started.md +16 -3
  36. package/docs/setup.md +61 -2
  37. package/package.json +2 -2
  38. package/schema/004-completion.sql +4 -4
  39. package/schema/010-v1-finalization-review.sql +72 -0
  40. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  41. package/scripts/backfill-canonical-key.js +1 -1
  42. package/scripts/diagnose-fts-zh.js +1 -1
  43. package/scripts/extract-insights-from-recent-sessions.js +4 -4
package/consumers/mcp.js CHANGED
@@ -21,6 +21,12 @@
21
21
 
22
22
  const { createAquiferFromConfig } = require('./shared/factory');
23
23
  const { version: packageVersion } = require('../package.json');
24
+ const {
25
+ formatMemoryStatsInterface,
26
+ formatPendingRowsInterface,
27
+ formatPendingWorkInterface,
28
+ } = require('../core/interface');
29
+ const { MCP_TOOL_MANIFEST } = require('../core/mcp-manifest');
24
30
 
25
31
  let _aquifer = null;
26
32
 
@@ -29,6 +35,19 @@ function getAquifer() {
29
35
  return _aquifer;
30
36
  }
31
37
 
38
+ function manifestToolDescription(name, fallback) {
39
+ return MCP_TOOL_MANIFEST.find(tool => tool.name === name)?.description || fallback;
40
+ }
41
+
42
+ function pendingSessionOpts(params = {}) {
43
+ return {
44
+ limit: params.limit ?? 20,
45
+ source: params.source || undefined,
46
+ agentId: params.agentId || undefined,
47
+ status: params.status || undefined,
48
+ };
49
+ }
50
+
32
51
  // ---------------------------------------------------------------------------
33
52
  // Format recall results as readable text
34
53
  // ---------------------------------------------------------------------------
@@ -65,6 +84,10 @@ function historicalRecallLaneHeader() {
65
84
  return 'Serving lane: explicit historical/session recall';
66
85
  }
67
86
 
87
+ function countEnvelopeList(value) {
88
+ return Array.isArray(value) ? value.length : 0;
89
+ }
90
+
68
91
  // ---------------------------------------------------------------------------
69
92
  // Start MCP server
70
93
  // ---------------------------------------------------------------------------
@@ -137,6 +160,8 @@ async function main() {
137
160
  source: z.string().optional().describe('Filter by source (e.g., gateway, cc)'),
138
161
  dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
139
162
  dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
163
+ host: z.string().optional().describe('Audit boundary host filter'),
164
+ sessionId: z.string().optional().describe('Audit boundary session ID filter'),
140
165
  entities: z.array(z.string()).optional().describe('Entity names to match'),
141
166
  entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
142
167
  mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
@@ -151,6 +176,8 @@ async function main() {
151
176
  source: params.source || undefined,
152
177
  dateFrom: params.dateFrom || undefined,
153
178
  dateTo: params.dateTo || undefined,
179
+ host: params.host || undefined,
180
+ sessionId: params.sessionId || undefined,
154
181
  };
155
182
  if (params.entities && params.entities.length > 0) {
156
183
  recallOpts.entities = params.entities;
@@ -374,45 +401,16 @@ async function main() {
374
401
 
375
402
  server.tool(
376
403
  'memory_stats',
377
- 'Return storage statistics for the Aquifer memory store, serving mode, current-memory record coverage, and session date range.',
378
- {},
379
- async () => {
404
+ manifestToolDescription('memory_stats', 'Return Aquifer product status.'),
405
+ {
406
+ diagnostics: z.boolean().optional().describe('Include raw storage counters and serving diagnostics. Default false.'),
407
+ },
408
+ async (params = {}) => {
380
409
  try {
381
410
  const aquifer = getAquifer();
382
411
  const stats = await aquifer.getStats();
383
- const lines = [
384
- `Backend: ${stats.backendKind || 'unknown'} (${stats.backendProfile || 'unknown'})`,
385
- `Serving mode: ${stats.serving?.mode || 'legacy'}`,
386
- `Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`,
387
- `Sessions: ${stats.sessionTotal} total`,
388
- ];
389
- for (const [status, count] of Object.entries(stats.sessions)) {
390
- lines.push(` ${status}: ${count}`);
391
- }
392
- if (stats.pendingSessions?.available) {
393
- lines.push(`Actionable pending/failed: ${stats.pendingSessions.total}`);
394
- }
395
- lines.push(`Summaries: ${stats.summaries}`);
396
- lines.push(`Turn embeddings: ${stats.turnEmbeddings}`);
397
- lines.push(`Entities: ${stats.entities}`);
398
- if (stats.memoryRecords) {
399
- lines.push(`Memory records: ${stats.memoryRecords.total} total (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
400
- if (stats.memoryRecords.latest) lines.push(`Memory record range: ${new Date(stats.memoryRecords.earliest).toISOString().slice(0, 10)} → ${new Date(stats.memoryRecords.latest).toISOString().slice(0, 10)}`);
401
- }
402
- if (stats.sessionFinalizations?.available) {
403
- const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
404
- .map(([status, count]) => `${status}: ${count}`)
405
- .join(', ') || 'none';
406
- lines.push(`Session finalizations: ${stats.sessionFinalizations.total} total (${statusText})`);
407
- if (stats.sessionFinalizations.latestFinalizedAt) {
408
- lines.push(`Latest finalization: ${new Date(stats.sessionFinalizations.latestFinalizedAt).toISOString().slice(0, 10)}`);
409
- }
410
- }
411
- if (stats.earliest) lines.push(`Date range: ${new Date(stats.earliest).toISOString().slice(0, 10)} → ${new Date(stats.latest).toISOString().slice(0, 10)}`);
412
- if ((stats.serving?.mode || 'legacy') !== 'curated') {
413
- lines.push('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
414
- }
415
- return { content: [{ type: 'text', text: lines.join('\n') }] };
412
+ const text = formatMemoryStatsInterface(stats, { diagnostics: params.diagnostics === true });
413
+ return { content: [{ type: 'text', text }] };
416
414
  } catch (err) {
417
415
  return {
418
416
  content: [{ type: 'text', text: `memory_stats error: ${err.message}` }],
@@ -424,22 +422,31 @@ async function main() {
424
422
 
425
423
  server.tool(
426
424
  'memory_pending',
427
- 'List sessions with pending or failed processing status.',
425
+ manifestToolDescription('memory_pending', 'Return saved-content preparation status.'),
428
426
  {
429
427
  limit: z.number().int().min(1).max(200).optional().describe('Max results (default 20)'),
428
+ source: z.string().optional().describe('Filter by source'),
429
+ agentId: z.string().optional().describe('Filter by agent ID'),
430
+ status: z.enum(['pending', 'failed']).optional().describe('Filter by processing status'),
431
+ diagnostics: z.boolean().optional().describe('Include source/agent/status buckets, guidance, and samples. Default false.'),
430
432
  },
431
433
  async (params) => {
432
434
  try {
433
435
  const aquifer = getAquifer();
434
- const rows = await aquifer.getPendingSessions({ limit: params.limit ?? 20 });
435
- if (rows.length === 0) {
436
- return { content: [{ type: 'text', text: 'No pending or failed sessions.' }] };
437
- }
438
- const lines = [`${rows.length} pending/failed session(s):\n`];
439
- for (const row of rows) {
440
- lines.push(`${row.session_id} [${row.processing_status}] agent=${row.agent_id}`);
436
+ const queryOpts = pendingSessionOpts(params);
437
+ const report = typeof aquifer.getPendingWork === 'function'
438
+ ? await aquifer.getPendingWork(queryOpts)
439
+ : null;
440
+ if (report) {
441
+ const text = formatPendingWorkInterface(report, {
442
+ diagnostics: params.diagnostics === true,
443
+ includePlan: false,
444
+ });
445
+ return { content: [{ type: 'text', text }] };
441
446
  }
442
- return { content: [{ type: 'text', text: lines.join('\n') }] };
447
+ const rows = await aquifer.getPendingSessions(queryOpts);
448
+ const text = formatPendingRowsInterface(rows, { diagnostics: params.diagnostics === true });
449
+ return { content: [{ type: 'text', text }] };
443
450
  } catch (err) {
444
451
  return {
445
452
  content: [{ type: 'text', text: `memory_pending error: ${err.message}` }],
@@ -499,16 +506,16 @@ async function main() {
499
506
  if (!envelope.ready) {
500
507
  const err = envelope.error || { code: 'AQ_MIGRATION_NOT_READY', message: 'aquifer.init() did not reach ready state' };
501
508
  process.stderr.write(
502
- `[aquifer-mcp] startup aborted: migrationMode=${envelope.migrationMode} ` +
503
- `memoryMode=${envelope.memoryMode} pending=${envelope.pendingMigrations.length} ` +
509
+ `[aquifer-mcp] startup aborted: migrationMode=${envelope.migrationMode || 'unknown'} ` +
510
+ `memoryMode=${envelope.memoryMode || 'unknown'} pending=${countEnvelopeList(envelope.pendingMigrations)} ` +
504
511
  `error=${err.code || 'unknown'}: ${err.message}\n`
505
512
  );
506
513
  await aquifer.close().catch(() => {});
507
514
  process.exit(1);
508
515
  }
509
516
  process.stderr.write(
510
- `[aquifer-mcp] init ok: mode=${envelope.migrationMode} applied=${envelope.appliedMigrations.length} ` +
511
- `pending=${envelope.pendingMigrations.length} durationMs=${envelope.durationMs}\n`
517
+ `[aquifer-mcp] init ok: mode=${envelope.migrationMode || 'unknown'} applied=${countEnvelopeList(envelope.appliedMigrations)} ` +
518
+ `pending=${countEnvelopeList(envelope.pendingMigrations)} durationMs=${envelope.durationMs ?? 0}\n`
512
519
  );
513
520
 
514
521
  const transport = new StdioServerTransport();
@@ -13,7 +13,7 @@
13
13
  // - Delegates to consumers/openclaw-plugin.js. If AQUIFER_PERSONA is set
14
14
  // (pluginConfig.persona or env), the plugin loads the persona module
15
15
  // and hands off mountOnOpenClaw(api); otherwise the default generic
16
- // path runs (before_reset capture + session_recall + session_feedback).
16
+ // path runs (before_reset capture + product status, recall, and feedback tools).
17
17
  //
18
18
  // Host-specific customization goes in a persona module, not here.
19
19
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "aquifer-memory",
3
3
  "name": "Aquifer Memory",
4
- "version": "1.9.0",
4
+ "version": "1.9.1",
5
5
  "description": "Session ingest + recall + feedback. Reads DATABASE_URL / EMBED_PROVIDER / AQUIFER_LLM_PROVIDER from host env; delegates to AQUIFER_PERSONA module if set.",
6
6
  "main": "index.js",
7
7
  "hooks": ["before_reset"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aquifer-openclaw-ext",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "private": true,
5
5
  "main": "index.js",
6
6
  "description": "Drop-in OpenClaw extension for Aquifer Memory. Symlink into $OPENCLAW_HOME/extensions/aquifer-memory/ — no host-side boilerplate required.",
@@ -4,12 +4,14 @@
4
4
  * Aquifer Memory — OpenClaw Host Adapter
5
5
  *
6
6
  * Ingest adapter: auto-captures sessions on before_reset.
7
- * Tool adapter: exposes session_recall/session_feedback via OpenClaw registerTool().
7
+ * Tool adapter: exposes product status and recall/feedback tools via OpenClaw
8
+ * registerTool().
8
9
  *
9
10
  * Status: COMPATIBILITY ONLY. The official tool delivery path is mcp.servers.aquifer
10
11
  * (see consumers/mcp.js). registerTool() exposure has OpenClaw upstream limitations
11
- * that prevent reliable tool visibility. This plugin is retained for before_reset
12
- * session capture; tool registration code is kept for future upstream fixes.
12
+ * that can affect tool visibility on some hosts. This plugin is retained for
13
+ * before_reset session capture; tool registration follows the same product
14
+ * status surface as the CLI and MCP server.
13
15
  *
14
16
  * Install: add to openclaw.json plugins or extensions directory.
15
17
  * Config via plugin config, environment variables, or aquifer.config.json.
@@ -18,6 +20,8 @@
18
20
  const { createAquiferFromConfig } = require('./shared/factory');
19
21
  const { runIngest } = require('./shared/ingest');
20
22
  const { formatRecallResults: sharedFormatRecallResults } = require('./shared/recall-format');
23
+ const { registerOpenClawProductStatusTools } = require('./shared/openclaw-product-tools');
24
+ const { buildCompatibilityRecallRequest, runCompatibilityRecall } = require('./shared/compat-recall');
21
25
 
22
26
  // ---------------------------------------------------------------------------
23
27
  // Helpers
@@ -80,7 +84,7 @@ function normalizeEntries(rawEntries) {
80
84
  if (!entry) continue;
81
85
  const msg = entry.message || entry;
82
86
  if (!msg || !msg.role) continue;
83
- if (!['user', 'assistant', 'system'].includes(msg.role)) continue;
87
+ if (!['user', 'assistant'].includes(msg.role)) continue;
84
88
 
85
89
  let content = '';
86
90
  if (typeof msg.content === 'string') {
@@ -156,17 +160,60 @@ module.exports.normalizeEntries = normalizeEntries;
156
160
  module.exports.coerceRawEntries = coerceRawEntries;
157
161
  module.exports.normalizeTimestamp = normalizeTimestamp;
158
162
 
163
+ function runPersonaMount(api, persona, pluginConfig) {
164
+ if (!api || typeof api.registerTool !== 'function') {
165
+ return {
166
+ mounted: persona.mountOnOpenClaw(api, pluginConfig) || {},
167
+ registeredTools: new Set(),
168
+ };
169
+ }
170
+
171
+ const registeredTools = new Set();
172
+ const originalRegisterTool = api.registerTool;
173
+ api.registerTool = function trackedRegisterTool(factory, opts) {
174
+ if (opts?.name) registeredTools.add(opts.name);
175
+ return originalRegisterTool.call(this, factory, opts);
176
+ };
177
+
178
+ try {
179
+ return {
180
+ mounted: persona.mountOnOpenClaw(api, pluginConfig) || {},
181
+ registeredTools,
182
+ };
183
+ } finally {
184
+ api.registerTool = originalRegisterTool;
185
+ }
186
+ }
187
+
188
+ function registerProductStatusAfterPersona(api, pluginConfig, mounted, registeredTools) {
189
+ if (registeredTools.has('memory_stats') && registeredTools.has('memory_pending')) return true;
190
+
191
+ let aquifer = mounted.aquifer || null;
192
+ if (!aquifer) {
193
+ try {
194
+ aquifer = createAquiferFromConfig(pluginConfig);
195
+ } catch (err) {
196
+ api.logger.warn(`[aquifer-memory] product status tools unavailable after persona mount: ${err.message}`);
197
+ return false;
198
+ }
199
+ }
200
+
201
+ registerOpenClawProductStatusTools(api, aquifer, { skipTools: registeredTools });
202
+ return true;
203
+ }
204
+
159
205
  function register(api) {
160
206
  const pluginConfig = api.pluginConfig || {};
161
207
 
162
208
  // v1.2.0: delegate to a persona layer if one is configured, otherwise
163
- // run the generic default path (before_reset + session_recall + feedback).
209
+ // run the generic default path.
164
210
  const personaPath = pluginConfig.persona || process.env.AQUIFER_PERSONA;
165
211
  if (personaPath) {
166
212
  try {
167
213
  const persona = require(personaPath);
168
214
  if (persona && typeof persona.mountOnOpenClaw === 'function') {
169
- persona.mountOnOpenClaw(api, pluginConfig);
215
+ const { mounted, registeredTools } = runPersonaMount(api, persona, pluginConfig);
216
+ registerProductStatusAfterPersona(api, pluginConfig, mounted, registeredTools);
170
217
  api.logger.info(`[aquifer-memory] registered via persona: ${personaPath}`);
171
218
  return;
172
219
  }
@@ -232,6 +279,10 @@ function register(api) {
232
279
  })();
233
280
  });
234
281
 
282
+ // --- product status tools ---
283
+
284
+ registerOpenClawProductStatusTools(api, aquifer);
285
+
235
286
  // --- session_recall tool ---
236
287
 
237
288
  api.registerTool((ctx) => {
@@ -249,31 +300,23 @@ function register(api) {
249
300
  source: { type: 'string', description: 'Filter by source' },
250
301
  dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
251
302
  dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
303
+ host: { type: 'string', description: 'Audit boundary host filter' },
304
+ sessionId: { type: 'string', description: 'Audit boundary session id filter' },
252
305
  entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
253
306
  entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
254
307
  mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall mode: "fts" (keyword only), "hybrid" (default), "vector" (vector only)' },
255
308
  explain: { type: 'boolean', description: 'Include per-result score breakdown (diagnostic)' },
309
+ activeScopeKey: { type: 'string', description: 'Active curated memory scope key' },
310
+ activeScopePath: { type: 'array', items: { type: 'string' }, description: 'Ordered curated scope path' },
256
311
  },
257
312
  required: ['query'],
258
313
  },
259
314
  async execute(_toolCallId, params) {
260
315
  try {
261
- const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
262
- const recallOpts = {
263
- limit,
264
- agentId: params.agentId || undefined,
265
- source: params.source || undefined,
266
- dateFrom: params.dateFrom || undefined,
267
- dateTo: params.dateTo || undefined,
268
- };
269
- if (Array.isArray(params.entities) && params.entities.length > 0) {
270
- recallOpts.entities = params.entities;
271
- recallOpts.entityMode = params.entityMode || 'any';
272
- }
273
- if (params.mode) recallOpts.mode = params.mode;
274
-
275
- const results = await aquifer.recall(params.query, recallOpts);
276
- const text = formatRecallResults(results, { showScore: true, showExplain: !!params.explain });
316
+ const request = buildCompatibilityRecallRequest(aquifer, params, ctx || {});
317
+ const results = await runCompatibilityRecall(aquifer, params.query, request);
318
+ const formatted = formatRecallResults(results, { showScore: true, showExplain: !!params.explain });
319
+ const text = [request.laneHeader, '', formatted].join('\n');
277
320
  return { content: [{ type: 'text', text }] };
278
321
  } catch (err) {
279
322
  return {
@@ -363,5 +406,5 @@ function register(api) {
363
406
  };
364
407
  }, { name: 'feedback_stats' });
365
408
 
366
- api.logger.info('[aquifer-memory] registered (before_reset + session_recall + session_feedback + feedback_stats)');
409
+ api.logger.info('[aquifer-memory] registered (before_reset + memory_stats + memory_pending + session_recall + session_feedback + feedback_stats)');
367
410
  }
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const VALID_RECALL_MODES = new Set(['fts', 'hybrid', 'vector']);
4
+
5
+ function getConfig(aquifer) {
6
+ return aquifer && typeof aquifer.getConfig === 'function' ? (aquifer.getConfig() || {}) : {};
7
+ }
8
+
9
+ function memoryServingMode(aquifer) {
10
+ const config = getConfig(aquifer);
11
+ return config.memoryServingMode || config.serving?.mode || 'legacy';
12
+ }
13
+
14
+ function firstDefined(...values) {
15
+ return values.find(value => value !== undefined && value !== null && value !== '');
16
+ }
17
+
18
+ function clampLimit(value, fallback = 5) {
19
+ return Math.max(1, Math.min(20, parseInt(value ?? fallback, 10) || fallback));
20
+ }
21
+
22
+ function parseScopePath(value) {
23
+ if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean);
24
+ if (typeof value !== 'string') return undefined;
25
+ const parts = value.split(',').map(item => item.trim()).filter(Boolean);
26
+ return parts.length > 0 ? parts : undefined;
27
+ }
28
+
29
+ function hasLegacyBoundary(opts = {}) {
30
+ return Boolean(opts.agentId || opts.source || opts.dateFrom || opts.dateTo || opts.host || opts.sessionId);
31
+ }
32
+
33
+ function buildCompatibilityRecallRequest(aquifer, params = {}, ctx = {}) {
34
+ const limit = clampLimit(params.limit);
35
+ const mode = memoryServingMode(aquifer);
36
+ const recallOpts = { limit };
37
+ const requestedMode = params.mode;
38
+ if (VALID_RECALL_MODES.has(requestedMode)) recallOpts.mode = requestedMode;
39
+
40
+ if (mode === 'curated') {
41
+ const activeScopeKey = firstDefined(params.activeScopeKey, params.active_scope_key);
42
+ const activeScopePath = firstDefined(params.activeScopePath, params.active_scope_path);
43
+ if (activeScopeKey) recallOpts.activeScopeKey = activeScopeKey;
44
+ const scopePath = parseScopePath(activeScopePath);
45
+ if (scopePath) recallOpts.activeScopePath = scopePath;
46
+ return {
47
+ mode,
48
+ method: 'recall',
49
+ recallOpts,
50
+ laneHeader: 'Current memory recall (curated lane).',
51
+ hasBoundary: true,
52
+ };
53
+ }
54
+
55
+ const agentId = firstDefined(params.agentId, params.agent_id, ctx.agentId);
56
+ if (agentId) recallOpts.agentId = agentId;
57
+ const source = firstDefined(params.source);
58
+ if (source) recallOpts.source = source;
59
+ const dateFrom = firstDefined(params.dateFrom, params.date_from);
60
+ if (dateFrom) recallOpts.dateFrom = dateFrom;
61
+ const dateTo = firstDefined(params.dateTo, params.date_to);
62
+ if (dateTo) recallOpts.dateTo = dateTo;
63
+ const host = firstDefined(params.host);
64
+ if (host) recallOpts.host = host;
65
+ const sessionId = firstDefined(params.sessionId, params.session_id);
66
+ if (sessionId) recallOpts.sessionId = sessionId;
67
+
68
+ const entities = Array.isArray(params.entities)
69
+ ? params.entities.map(item => String(item || '').trim()).filter(Boolean)
70
+ : [];
71
+ if (entities.length > 0) {
72
+ recallOpts.entities = entities;
73
+ recallOpts.entityMode = firstDefined(params.entityMode, params.entity_mode) || 'any';
74
+ }
75
+
76
+ return {
77
+ mode,
78
+ method: 'evidenceRecall',
79
+ recallOpts,
80
+ laneHeader: 'Historical/session recall (legacy evidence lane; not current memory).',
81
+ hasBoundary: hasLegacyBoundary(recallOpts),
82
+ };
83
+ }
84
+
85
+ async function runCompatibilityRecall(aquifer, query, request = {}) {
86
+ if (request.method === 'evidenceRecall') {
87
+ if (!request.hasBoundary) {
88
+ throw new Error('legacy session_recall requires a boundary filter (agentId, source, dateFrom/dateTo, host, or sessionId). Use MCP memory_recall for current memory.');
89
+ }
90
+ if (aquifer && typeof aquifer.evidenceRecall === 'function') {
91
+ return aquifer.evidenceRecall(query, request.recallOpts);
92
+ }
93
+ }
94
+ return aquifer.recall(query, request.recallOpts);
95
+ }
96
+
97
+ module.exports = {
98
+ buildCompatibilityRecallRequest,
99
+ runCompatibilityRecall,
100
+ memoryServingMode,
101
+ };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ formatMemoryStatsInterface,
5
+ formatPendingRowsInterface,
6
+ formatPendingWorkInterface,
7
+ } = require('../../core/interface');
8
+ const { MCP_TOOL_MANIFEST } = require('../../core/mcp-manifest');
9
+
10
+ function manifestToolDescription(name, fallback) {
11
+ return MCP_TOOL_MANIFEST.find(tool => tool.name === name)?.description || fallback;
12
+ }
13
+
14
+ function clampLimit(value, fallback = 20, max = 200) {
15
+ const parsed = parseInt(value ?? fallback, 10);
16
+ if (!Number.isFinite(parsed)) return fallback;
17
+ return Math.max(1, Math.min(max, parsed));
18
+ }
19
+
20
+ function pendingSessionOpts(params = {}) {
21
+ return {
22
+ limit: clampLimit(params.limit, 20, 200),
23
+ source: params.source || undefined,
24
+ agentId: params.agentId || params.agent_id || undefined,
25
+ status: params.status || undefined,
26
+ };
27
+ }
28
+
29
+ function normalizeSkipTools(value) {
30
+ if (!value) return new Set();
31
+ if (value instanceof Set) return value;
32
+ if (Array.isArray(value)) return new Set(value);
33
+ return new Set();
34
+ }
35
+
36
+ function registerOpenClawProductStatusTools(api, aquifer, opts = {}) {
37
+ if (!api || typeof api.registerTool !== 'function') {
38
+ throw new Error('OpenClaw-compatible api.registerTool is required');
39
+ }
40
+ if (!aquifer) {
41
+ throw new Error('aquifer instance is required');
42
+ }
43
+ const skipTools = normalizeSkipTools(opts.skipTools);
44
+
45
+ if (!skipTools.has('memory_stats')) {
46
+ api.registerTool((ctx) => {
47
+ if ((ctx?.sessionKey || '').includes('subagent')) return null;
48
+
49
+ return {
50
+ name: 'memory_stats',
51
+ description: manifestToolDescription('memory_stats', 'Return Aquifer product status.'),
52
+ parameters: {
53
+ type: 'object',
54
+ properties: {
55
+ diagnostics: { type: 'boolean', description: 'Include raw storage counters and serving diagnostics. Default false.' },
56
+ },
57
+ },
58
+ async execute(_toolCallId, params = {}) {
59
+ try {
60
+ const stats = await aquifer.getStats();
61
+ const text = formatMemoryStatsInterface(stats, { diagnostics: params.diagnostics === true });
62
+ return { content: [{ type: 'text', text }] };
63
+ } catch (err) {
64
+ return {
65
+ content: [{ type: 'text', text: `memory_stats error: ${err.message}` }],
66
+ isError: true,
67
+ };
68
+ }
69
+ },
70
+ };
71
+ }, { name: 'memory_stats' });
72
+ }
73
+
74
+ if (!skipTools.has('memory_pending')) {
75
+ api.registerTool((ctx) => {
76
+ if ((ctx?.sessionKey || '').includes('subagent')) return null;
77
+
78
+ return {
79
+ name: 'memory_pending',
80
+ description: manifestToolDescription('memory_pending', 'Return saved-content preparation status.'),
81
+ parameters: {
82
+ type: 'object',
83
+ properties: {
84
+ limit: { type: 'number', description: 'Max results (default 20)' },
85
+ source: { type: 'string', description: 'Filter by source' },
86
+ agentId: { type: 'string', description: 'Filter by agent ID' },
87
+ agent_id: { type: 'string', description: 'Filter by agent ID' },
88
+ status: { type: 'string', enum: ['pending', 'failed'], description: 'Filter by processing status' },
89
+ diagnostics: { type: 'boolean', description: 'Include source/agent/status buckets, guidance, and samples. Default false.' },
90
+ },
91
+ },
92
+ async execute(_toolCallId, params = {}) {
93
+ try {
94
+ const opts = pendingSessionOpts(params);
95
+ const report = typeof aquifer.getPendingWork === 'function'
96
+ ? await aquifer.getPendingWork(opts)
97
+ : null;
98
+ const text = report
99
+ ? formatPendingWorkInterface(report, {
100
+ diagnostics: params.diagnostics === true,
101
+ includePlan: false,
102
+ })
103
+ : formatPendingRowsInterface(
104
+ typeof aquifer.getPendingSessions === 'function'
105
+ ? await aquifer.getPendingSessions(opts)
106
+ : [],
107
+ { diagnostics: params.diagnostics === true }
108
+ );
109
+ return { content: [{ type: 'text', text }] };
110
+ } catch (err) {
111
+ return {
112
+ content: [{ type: 'text', text: `memory_pending error: ${err.message}` }],
113
+ isError: true,
114
+ };
115
+ }
116
+ },
117
+ };
118
+ }, { name: 'memory_pending' });
119
+ }
120
+
121
+ return { aquifer, productStatusToolsRegistered: true };
122
+ }
123
+
124
+ module.exports = {
125
+ clampLimit,
126
+ manifestToolDescription,
127
+ normalizeSkipTools,
128
+ pendingSessionOpts,
129
+ registerOpenClawProductStatusTools,
130
+ };
@@ -2,8 +2,8 @@
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
4
  // Shared recall formatter — turns aquifer.recall() rows into human-readable
5
- // text. The default is English and markdown-ish; consumers with a persona
6
- // (Miranda: zh-TW narrative) can override individual renderers.
5
+ // text. The default is English and markdown-ish; persona adapters can override
6
+ // individual renderers.
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
9
  function truncate(s, n) {