@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.
- package/.env.example +1 -0
- package/README.md +82 -26
- package/README_CN.md +33 -23
- package/README_TW.md +25 -24
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +587 -33
- package/consumers/codex-active-checkpoint.js +3 -1
- package/consumers/codex-current-memory.js +10 -6
- package/consumers/codex.js +6 -3
- package/consumers/default/daily-entries.js +2 -2
- package/consumers/default/index.js +40 -30
- package/consumers/default/prompts/summary.js +2 -2
- package/consumers/mcp.js +56 -46
- package/consumers/openclaw-ext/index.js +65 -7
- package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
- package/consumers/openclaw-ext/package.json +1 -1
- package/consumers/openclaw-install.js +326 -0
- package/consumers/openclaw-plugin.js +105 -24
- package/consumers/shared/compat-recall.js +101 -0
- package/consumers/shared/config.js +2 -0
- package/consumers/shared/openclaw-product-tools.js +130 -0
- package/consumers/shared/recall-format.js +2 -2
- package/core/aquifer.js +553 -41
- package/core/backends/local.js +169 -1
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/finalization-review.js +88 -42
- package/core/interface.js +629 -0
- package/core/mcp-manifest.js +11 -3
- package/core/memory-bootstrap.js +25 -27
- package/core/memory-consolidation.js +564 -42
- package/core/memory-explain.js +593 -0
- package/core/memory-promotion.js +392 -55
- package/core/memory-recall.js +75 -71
- package/core/memory-records.js +107 -108
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/memory-type-policy.js +298 -0
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +22 -0
- package/core/session-checkpoint-producer.js +3 -1
- package/core/session-checkpoints.js +1 -1
- package/core/session-finalization.js +78 -3
- package/core/storage.js +124 -8
- package/docs/getting-started.md +50 -4
- package/docs/setup.md +163 -24
- package/package.json +5 -4
- package/schema/004-completion.sql +4 -4
- package/schema/010-v1-finalization-review.sql +72 -0
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/schema/020-v1-assistant-shaping-memory.sql +30 -0
- package/scripts/backfill-canonical-key.js +1 -1
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
- package/scripts/diagnose-fts-zh.js +1 -1
- 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
|
|
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
|
|
12
|
-
* session capture; tool registration
|
|
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'
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
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],
|