@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
@@ -6,6 +6,7 @@ const {
6
6
  promptSafeSynthesisInput,
7
7
  stableJson,
8
8
  } = require('../core/session-checkpoint-producer');
9
+ const { assistantShapingPromptLines } = require('../core/memory-type-policy');
9
10
  const { compactCurrentMemorySnapshot } = require('./codex-current-memory');
10
11
 
11
12
  function positiveInt(value, fallback, max = 100000) {
@@ -122,8 +123,9 @@ function buildActiveSessionCheckpointPrompt(checkpointInput = {}, opts = {}) {
122
123
  'Use only the <active_checkpoint_input> block. Do not use hidden tool output, injected context, or debug material.',
123
124
  'This checkpoint is process material for later handoff. It is not active current memory and must not be treated as final truth.',
124
125
  'Return compact JSON with this shape:',
125
- '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}',
126
+ '{"summaryText":"...","structuredSummary":{"assistant_shaping":[],"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}',
126
127
  `Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
128
+ ...assistantShapingPromptLines(),
127
129
  'Preserve the coverage object so a later handoff can skip the already-covered transcript prefix.',
128
130
  '',
129
131
  '<active_checkpoint_input>',
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { formatRuntimeMemoryLine } = require('../core/memory-type-policy');
4
+
3
5
  function compactCurrentMemoryRow(row = {}) {
4
6
  const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
5
7
  const confidence = payload.confidence || payload.currentMemoryConfidence || null;
@@ -10,6 +12,9 @@ function compactCurrentMemoryRow(row = {}) {
10
12
  summary: String(row.summary || row.title || '').replace(/\s+/g, ' ').trim(),
11
13
  authority: row.authority || null,
12
14
  confidence,
15
+ policyKey: payload.policyKey || payload.policy_key || row.policyKey || row.policy_key || null,
16
+ shapingKind: payload.shapingKind || payload.shaping_kind || payload.kind || payload.category || row.shapingKind || row.shaping_kind || null,
17
+ servingImpact: payload.servingImpact || payload.serving_impact || payload.impact || row.servingImpact || row.serving_impact || null,
13
18
  };
14
19
  }
15
20
 
@@ -35,12 +40,11 @@ function formatCurrentMemoryPromptBlock(currentMemory = null, opts = {}) {
35
40
  `truncated="${Boolean(meta.truncated || rows.length > compactRows.length)}"`,
36
41
  `degraded="${Boolean(meta.degraded || currentMemory?.error)}"`,
37
42
  ];
38
- const lines = compactRows.map(row => {
39
- const scope = row.scopeKey ? ` scope=${row.scopeKey}` : '';
40
- const authority = row.authority ? ` authority=${row.authority}` : '';
41
- const confidence = row.confidence ? ` confidence=${row.confidence}` : '';
42
- return `- ${row.memoryType}${scope}${authority}${confidence}: ${row.summary}`;
43
- });
43
+ const lines = compactRows.map(row => formatRuntimeMemoryLine(row, {
44
+ includeScope: true,
45
+ includeAuthority: true,
46
+ includeConfidence: true,
47
+ }));
44
48
  if (currentMemory && currentMemory.error && lines.length === 0) {
45
49
  lines.push(`- degraded: ${String(currentMemory.error).replace(/\s+/g, ' ').trim()}`);
46
50
  }
@@ -22,6 +22,7 @@ const crypto = require('crypto');
22
22
  const DEFAULT_CODEX_HOME = path.join(os.homedir(), '.codex');
23
23
  const { normalizeMessages } = require('./shared/normalize');
24
24
  const { applyEnrichSafetyGate } = require('../core/memory-safety-gate');
25
+ const { assistantShapingPromptLines } = require('../core/memory-type-policy');
25
26
  const {
26
27
  buildActiveSessionCheckpointInput,
27
28
  buildActiveSessionCheckpointPrompt,
@@ -40,7 +41,7 @@ const DEFAULT_MAX_AFTERBURNS = 1;
40
41
  const DEFAULT_MIN_IMPORT_USER_MESSAGES = 3;
41
42
  const MAX_RETRY_COUNT = 3;
42
43
  const SAFE_SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
43
- const DEFAULT_RECOVERY_MAX_BYTES = 1024 * 1024;
44
+ const DEFAULT_RECOVERY_MAX_BYTES = 20 * 1024 * 1024;
44
45
  const DEFAULT_RECOVERY_MAX_MESSAGES = 80;
45
46
  const DEFAULT_RECOVERY_MAX_CHARS = 24000;
46
47
  const DEFAULT_RECOVERY_MAX_PROMPT_TOKENS = 9000;
@@ -1335,8 +1336,9 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
1335
1336
  'You are finalizing an Aquifer memory session for Codex.',
1336
1337
  'Use only the sanitized transcript below. Do not infer from hidden tool output or injected context.',
1337
1338
  'Return compact JSON with this shape:',
1338
- '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
1339
+ '{"summaryText":"...","structuredSummary":{"assistant_shaping":[],"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
1339
1340
  `Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
1341
+ ...assistantShapingPromptLines(),
1340
1342
  '',
1341
1343
  `sessionId: ${view.sessionId}`,
1342
1344
  `transcriptHash: ${view.transcriptHash}`,
@@ -1352,7 +1354,8 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
1352
1354
  0,
1353
1355
  'Use current_memory as the already-committed current state. Reconcile the transcript against it: keep valid state, supersede stale state, and mark uncertain items explicitly.',
1354
1356
  );
1355
- lines.splice(10, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
1357
+ const transcriptIndex = lines.indexOf('<sanitized_transcript>');
1358
+ lines.splice(transcriptIndex >= 0 ? transcriptIndex : lines.length, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
1356
1359
  }
1357
1360
  return lines.join('\n');
1358
1361
  }
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  // Aquifer default persona — parameterized daily_entries writer.
4
- // Schema matches miranda.daily_entries (id / event_at / source / tag / text /
4
+ // Schema matches the generic daily_entries shape (id / event_at / source / tag / text /
5
5
  // agent_id / session_id / metadata / dedupe_key) — hosts clone that DDL into
6
6
  // their own schema and set persona.dailyTable = '<schema>.daily_entries'.
7
7
  //
8
8
  // Host must create the table before use:
9
- // CREATE TABLE jenny.daily_entries (LIKE miranda.daily_entries INCLUDING ALL);
9
+ // CREATE TABLE app_memory.daily_entries (...);
10
10
 
11
11
  const crypto = require('crypto');
12
12
  const { parseHandoffSection } = require('../shared/summary-parser');
@@ -6,13 +6,13 @@
6
6
  // Entry point:
7
7
  // const persona = require('@shadowforge0/aquifer-memory/consumers/default')
8
8
  // .createPersona({
9
- // agentName: 'Dobby',
10
- // observedOwner: 'evan',
11
- // schema: 'jenny',
12
- // scope: 'jenny',
13
- // dailyTable: 'jenny.daily_entries', // or null to skip daily writes
9
+ // agentName: 'Assistant',
10
+ // observedOwner: 'operator',
11
+ // schema: 'app_memory',
12
+ // scope: 'app_memory',
13
+ // dailyTable: 'app_memory.daily_entries', // or null to skip daily writes
14
14
  // language: 'zh-TW', // or 'en'
15
- // briefingIntro: '你是 Dobby。以下是現況...', // optional context-inject preamble
15
+ // briefingIntro: '以下是現況...', // optional context-inject preamble
16
16
  // });
17
17
  //
18
18
  // Returns a persona module with the standard persona adapter shape — host
@@ -21,13 +21,15 @@
21
21
  // .createPersona({ ... });
22
22
  //
23
23
  // This is intentionally a minimal persona: summary + optional daily_entries,
24
- // no workspace-files, no consolidation, no Miranda-specific scaffolding.
24
+ // no workspace-files, no consolidation, no host-specific scaffolding.
25
25
  // Host can extend by composing with Aquifer primitives from consumers/shared.
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
28
  const { createAquifer } = require('../../index');
29
29
  const { runIngest } = require('../shared/ingest');
30
30
  const { parseEntitySection } = require('../shared/entity-parser');
31
+ const { registerOpenClawProductStatusTools } = require('../shared/openclaw-product-tools');
32
+ const { buildCompatibilityRecallRequest, runCompatibilityRecall, memoryServingMode } = require('../shared/compat-recall');
31
33
 
32
34
  const summaryModule = require('./prompts/summary');
33
35
  const dailyEntriesModule = require('./daily-entries');
@@ -129,10 +131,10 @@ function createPersona(personaOpts = {}) {
129
131
  // v1.2.0: all four are env-driven by default. Host may override any of
130
132
  // them via opts. Aquifer core throws with clear guidance if the required
131
133
  // env vars are missing, so we do not pre-validate here.
132
- const aquifer = getAquifer(opts);
133
- const pool = opts.pool || aquifer.getPool();
134
- const llmFn = opts.llmFn || aquifer.getLlmFn();
135
- const embedFn = opts.embedFn || aquifer.getEmbedFn();
134
+ const aquifer = opts.aquifer || getAquifer(opts);
135
+ const pool = opts.pool || (aquifer.getPool ? aquifer.getPool() : null);
136
+ const llmFn = opts.llmFn || (aquifer.getLlmFn ? aquifer.getLlmFn() : null);
137
+ const embedFn = opts.embedFn || (aquifer.getEmbedFn ? aquifer.getEmbedFn() : null);
136
138
  return {
137
139
  pool,
138
140
  embedFn,
@@ -205,7 +207,16 @@ function createPersona(personaOpts = {}) {
205
207
  try {
206
208
  const agentId = ctx?.agentId || defaultAgentId;
207
209
  if ((ctx?.sessionKey || '').includes('subagent')) return;
208
- const recalled = await aquifer.bootstrap({ agentId, limit: 5, maxChars: 2000, format: 'text' });
210
+ const bootstrapOpts = { limit: 5, maxChars: 2000, format: 'text' };
211
+ if (memoryServingMode(aquifer) === 'curated') {
212
+ const activeScopeKey = opts.activeScopeKey || opts.active_scope_key || ctx?.activeScopeKey || ctx?.active_scope_key;
213
+ const activeScopePath = opts.activeScopePath || opts.active_scope_path || ctx?.activeScopePath || ctx?.active_scope_path;
214
+ if (activeScopeKey) bootstrapOpts.activeScopeKey = activeScopeKey;
215
+ if (activeScopePath) bootstrapOpts.activeScopePath = activeScopePath;
216
+ } else {
217
+ bootstrapOpts.agentId = agentId;
218
+ }
219
+ const recalled = await aquifer.bootstrap(bootstrapOpts);
209
220
  const context = persona.briefingIntro + (recalled ? `\n\n${recalled}` : '');
210
221
  if (context.length > 0) return { prependSystemContext: context };
211
222
  } catch (err) {
@@ -232,33 +243,23 @@ function createPersona(personaOpts = {}) {
232
243
  source: { type: 'string' },
233
244
  date_from: { type: 'string' },
234
245
  date_to: { type: 'string' },
246
+ host: { type: 'string' },
247
+ session_id: { type: 'string' },
235
248
  entities: { type: 'array', items: { type: 'string' }, description: 'Named entities (person/project/tool/file)' },
236
249
  entity_mode: { type: 'string', enum: ['any', 'all'], description: '"any" boosts; "all" hard-filters to sessions containing every entity' },
237
250
  mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall strategy, default hybrid' },
251
+ active_scope_key: { type: 'string' },
252
+ active_scope_path: { type: 'array', items: { type: 'string' } },
238
253
  },
239
254
  },
240
255
  async execute(_toolCallId, params) {
241
256
  try {
242
- const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
243
- const recallOpts = {
244
- agentId: params?.agent_id || ctx?.agentId || undefined,
245
- source: params?.source || undefined,
246
- dateFrom: params?.date_from || undefined,
247
- dateTo: params?.date_to || undefined,
248
- limit,
249
- };
250
- if (Array.isArray(params?.entities) && params.entities.length > 0) {
251
- recallOpts.entities = params.entities;
252
- recallOpts.entityMode = params?.entity_mode || 'any';
253
- }
254
- if (params?.mode === 'fts' || params?.mode === 'hybrid' || params?.mode === 'vector') {
255
- recallOpts.mode = params.mode;
256
- }
257
- const results = await aquifer.recall(String(params?.query || ''), recallOpts);
257
+ const request = buildCompatibilityRecallRequest(aquifer, params || {}, ctx || {});
258
+ const results = await runCompatibilityRecall(aquifer, String(params?.query || ''), request);
258
259
  const lines = results.map((r, i) =>
259
260
  `${i+1}. ${r.structuredSummary?.title || r.summaryText?.slice(0, 80) || '(untitled)'}`
260
261
  );
261
- return { content: [{ type: 'text', text: lines.join('\n') || 'No matching sessions.' }] };
262
+ return { content: [{ type: 'text', text: [request.laneHeader, '', lines.join('\n') || 'No matching sessions.'].join('\n') }] };
262
263
  } catch (err) {
263
264
  return { content: [{ type: 'text', text: `session_recall error: ${err.message}` }], isError: true };
264
265
  }
@@ -269,11 +270,19 @@ function createPersona(personaOpts = {}) {
269
270
  return { aquifer };
270
271
  }
271
272
 
273
+ function registerProductStatusTools(api, opts = {}) {
274
+ const aquifer = opts.aquifer || resolveCommon(opts).aquifer;
275
+ registerOpenClawProductStatusTools(api, aquifer);
276
+ api.logger.info('[default-persona] registerProductStatusTools: memory_stats + memory_pending registered');
277
+ return { aquifer, productStatusToolsRegistered: true };
278
+ }
279
+
272
280
  function mountOnOpenClaw(api, opts = {}) {
273
281
  const r = registerAfterburn(api, opts);
282
+ registerProductStatusTools(api, { ...opts, aquifer: r.aquifer });
274
283
  registerContextInject(api, opts);
275
284
  registerRecallTool(api, opts);
276
- return r;
285
+ return { ...r, productStatusToolsRegistered: true };
277
286
  }
278
287
 
279
288
  return {
@@ -282,6 +291,7 @@ function createPersona(personaOpts = {}) {
282
291
  registerAfterburn,
283
292
  registerContextInject,
284
293
  registerRecallTool,
294
+ registerProductStatusTools,
285
295
  buildPostProcess,
286
296
  buildSummaryFn,
287
297
  buildEntityParseFn,
@@ -5,11 +5,11 @@
5
5
  // Parameterized via personaOpts:
6
6
  // agentName — human name/role the prompt addresses (default 'Assistant')
7
7
  // observedOwner — if set, the prompt asks for a short observation about
8
- // that person (matches Miranda's "對 MK 的觀察" slot).
8
+ // that person.
9
9
  // null → the section is omitted entirely.
10
10
  // language — 'en' | 'zh-TW' (default 'en')
11
11
  //
12
- // Output format mirrors Miranda's RECAP fields so downstream daily-entries
12
+ // Output format mirrors the generic RECAP fields so downstream daily-entries
13
13
  // parsing works uniformly across personas.
14
14
 
15
15
  function buildSummaryPrompt({ conversationText, agentId, now, dailyContext, persona = {} }) {
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,42 +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
- lines.push(`Summaries: ${stats.summaries}`);
393
- lines.push(`Turn embeddings: ${stats.turnEmbeddings}`);
394
- lines.push(`Entities: ${stats.entities}`);
395
- if (stats.memoryRecords) {
396
- lines.push(`Memory records: ${stats.memoryRecords.total} total (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
397
- 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)}`);
398
- }
399
- if (stats.sessionFinalizations?.available) {
400
- const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
401
- .map(([status, count]) => `${status}: ${count}`)
402
- .join(', ') || 'none';
403
- lines.push(`Session finalizations: ${stats.sessionFinalizations.total} total (${statusText})`);
404
- if (stats.sessionFinalizations.latestFinalizedAt) {
405
- lines.push(`Latest finalization: ${new Date(stats.sessionFinalizations.latestFinalizedAt).toISOString().slice(0, 10)}`);
406
- }
407
- }
408
- if (stats.earliest) lines.push(`Date range: ${new Date(stats.earliest).toISOString().slice(0, 10)} → ${new Date(stats.latest).toISOString().slice(0, 10)}`);
409
- if ((stats.serving?.mode || 'legacy') !== 'curated') {
410
- lines.push('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
411
- }
412
- return { content: [{ type: 'text', text: lines.join('\n') }] };
412
+ const text = formatMemoryStatsInterface(stats, { diagnostics: params.diagnostics === true });
413
+ return { content: [{ type: 'text', text }] };
413
414
  } catch (err) {
414
415
  return {
415
416
  content: [{ type: 'text', text: `memory_stats error: ${err.message}` }],
@@ -421,22 +422,31 @@ async function main() {
421
422
 
422
423
  server.tool(
423
424
  'memory_pending',
424
- 'List sessions with pending or failed processing status.',
425
+ manifestToolDescription('memory_pending', 'Return saved-content preparation status.'),
425
426
  {
426
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.'),
427
432
  },
428
433
  async (params) => {
429
434
  try {
430
435
  const aquifer = getAquifer();
431
- const rows = await aquifer.getPendingSessions({ limit: params.limit ?? 20 });
432
- if (rows.length === 0) {
433
- return { content: [{ type: 'text', text: 'No pending or failed sessions.' }] };
434
- }
435
- const lines = [`${rows.length} pending/failed session(s):\n`];
436
- for (const row of rows) {
437
- 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 }] };
438
446
  }
439
- 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 }] };
440
450
  } catch (err) {
441
451
  return {
442
452
  content: [{ type: 'text', text: `memory_pending error: ${err.message}` }],
@@ -496,16 +506,16 @@ async function main() {
496
506
  if (!envelope.ready) {
497
507
  const err = envelope.error || { code: 'AQ_MIGRATION_NOT_READY', message: 'aquifer.init() did not reach ready state' };
498
508
  process.stderr.write(
499
- `[aquifer-mcp] startup aborted: migrationMode=${envelope.migrationMode} ` +
500
- `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)} ` +
501
511
  `error=${err.code || 'unknown'}: ${err.message}\n`
502
512
  );
503
513
  await aquifer.close().catch(() => {});
504
514
  process.exit(1);
505
515
  }
506
516
  process.stderr.write(
507
- `[aquifer-mcp] init ok: mode=${envelope.migrationMode} applied=${envelope.appliedMigrations.length} ` +
508
- `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`
509
519
  );
510
520
 
511
521
  const transport = new StdioServerTransport();
@@ -6,12 +6,14 @@
6
6
  // $OPENCLAW_HOME/extensions/aquifer-memory/ ← symlink to this directory
7
7
  //
8
8
  // Behavior:
9
- // - Loads $OPENCLAW_HOME/.env so DATABASE_URL / EMBED_PROVIDER /
10
- // AQUIFER_LLM_PROVIDER etc. are visible to the plugin.
9
+ // - Reads $OPENCLAW_HOME/.env and then mcp.servers.aquifer.env from
10
+ // $OPENCLAW_HOME/openclaw.json so ingest uses the same DB/schema/env as
11
+ // the Aquifer MCP recall tools, without leaving those values in the
12
+ // shared OpenClaw process environment.
11
13
  // - Delegates to consumers/openclaw-plugin.js. If AQUIFER_PERSONA is set
12
14
  // (pluginConfig.persona or env), the plugin loads the persona module
13
15
  // and hands off mountOnOpenClaw(api); otherwise the default generic
14
- // path runs (before_reset capture + session_recall + session_feedback).
16
+ // path runs (before_reset capture + product status, recall, and feedback tools).
15
17
  //
16
18
  // Host-specific customization goes in a persona module, not here.
17
19
 
@@ -21,17 +23,73 @@ const os = require('os');
21
23
 
22
24
  const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
23
25
 
24
- function loadEnvFile(envPath) {
26
+ function mergeEnvObject(target, values, opts = {}) {
27
+ if (!values || typeof values !== 'object') return;
28
+ for (const [key, value] of Object.entries(values)) {
29
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) continue;
30
+ if (value === undefined || value === null) continue;
31
+ if (!opts.override && (process.env[key] || target[key])) continue;
32
+ target[key] = String(value);
33
+ }
34
+ }
35
+
36
+ function readEnvFile(envPath) {
37
+ const values = {};
25
38
  try {
26
39
  const text = fs.readFileSync(envPath, 'utf8');
27
40
  for (const line of text.split('\n')) {
28
41
  const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
29
- if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
42
+ if (m) values[m[1]] = m[2].trim();
30
43
  }
31
44
  } catch { /* .env missing — ok */ }
45
+ return values;
46
+ }
47
+
48
+ function readOpenClawAquiferMcpEnv(configPath) {
49
+ try {
50
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
51
+ return cfg.mcp?.servers?.aquifer?.env || {};
52
+ } catch { /* openclaw.json missing or malformed — plugin config/env may still work */ }
53
+ return {};
54
+ }
55
+
56
+ function buildAquiferEnv() {
57
+ const env = {};
58
+ mergeEnvObject(env, readEnvFile(path.join(OPENCLAW_HOME, '.env')));
59
+ mergeEnvObject(env, readOpenClawAquiferMcpEnv(path.join(OPENCLAW_HOME, 'openclaw.json')), { override: true });
60
+ return env;
32
61
  }
33
62
 
34
- loadEnvFile(path.join(OPENCLAW_HOME, '.env'));
63
+ function withEnvOverlay(env, fn) {
64
+ const previous = new Map();
65
+ for (const [key, value] of Object.entries(env || {})) {
66
+ previous.set(key, Object.prototype.hasOwnProperty.call(process.env, key) ? process.env[key] : undefined);
67
+ process.env[key] = value;
68
+ }
69
+ try {
70
+ return fn();
71
+ } finally {
72
+ for (const [key, value] of previous.entries()) {
73
+ if (value === undefined) delete process.env[key];
74
+ else process.env[key] = value;
75
+ }
76
+ }
77
+ }
35
78
 
36
79
  // Re-export the plugin as-is. OpenClaw expects { id, name, register }.
37
- module.exports = require('../openclaw-plugin');
80
+ const plugin = require('../openclaw-plugin');
81
+
82
+ module.exports = {
83
+ ...plugin,
84
+ register(api) {
85
+ const aquiferEnv = buildAquiferEnv();
86
+ const pluginConfig = { ...(api.pluginConfig || {}) };
87
+ if (!pluginConfig.persona && aquiferEnv.AQUIFER_PERSONA) {
88
+ pluginConfig.persona = aquiferEnv.AQUIFER_PERSONA;
89
+ }
90
+ return withEnvOverlay(aquiferEnv, () => plugin.register({
91
+ ...api,
92
+ pluginConfig,
93
+ }));
94
+ },
95
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "aquifer-memory",
3
3
  "name": "Aquifer Memory",
4
- "version": "1.2.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.2.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.",