@shadowforge0/aquifer-memory 1.8.1 → 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 (57) hide show
  1. package/.env.example +1 -0
  2. package/README.md +82 -26
  3. package/README_CN.md +33 -23
  4. package/README_TW.md +25 -24
  5. package/aquifer.config.example.json +2 -1
  6. package/consumers/cli.js +587 -33
  7. package/consumers/codex-active-checkpoint.js +3 -1
  8. package/consumers/codex-current-memory.js +10 -6
  9. package/consumers/codex.js +6 -3
  10. package/consumers/default/daily-entries.js +2 -2
  11. package/consumers/default/index.js +40 -30
  12. package/consumers/default/prompts/summary.js +2 -2
  13. package/consumers/mcp.js +56 -46
  14. package/consumers/openclaw-ext/index.js +65 -7
  15. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  16. package/consumers/openclaw-ext/package.json +1 -1
  17. package/consumers/openclaw-install.js +326 -0
  18. package/consumers/openclaw-plugin.js +105 -24
  19. package/consumers/shared/compat-recall.js +101 -0
  20. package/consumers/shared/config.js +2 -0
  21. package/consumers/shared/openclaw-product-tools.js +130 -0
  22. package/consumers/shared/recall-format.js +2 -2
  23. package/core/aquifer.js +553 -41
  24. package/core/backends/local.js +169 -1
  25. package/core/doctor.js +924 -0
  26. package/core/finalization-inspector.js +164 -0
  27. package/core/finalization-review.js +88 -42
  28. package/core/interface.js +629 -0
  29. package/core/mcp-manifest.js +11 -3
  30. package/core/memory-bootstrap.js +25 -27
  31. package/core/memory-consolidation.js +564 -42
  32. package/core/memory-explain.js +593 -0
  33. package/core/memory-promotion.js +392 -55
  34. package/core/memory-recall.js +75 -71
  35. package/core/memory-records.js +107 -108
  36. package/core/memory-review.js +891 -0
  37. package/core/memory-serving.js +61 -4
  38. package/core/memory-type-policy.js +298 -0
  39. package/core/operator-observability.js +249 -0
  40. package/core/postgres-migrations.js +22 -0
  41. package/core/session-checkpoint-producer.js +3 -1
  42. package/core/session-checkpoints.js +1 -1
  43. package/core/session-finalization.js +78 -3
  44. package/core/storage.js +124 -8
  45. package/docs/getting-started.md +50 -4
  46. package/docs/setup.md +163 -24
  47. package/package.json +5 -4
  48. package/schema/004-completion.sql +4 -4
  49. package/schema/010-v1-finalization-review.sql +72 -0
  50. package/schema/019-v1-memory-review-resolutions.sql +53 -0
  51. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  52. package/scripts/backfill-canonical-key.js +1 -1
  53. package/scripts/codex-checkpoint-commands.js +28 -0
  54. package/scripts/codex-checkpoint-runtime.js +109 -0
  55. package/scripts/codex-recovery.js +16 -4
  56. package/scripts/diagnose-fts-zh.js +1 -1
  57. package/scripts/extract-insights-from-recent-sessions.js +4 -4
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { version: packageVersion } = require('../package.json');
7
+
8
+ const OPENCLAW_PLUGIN_ID = 'aquifer-memory';
9
+
10
+ function nowStamp() {
11
+ return new Date().toISOString().replace(/[:.]/g, '-');
12
+ }
13
+
14
+ function uniqueBackupPath(filePath) {
15
+ const base = `${filePath}.bak-${nowStamp()}`;
16
+ if (!fs.existsSync(base)) return base;
17
+ for (let i = 1; i < 1000; i += 1) {
18
+ const candidate = `${base}-${i}`;
19
+ if (!fs.existsSync(candidate)) return candidate;
20
+ }
21
+ throw new Error(`Could not allocate unique backup path for ${filePath}`);
22
+ }
23
+
24
+ function valueFlag(flags = {}, name, fallback = null) {
25
+ const value = flags[name];
26
+ if (value === undefined || value === null || value === true || value === '') return fallback;
27
+ return String(value);
28
+ }
29
+
30
+ function resolveOpenClawHome(flags = {}, env = process.env) {
31
+ return path.resolve(valueFlag(flags, 'openclaw-home', env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw')));
32
+ }
33
+
34
+ function readJson(filePath) {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ }
37
+
38
+ function writeJsonWithBackup(filePath, value, opts = {}) {
39
+ if (opts.dryRun) return null;
40
+ const backupPath = uniqueBackupPath(filePath);
41
+ if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath);
42
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
43
+ return backupPath;
44
+ }
45
+
46
+ function samePath(a, b) {
47
+ return path.resolve(a) === path.resolve(b);
48
+ }
49
+
50
+ function ensureOpenClawHome(openclawHome) {
51
+ if (!fs.existsSync(openclawHome) || !fs.statSync(openclawHome).isDirectory()) {
52
+ throw new Error(`OPENCLAW_HOME not found: ${openclawHome}`);
53
+ }
54
+ }
55
+
56
+ function ensureExtensionLink({ openclawHome, packageRoot, dryRun = false, force = false }) {
57
+ const source = path.join(packageRoot, 'consumers', 'openclaw-ext');
58
+ const dest = path.join(openclawHome, 'extensions', 'aquifer-memory');
59
+ if (!fs.existsSync(source)) throw new Error(`OpenClaw extension source not found: ${source}`);
60
+
61
+ const result = {
62
+ source,
63
+ dest,
64
+ action: 'link',
65
+ backupPath: null,
66
+ };
67
+
68
+ if (fs.existsSync(dest)) {
69
+ const stat = fs.lstatSync(dest);
70
+ if (stat.isSymbolicLink()) {
71
+ const target = fs.readlinkSync(dest);
72
+ const resolvedTarget = path.resolve(path.dirname(dest), target);
73
+ if (samePath(resolvedTarget, source)) {
74
+ return { ...result, action: 'already-linked' };
75
+ }
76
+ }
77
+
78
+ if (!force) {
79
+ throw new Error(`${dest} already exists; rerun with --force to move it aside and relink`);
80
+ }
81
+
82
+ result.backupPath = `${dest}.bak-${nowStamp()}`;
83
+ result.action = 'replace-link';
84
+ if (!dryRun) fs.renameSync(dest, result.backupPath);
85
+ }
86
+
87
+ if (!dryRun) {
88
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
89
+ fs.symlinkSync(source, dest, 'dir');
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function normalizeMcpServer(existing = {}, packageRoot) {
95
+ return {
96
+ ...existing,
97
+ command: 'node',
98
+ args: [path.join(packageRoot, 'consumers', 'mcp.js')],
99
+ env: {
100
+ ...(existing.env && typeof existing.env === 'object' ? existing.env : {}),
101
+ },
102
+ };
103
+ }
104
+
105
+ function ensureMcpConfig({ openclawHome, packageRoot, dryRun = false }) {
106
+ const configPath = path.join(openclawHome, 'openclaw.json');
107
+ const mcpPath = path.join(packageRoot, 'consumers', 'mcp.js');
108
+ const result = {
109
+ configPath,
110
+ mcpPath,
111
+ action: 'skipped',
112
+ backupPath: null,
113
+ previousArgs: null,
114
+ nextArgs: [mcpPath],
115
+ };
116
+
117
+ if (!fs.existsSync(configPath)) {
118
+ return { ...result, reason: 'openclaw.json not found' };
119
+ }
120
+
121
+ const config = readJson(configPath);
122
+ const existing = config.mcp?.servers?.aquifer || {};
123
+ result.previousArgs = Array.isArray(existing.args) ? existing.args : null;
124
+
125
+ const nextServer = normalizeMcpServer(existing, packageRoot);
126
+ const previousText = JSON.stringify(existing);
127
+ const nextText = JSON.stringify(nextServer);
128
+ if (previousText === nextText) {
129
+ return { ...result, action: 'already-configured' };
130
+ }
131
+
132
+ config.mcp = config.mcp && typeof config.mcp === 'object' ? config.mcp : {};
133
+ config.mcp.servers = config.mcp.servers && typeof config.mcp.servers === 'object'
134
+ ? config.mcp.servers
135
+ : {};
136
+ config.mcp.servers.aquifer = nextServer;
137
+ result.action = 'write-config';
138
+ result.backupPath = writeJsonWithBackup(configPath, config, { dryRun });
139
+ return result;
140
+ }
141
+
142
+ function normalizePluginEntry(existing = {}) {
143
+ const entry = existing && typeof existing === 'object' ? existing : {};
144
+ return {
145
+ ...entry,
146
+ enabled: true,
147
+ config: entry.config && typeof entry.config === 'object' ? entry.config : {},
148
+ };
149
+ }
150
+
151
+ function ensurePluginConfig({ openclawHome, extensionPath, dryRun = false }) {
152
+ const configPath = path.join(openclawHome, 'openclaw.json');
153
+ const result = {
154
+ configPath,
155
+ pluginId: OPENCLAW_PLUGIN_ID,
156
+ extensionPath,
157
+ action: 'skipped',
158
+ backupPath: null,
159
+ previousLoadPaths: null,
160
+ nextLoadPaths: null,
161
+ };
162
+
163
+ if (!fs.existsSync(configPath)) {
164
+ return { ...result, reason: 'openclaw.json not found' };
165
+ }
166
+
167
+ const config = readJson(configPath);
168
+ const plugins = config.plugins && typeof config.plugins === 'object' ? config.plugins : {};
169
+ const load = plugins.load && typeof plugins.load === 'object' ? plugins.load : {};
170
+ const previousLoadPaths = Array.isArray(load.paths) ? load.paths : [];
171
+ const nextLoadPaths = previousLoadPaths.includes(extensionPath)
172
+ ? previousLoadPaths
173
+ : [...previousLoadPaths, extensionPath];
174
+ const entries = plugins.entries && typeof plugins.entries === 'object' ? plugins.entries : {};
175
+ const previousEntry = entries[OPENCLAW_PLUGIN_ID];
176
+ const nextEntry = normalizePluginEntry(previousEntry);
177
+ const previousAllow = Array.isArray(plugins.allow) ? plugins.allow : null;
178
+ const nextAllow = previousAllow && !previousAllow.includes(OPENCLAW_PLUGIN_ID)
179
+ ? [...previousAllow, OPENCLAW_PLUGIN_ID]
180
+ : previousAllow;
181
+
182
+ result.previousLoadPaths = previousLoadPaths;
183
+ result.nextLoadPaths = nextLoadPaths;
184
+
185
+ const unchanged = JSON.stringify(previousLoadPaths) === JSON.stringify(nextLoadPaths)
186
+ && JSON.stringify(previousEntry) === JSON.stringify(nextEntry)
187
+ && JSON.stringify(previousAllow) === JSON.stringify(nextAllow);
188
+ if (unchanged) {
189
+ return { ...result, action: 'already-configured' };
190
+ }
191
+
192
+ config.plugins = {
193
+ ...plugins,
194
+ entries: {
195
+ ...entries,
196
+ [OPENCLAW_PLUGIN_ID]: nextEntry,
197
+ },
198
+ load: {
199
+ ...load,
200
+ paths: nextLoadPaths,
201
+ },
202
+ };
203
+ if (nextAllow) config.plugins.allow = nextAllow;
204
+
205
+ result.action = 'write-config';
206
+ result.backupPath = writeJsonWithBackup(configPath, config, { dryRun });
207
+ return result;
208
+ }
209
+
210
+ function inspectInstalledPackage(openclawHome) {
211
+ const packagePath = path.join(openclawHome, 'node_modules', '@shadowforge0', 'aquifer-memory', 'package.json');
212
+ if (!fs.existsSync(packagePath)) {
213
+ return { packagePath, installed: false, version: null };
214
+ }
215
+ const pkg = readJson(packagePath);
216
+ return { packagePath, installed: true, version: pkg.version || null };
217
+ }
218
+
219
+ function buildInstallReport(args = {}, opts = {}) {
220
+ const flags = args.flags || {};
221
+ const env = opts.env || process.env;
222
+ const openclawHome = resolveOpenClawHome(flags, env);
223
+ const packageRoot = opts.packageRoot || path.resolve(__dirname, '..');
224
+ const dryRun = flags['dry-run'] === true;
225
+ const force = flags.force === true;
226
+ const linkCurrentPackage = flags['link-current-package'] === true;
227
+ const skipExtension = flags['skip-extension'] === true;
228
+ const skipMcp = flags['skip-mcp'] === true;
229
+
230
+ ensureOpenClawHome(openclawHome);
231
+
232
+ const installedPackage = inspectInstalledPackage(openclawHome);
233
+ const expectedPackageRoot = path.join(openclawHome, 'node_modules', '@shadowforge0', 'aquifer-memory');
234
+ const targetPackageRoot = linkCurrentPackage || !installedPackage.installed
235
+ ? packageRoot
236
+ : expectedPackageRoot;
237
+ const report = {
238
+ ok: true,
239
+ dryRun,
240
+ openclawHome,
241
+ packageRoot,
242
+ targetPackageRoot,
243
+ linkCurrentPackage,
244
+ packageVersion,
245
+ expectedPackageRoot,
246
+ installedPackage,
247
+ extension: skipExtension ? { action: 'skipped', reason: '--skip-extension' } : null,
248
+ plugin: skipExtension ? { action: 'skipped', reason: '--skip-extension' } : null,
249
+ mcp: skipMcp ? { action: 'skipped', reason: '--skip-mcp' } : null,
250
+ warnings: [],
251
+ };
252
+
253
+ if (installedPackage.installed && installedPackage.version && installedPackage.version !== packageVersion) {
254
+ report.warnings.push(
255
+ `OpenClaw node_modules has @shadowforge0/aquifer-memory ${installedPackage.version}; installer package is ${packageVersion}.`
256
+ );
257
+ }
258
+ if (!samePath(targetPackageRoot, expectedPackageRoot)) {
259
+ report.warnings.push(
260
+ `MCP will point outside OpenClaw node_modules: ${targetPackageRoot}. For a durable package install, run npm install --prefix "${openclawHome}" @shadowforge0/aquifer-memory@${packageVersion}.`
261
+ );
262
+ }
263
+ if (!samePath(packageRoot, expectedPackageRoot) && samePath(targetPackageRoot, expectedPackageRoot)) {
264
+ report.warnings.push(
265
+ `Installer is running from ${packageRoot}, but durable OpenClaw wiring targets ${expectedPackageRoot}. Use --link-current-package only for source-checkout development.`
266
+ );
267
+ }
268
+
269
+ if (!skipExtension) {
270
+ report.extension = ensureExtensionLink({ openclawHome, packageRoot: targetPackageRoot, dryRun, force });
271
+ report.plugin = ensurePluginConfig({
272
+ openclawHome,
273
+ extensionPath: report.extension.dest,
274
+ dryRun,
275
+ });
276
+ }
277
+
278
+ if (!skipMcp) {
279
+ report.mcp = ensureMcpConfig({ openclawHome, packageRoot: targetPackageRoot, dryRun });
280
+ }
281
+
282
+ return report;
283
+ }
284
+
285
+ function formatInstallReport(report) {
286
+ const lines = [];
287
+ lines.push(`Aquifer package: ${report.packageVersion} at ${report.packageRoot}`);
288
+ if (report.installedPackage.installed) {
289
+ lines.push(`OpenClaw package: ${report.installedPackage.version} at ${report.installedPackage.packagePath}`);
290
+ } else {
291
+ lines.push(`OpenClaw package: not installed at ${report.installedPackage.packagePath}`);
292
+ }
293
+ lines.push(`Extension: ${report.extension.action}${report.extension.dest ? ` (${report.extension.dest})` : ''}`);
294
+ if (report.extension.backupPath) lines.push(`Extension backup: ${report.extension.backupPath}`);
295
+ lines.push(`Plugin config: ${report.plugin.action}${report.plugin.configPath ? ` (${report.plugin.configPath})` : ''}`);
296
+ if (report.plugin.backupPath) lines.push(`Plugin config backup: ${report.plugin.backupPath}`);
297
+ if (report.plugin.reason) lines.push(`Plugin config note: ${report.plugin.reason}`);
298
+ lines.push(`MCP config: ${report.mcp.action}${report.mcp.configPath ? ` (${report.mcp.configPath})` : ''}`);
299
+ if (report.mcp.backupPath) lines.push(`MCP config backup: ${report.mcp.backupPath}`);
300
+ if (report.mcp.reason) lines.push(`MCP config note: ${report.mcp.reason}`);
301
+ for (const warning of report.warnings) lines.push(`Warning: ${warning}`);
302
+ if (report.dryRun) lines.push('Dry run: no files were changed.');
303
+ return lines.join('\n');
304
+ }
305
+
306
+ async function cmdInstallOpenClaw(args = {}, opts = {}) {
307
+ const report = buildInstallReport(args, opts);
308
+ if (args.flags?.json) {
309
+ console.log(JSON.stringify(report, null, 2));
310
+ } else {
311
+ console.log(formatInstallReport(report));
312
+ }
313
+ return report;
314
+ }
315
+
316
+ module.exports = {
317
+ buildInstallReport,
318
+ cmdInstallOpenClaw,
319
+ ensureExtensionLink,
320
+ ensurePluginConfig,
321
+ ensureMcpConfig,
322
+ formatInstallReport,
323
+ inspectInstalledPackage,
324
+ normalizeMcpServer,
325
+ resolveOpenClawHome,
326
+ };
@@ -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
@@ -33,6 +37,43 @@ function coerceRawEntries(messages) {
33
37
  });
34
38
  }
35
39
 
40
+ function normalizeTimestamp(value) {
41
+ if (value === null || value === undefined || value === '') return null;
42
+
43
+ if (value instanceof Date) {
44
+ const time = value.getTime();
45
+ return Number.isFinite(time) ? value.toISOString() : null;
46
+ }
47
+
48
+ if (typeof value === 'number') {
49
+ if (!Number.isFinite(value)) return null;
50
+ const millis = Math.abs(value) < 1e12 ? value * 1000 : value;
51
+ const date = new Date(millis);
52
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
53
+ }
54
+
55
+ if (typeof value === 'string') {
56
+ const trimmed = value.trim();
57
+ if (!trimmed) return null;
58
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
59
+ return normalizeTimestamp(Number(trimmed));
60
+ }
61
+ const parsed = Date.parse(trimmed);
62
+ if (!Number.isFinite(parsed)) return null;
63
+ return new Date(parsed).toISOString();
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ function selectTimestamp(...values) {
70
+ for (const value of values) {
71
+ const ts = normalizeTimestamp(value);
72
+ if (ts) return ts;
73
+ }
74
+ return null;
75
+ }
76
+
36
77
  function normalizeEntries(rawEntries) {
37
78
  const normalized = [];
38
79
  let userCount = 0, assistantCount = 0;
@@ -43,7 +84,7 @@ function normalizeEntries(rawEntries) {
43
84
  if (!entry) continue;
44
85
  const msg = entry.message || entry;
45
86
  if (!msg || !msg.role) continue;
46
- if (!['user', 'assistant', 'system'].includes(msg.role)) continue;
87
+ if (!['user', 'assistant'].includes(msg.role)) continue;
47
88
 
48
89
  let content = '';
49
90
  if (typeof msg.content === 'string') {
@@ -55,7 +96,7 @@ function normalizeEntries(rawEntries) {
55
96
  .join('\n');
56
97
  }
57
98
 
58
- const ts = entry.timestamp || msg.timestamp || null;
99
+ const ts = selectTimestamp(entry.timestamp, msg.timestamp);
59
100
  if (ts && !startedAt) startedAt = ts;
60
101
  if (ts) lastMessageAt = ts;
61
102
 
@@ -117,18 +158,62 @@ module.exports = buildPlugin();
117
158
  // contract; OpenClaw reads { id, name, register } only.
118
159
  module.exports.normalizeEntries = normalizeEntries;
119
160
  module.exports.coerceRawEntries = coerceRawEntries;
161
+ module.exports.normalizeTimestamp = normalizeTimestamp;
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
+ }
120
204
 
121
205
  function register(api) {
122
206
  const pluginConfig = api.pluginConfig || {};
123
207
 
124
208
  // v1.2.0: delegate to a persona layer if one is configured, otherwise
125
- // run the generic default path (before_reset + session_recall + feedback).
209
+ // run the generic default path.
126
210
  const personaPath = pluginConfig.persona || process.env.AQUIFER_PERSONA;
127
211
  if (personaPath) {
128
212
  try {
129
213
  const persona = require(personaPath);
130
214
  if (persona && typeof persona.mountOnOpenClaw === 'function') {
131
- persona.mountOnOpenClaw(api, pluginConfig);
215
+ const { mounted, registeredTools } = runPersonaMount(api, persona, pluginConfig);
216
+ registerProductStatusAfterPersona(api, pluginConfig, mounted, registeredTools);
132
217
  api.logger.info(`[aquifer-memory] registered via persona: ${personaPath}`);
133
218
  return;
134
219
  }
@@ -194,6 +279,10 @@ function register(api) {
194
279
  })();
195
280
  });
196
281
 
282
+ // --- product status tools ---
283
+
284
+ registerOpenClawProductStatusTools(api, aquifer);
285
+
197
286
  // --- session_recall tool ---
198
287
 
199
288
  api.registerTool((ctx) => {
@@ -211,31 +300,23 @@ function register(api) {
211
300
  source: { type: 'string', description: 'Filter by source' },
212
301
  dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
213
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' },
214
305
  entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
215
306
  entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
216
307
  mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall mode: "fts" (keyword only), "hybrid" (default), "vector" (vector only)' },
217
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' },
218
311
  },
219
312
  required: ['query'],
220
313
  },
221
314
  async execute(_toolCallId, params) {
222
315
  try {
223
- const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
224
- const recallOpts = {
225
- limit,
226
- agentId: params.agentId || undefined,
227
- source: params.source || undefined,
228
- dateFrom: params.dateFrom || undefined,
229
- dateTo: params.dateTo || undefined,
230
- };
231
- if (Array.isArray(params.entities) && params.entities.length > 0) {
232
- recallOpts.entities = params.entities;
233
- recallOpts.entityMode = params.entityMode || 'any';
234
- }
235
- if (params.mode) recallOpts.mode = params.mode;
236
-
237
- const results = await aquifer.recall(params.query, recallOpts);
238
- 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');
239
320
  return { content: [{ type: 'text', text }] };
240
321
  } catch (err) {
241
322
  return {
@@ -325,5 +406,5 @@ function register(api) {
325
406
  };
326
407
  }, { name: 'feedback_stats' });
327
408
 
328
- 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)');
329
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
+ };
@@ -55,6 +55,7 @@ const DEFAULTS = {
55
55
  servingMode: 'legacy', // 'legacy' | 'curated'
56
56
  activeScopeKey: null,
57
57
  activeScopePath: null,
58
+ allowedScopeKeys: null,
58
59
  },
59
60
  codex: {
60
61
  checkpoint: {
@@ -120,6 +121,7 @@ const ENV_MAP = [
120
121
  ['AQUIFER_MEMORY_SERVING_MODE', 'memory.servingMode'],
121
122
  ['AQUIFER_MEMORY_ACTIVE_SCOPE_KEY', 'memory.activeScopeKey'],
122
123
  ['AQUIFER_MEMORY_ACTIVE_SCOPE_PATH', 'memory.activeScopePath'],
124
+ ['AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS', 'memory.allowedScopeKeys'],
123
125
  ['AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MS', 'codex.checkpoint.checkIntervalMs', Number],
124
126
  ['AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MINUTES', 'codex.checkpoint.checkIntervalMinutes', Number],
125
127
  ['AQUIFER_CODEX_CHECKPOINT_EVERY_MESSAGES', 'codex.checkpoint.everyMessages', Number],