@shadowforge0/aquifer-memory 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +374 -39
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
@@ -0,0 +1,34 @@
1
+ {
2
+ "db": {
3
+ "url": "postgresql://user:password@localhost:5432/mydb",
4
+ "max": 10
5
+ },
6
+ "schema": "aquifer",
7
+ "tenantId": "default",
8
+ "defaults": {
9
+ "agentId": "main",
10
+ "source": "api"
11
+ },
12
+ "memory": {
13
+ "servingMode": "legacy",
14
+ "activeScopeKey": null,
15
+ "activeScopePath": null
16
+ },
17
+ "embed": {
18
+ "baseUrl": "http://localhost:11434/v1",
19
+ "model": "bge-m3",
20
+ "dim": null
21
+ },
22
+ "llm": {
23
+ "baseUrl": null,
24
+ "model": null
25
+ },
26
+ "entities": {
27
+ "enabled": false,
28
+ "scope": "default"
29
+ },
30
+ "rerank": {
31
+ "enabled": false,
32
+ "provider": null
33
+ }
34
+ }
@@ -3,10 +3,9 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  // Claude Code host adapter.
5
5
  //
6
- // Generic entry points for CC-side afterburn hooks. No persona logic — the
7
- // caller (typically cc-afterburn.js) constructs the Miranda persona hooks
8
- // via consumers/miranda and injects them via `postProcess`, `summaryFn`,
9
- // `entityParseFn`.
6
+ // Generic entry points for CC-side afterburn hooks. No persona logic — callers
7
+ // construct any persona-specific hooks outside this package and inject them via
8
+ // `postProcess`, `summaryFn`, `entityParseFn`, or `contextInjector`.
10
9
  //
11
10
  // API:
12
11
  // runEnrich({ aquifer, sessionId, agentId, ... })
@@ -16,9 +15,9 @@
16
15
  // runBackfill({ aquifer, sessionIds, ... })
17
16
  // Iterate enrich() over pending sessions (for catch-up after a gap).
18
17
  //
19
- // runContextInject({ aquifer, pool, agentId })
20
- // Return the Miranda-flavored system context string for a CC session
21
- // start hook. (Delegates to consumers/miranda/context-inject.)
18
+ // runContextInject({ contextInjector, ... })
19
+ // Return a host-specific system context string for a CC session start
20
+ // hook. The injector is supplied by the deployment, not by Aquifer core.
22
21
  // ---------------------------------------------------------------------------
23
22
 
24
23
  /**
@@ -106,12 +105,13 @@ async function runBackfill({
106
105
  }
107
106
 
108
107
  /**
109
- * Build the Miranda-flavored system context for a CC SessionStart hook.
110
- * Delegates to consumers/miranda/context-inject.computeInjection.
108
+ * Build a host-specific system context for a CC SessionStart hook.
109
+ * The caller supplies the context injector so this public adapter stays generic.
111
110
  */
112
111
  async function runContextInject(opts = {}) {
113
- const { computeInjection } = require('./miranda/context-inject');
114
- return computeInjection(opts);
112
+ const injector = opts.contextInjector || opts.computeInjection;
113
+ if (typeof injector !== 'function') throw new Error('runContextInject: contextInjector is required');
114
+ return injector(opts);
115
115
  }
116
116
 
117
117
  module.exports = { runEnrich, runBackfill, runContextInject };
package/consumers/cli.js CHANGED
@@ -11,10 +11,13 @@
11
11
  * aquifer backfill [options] Enrich pending sessions
12
12
  * aquifer stats [options] Show database statistics
13
13
  * aquifer export [options] Export sessions
14
+ * aquifer operator ... Run operator-safe consolidation jobs
14
15
  * aquifer mcp Start MCP server
15
16
  */
16
17
 
18
+ const fs = require('fs');
17
19
  const { createAquiferFromConfig } = require('./shared/factory');
20
+ const { formatRecallResults } = require('./shared/recall-format');
18
21
 
19
22
  function formatDate(value, fallback) {
20
23
  if (!value) return fallback;
@@ -36,6 +39,98 @@ function parsePositiveInt(value, fallback) {
36
39
  return Math.max(1, parsed);
37
40
  }
38
41
 
42
+ function parseScopePath(value) {
43
+ if (!value) return undefined;
44
+ const parts = String(value).split(',').map(s => s.trim()).filter(Boolean);
45
+ return parts.length > 0 ? parts : undefined;
46
+ }
47
+
48
+ function parseCsvList(value) {
49
+ if (!value) return undefined;
50
+ const parts = String(value).split(',').map(s => s.trim()).filter(Boolean);
51
+ return parts.length > 0 ? parts : undefined;
52
+ }
53
+
54
+ function hasQuickstartEmbedConfig(env) {
55
+ return !!(
56
+ env.EMBED_PROVIDER
57
+ || (env.AQUIFER_EMBED_BASE_URL && env.AQUIFER_EMBED_MODEL)
58
+ );
59
+ }
60
+
61
+ function printQuickstartFailure(title, detailLines = []) {
62
+ console.error(` FAIL — ${title}`);
63
+ for (const line of detailLines) {
64
+ if (line) console.error(` ${line}`);
65
+ }
66
+ }
67
+
68
+ function buildQuickstartSetupHints(env, detected, err) {
69
+ const hints = [];
70
+ const message = err && err.message ? err.message : String(err || 'Unknown error');
71
+ const hasDb = !!(env.DATABASE_URL || env.AQUIFER_DB_URL || detected.DATABASE_URL);
72
+ const hasEmbed = hasQuickstartEmbedConfig(env)
73
+ || !!detected.EMBED_PROVIDER;
74
+
75
+ if (/Database URL is required/i.test(message)) {
76
+ hints.push('Quickstart could not find a PostgreSQL connection.');
77
+ if (!hasDb) {
78
+ hints.push('If you expect local defaults, make sure PostgreSQL is running on localhost:5432.');
79
+ hints.push('Otherwise set DATABASE_URL or AQUIFER_DB_URL explicitly and run quickstart again.');
80
+ }
81
+ return hints;
82
+ }
83
+
84
+ if (/OPENAI_API_KEY/i.test(message)) {
85
+ hints.push('OpenAI embeddings were selected, but OPENAI_API_KEY is not set.');
86
+ hints.push('Export OPENAI_API_KEY or switch EMBED_PROVIDER back to ollama for local quickstart.');
87
+ return hints;
88
+ }
89
+
90
+ if (!hasDb || !hasEmbed) {
91
+ hints.push('Quickstart is missing part of the local setup.');
92
+ if (!hasDb) hints.push('PostgreSQL was not autodetected and no DATABASE_URL is set.');
93
+ if (!hasEmbed) hints.push('No embedding provider was autodetected and no embed env is set.');
94
+ hints.push('Try `docker compose up -d`, then run `npx aquifer quickstart` again.');
95
+ }
96
+
97
+ hints.push(`Raw error: ${message}`);
98
+ return hints;
99
+ }
100
+
101
+ function buildQuickstartRecallHints(err) {
102
+ const message = err && err.message ? err.message : String(err || 'Unknown error');
103
+
104
+ if (/requires config\.embed\.fn|EMBED_PROVIDER/i.test(message)) {
105
+ return [
106
+ 'Quickstart reached recall, but embeddings are not configured.',
107
+ 'Set EMBED_PROVIDER=ollama for local Ollama, or EMBED_PROVIDER=openai with OPENAI_API_KEY.',
108
+ `Raw error: ${message}`,
109
+ ];
110
+ }
111
+
112
+ if (/OPENAI_API_KEY/i.test(message)) {
113
+ return [
114
+ 'Recall is configured to use OpenAI embeddings, but OPENAI_API_KEY is missing.',
115
+ 'Export OPENAI_API_KEY and rerun quickstart.',
116
+ `Raw error: ${message}`,
117
+ ];
118
+ }
119
+
120
+ if (/ECONNREFUSED|ENOTFOUND|fetch failed|connect/i.test(message)) {
121
+ return [
122
+ 'Aquifer could not reach the embedding service during recall.',
123
+ 'If you expect local Ollama, make sure it is running and the model is available.',
124
+ `Raw error: ${message}`,
125
+ ];
126
+ }
127
+
128
+ return [
129
+ 'Aquifer could not recall the quickstart test session.',
130
+ `Raw error: ${message}`,
131
+ ];
132
+ }
133
+
39
134
  // ---------------------------------------------------------------------------
40
135
  // Argument parser (minimal, no deps)
41
136
  // ---------------------------------------------------------------------------
@@ -43,7 +138,14 @@ function parsePositiveInt(value, fallback) {
43
138
  function parseArgs(argv) {
44
139
  const args = { _: [], flags: {} };
45
140
  // Flags that take a value (not boolean)
46
- const VALUE_FLAGS = new Set(['limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status', 'concurrency', 'entities', 'entity-mode', 'session-id', 'verdict', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars', 'out']);
141
+ const VALUE_FLAGS = new Set([
142
+ 'limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status',
143
+ 'concurrency', 'entities', 'entity-mode', 'mode', 'session-id', 'memory-id', 'canonical-key',
144
+ 'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
145
+ 'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
146
+ 'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
147
+ 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
148
+ ]);
47
149
  for (let i = 0; i < argv.length; i++) {
48
150
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
49
151
  if (argv[i].startsWith('--')) {
@@ -76,13 +178,13 @@ async function cmdRecall(aquifer, args) {
76
178
  process.exit(1);
77
179
  }
78
180
 
79
- const recallOpts = {
80
- limit: parsePositiveInt(args.flags.limit, 5),
81
- agentId: args.flags['agent-id'] || undefined,
82
- source: args.flags.source || undefined,
83
- dateFrom: args.flags['date-from'] || undefined,
84
- dateTo: args.flags['date-to'] || undefined,
85
- };
181
+ const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
182
+ if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
183
+ if (args.flags.source) recallOpts.source = args.flags.source;
184
+ if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
185
+ if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
186
+ if (args.flags['active-scope-key']) recallOpts.activeScopeKey = args.flags['active-scope-key'];
187
+ if (args.flags['active-scope-path']) recallOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
86
188
  if (args.flags.entities) {
87
189
  recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
88
190
  recallOpts.entityMode = args.flags['entity-mode'] || 'any';
@@ -94,26 +196,20 @@ async function cmdRecall(aquifer, args) {
94
196
  return;
95
197
  }
96
198
 
97
- if (results.length === 0) {
98
- console.log('No results found.');
99
- return;
100
- }
101
-
102
- for (let i = 0; i < results.length; i++) {
103
- const r = results[i];
104
- const ss = r.structuredSummary || {};
105
- const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
106
- const date = formatDate(r.startedAt, '?');
107
- console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
108
- if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
109
- if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
110
- console.log();
111
- }
199
+ console.log(formatRecallResults(results, {
200
+ query,
201
+ showScore: true,
202
+ showExplain: !!args.flags.explain,
203
+ }));
112
204
  }
113
205
 
114
206
  async function cmdFeedback(aquifer, args) {
115
207
  const sessionId = args.flags['session-id'] || args._[1];
116
208
  const verdict = args.flags.verdict;
209
+ if (args.flags['memory-id'] || args.flags['canonical-key'] || args.flags['feedback-type']) {
210
+ console.error('Use `aquifer memory-feedback` for curated memory feedback.');
211
+ process.exit(1);
212
+ }
117
213
 
118
214
  if (!sessionId || !verdict) {
119
215
  console.error('Usage: aquifer feedback --session-id ID --verdict helpful|unhelpful [--note TEXT] [--agent-id ID]');
@@ -133,6 +229,82 @@ async function cmdFeedback(aquifer, args) {
133
229
  }
134
230
  }
135
231
 
232
+ async function cmdMemoryFeedback(aquifer, args) {
233
+ const memoryId = args.flags['memory-id'];
234
+ const canonicalKey = args.flags['canonical-key'];
235
+ const feedbackType = args.flags['feedback-type'];
236
+
237
+ if ((!memoryId && !canonicalKey) || !feedbackType) {
238
+ console.error('Usage: aquifer memory-feedback (--memory-id ID | --canonical-key KEY) --feedback-type helpful|confirm|irrelevant|scope_mismatch|stale|incorrect [--note TEXT] [--agent-id ID]');
239
+ process.exit(1);
240
+ }
241
+
242
+ const result = await aquifer.memoryFeedback({
243
+ memoryId: memoryId || undefined,
244
+ canonicalKey: canonicalKey || undefined,
245
+ }, {
246
+ feedbackType,
247
+ agentId: args.flags['agent-id'] || undefined,
248
+ note: args.flags.note || undefined,
249
+ });
250
+
251
+ if (args.flags.json) {
252
+ console.log(JSON.stringify(result, null, 2));
253
+ } else {
254
+ const target = result.canonicalKey || result.memoryId;
255
+ console.log(`Memory feedback: ${result.feedbackType} (${target})`);
256
+ }
257
+ }
258
+
259
+ async function cmdEvidenceRecall(aquifer, args) {
260
+ const query = args._.slice(1).join(' ');
261
+ if (!query) {
262
+ console.error('Usage: aquifer evidence-recall <query> [--limit N] [--agent-id ID] [--allow-unsafe-debug] [--json]');
263
+ process.exit(1);
264
+ }
265
+
266
+ const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
267
+ if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
268
+ if (args.flags.source) recallOpts.source = args.flags.source;
269
+ if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
270
+ if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
271
+ if (args.flags.entities) {
272
+ recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
273
+ recallOpts.entityMode = args.flags['entity-mode'] || 'any';
274
+ }
275
+ if (args.flags.mode) recallOpts.mode = args.flags.mode;
276
+ if (args.flags['allow-unsafe-debug']) recallOpts.allowUnsafeDebug = true;
277
+
278
+ const results = await aquifer.evidenceRecall(query, recallOpts);
279
+
280
+ if (args.flags.json) {
281
+ console.log(JSON.stringify(results, null, 2));
282
+ return;
283
+ }
284
+
285
+ console.log(formatRecallResults(results, {
286
+ query,
287
+ showScore: true,
288
+ showExplain: !!args.flags.explain,
289
+ }));
290
+ }
291
+
292
+ async function cmdFeedbackStats(aquifer, args) {
293
+ const stats = await aquifer.feedbackStats({
294
+ agentId: args.flags['agent-id'] || undefined,
295
+ dateFrom: args.flags['date-from'] || undefined,
296
+ dateTo: args.flags['date-to'] || undefined,
297
+ });
298
+
299
+ if (args.flags.json) {
300
+ console.log(JSON.stringify(stats, null, 2));
301
+ } else {
302
+ console.log(`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`);
303
+ console.log(`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`);
304
+ console.log(`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`);
305
+ }
306
+ }
307
+
136
308
  async function cmdBackfill(aquifer, args) {
137
309
  const limit = parsePositiveInt(args.flags.limit, 100);
138
310
  const dryRun = !!args.flags['dry-run'];
@@ -209,13 +381,39 @@ async function cmdQuickstart(aquifer) {
209
381
  skipSummary: true,
210
382
  skipEntities: true,
211
383
  });
384
+ if (Array.isArray(enrichResult.warnings) && enrichResult.warnings.length > 0) {
385
+ printQuickstartFailure('embedding step returned warnings.', [
386
+ 'Quickstart expects turn embeddings to succeed cleanly.',
387
+ ...enrichResult.warnings.map(w => `Warning: ${w}`),
388
+ ]);
389
+ process.exitCode = 1;
390
+ return;
391
+ }
392
+ if (!Number.isFinite(enrichResult.turnsEmbedded) || enrichResult.turnsEmbedded <= 0) {
393
+ printQuickstartFailure('0 turns were embedded.', [
394
+ 'The quickstart test session contains user turns, so this usually means the embedding setup is not working.',
395
+ 'Check EMBED_PROVIDER / AQUIFER_EMBED_* settings, or make sure Ollama/OpenAI is reachable.',
396
+ ]);
397
+ process.exitCode = 1;
398
+ return;
399
+ }
212
400
  console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded\n`);
213
401
 
214
402
  // 4. Recall
215
403
  console.log('4/5 Recalling "PostgreSQL memory store"...');
216
- const results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
404
+ let results;
405
+ try {
406
+ results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
407
+ } catch (err) {
408
+ printQuickstartFailure('recall step failed.', buildQuickstartRecallHints(err));
409
+ process.exitCode = 1;
410
+ return;
411
+ }
217
412
  if (results.length === 0) {
218
- console.error(' FAIL no results returned. Check your embedding config.');
413
+ printQuickstartFailure('quickstart could not recall its own test session.', [
414
+ 'The write step succeeded, but the test query returned no matches.',
415
+ 'This usually means the embedding path is misconfigured or the embed service is not reachable.',
416
+ ]);
219
417
  process.exitCode = 1;
220
418
  return;
221
419
  }
@@ -253,14 +451,17 @@ async function cmdQuickstart(aquifer) {
253
451
  }
254
452
 
255
453
  async function cmdBootstrap(aquifer, args) {
256
- const result = await aquifer.bootstrap({
257
- agentId: args.flags['agent-id'] || undefined,
258
- source: args.flags.source || undefined,
454
+ const bootstrapOpts = {
259
455
  limit: parsePositiveInt(args.flags.limit, 5),
260
- lookbackDays: parsePositiveInt(args.flags['lookback-days'], 14),
261
456
  maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
262
457
  format: args.flags.json ? 'structured' : 'text',
263
- });
458
+ };
459
+ if (args.flags['agent-id']) bootstrapOpts.agentId = args.flags['agent-id'];
460
+ if (args.flags.source) bootstrapOpts.source = args.flags.source;
461
+ if (args.flags['lookback-days']) bootstrapOpts.lookbackDays = parsePositiveInt(args.flags['lookback-days'], 14);
462
+ if (args.flags['active-scope-key']) bootstrapOpts.activeScopeKey = args.flags['active-scope-key'];
463
+ if (args.flags['active-scope-path']) bootstrapOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
464
+ const result = await aquifer.bootstrap(bootstrapOpts);
264
465
 
265
466
  if (args.flags.json) {
266
467
  console.log(JSON.stringify(result, null, 2));
@@ -276,6 +477,85 @@ async function cmdBootstrap(aquifer, args) {
276
477
  }
277
478
  }
278
479
 
480
+ async function cmdOperator(aquifer, args) {
481
+ const operatorVerb = args._[1] || 'compaction';
482
+ const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
483
+
484
+ if (operatorVerb === 'archive-distill') {
485
+ const inputPath = args.flags.input || args._[2];
486
+ if (!inputPath) {
487
+ console.error('Usage: aquifer operator archive-distill --input /path/to/archive.json [--authority verified_summary] [--json]');
488
+ process.exit(1);
489
+ }
490
+ const archiveSnapshot = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
491
+ const result = await aquifer.memory.consolidation.runJob({
492
+ job: 'archive-distill',
493
+ archiveSnapshot,
494
+ authority: args.flags.authority || undefined,
495
+ scopeKind: args.flags['scope-kind'] || undefined,
496
+ scopeKey: args.flags['scope-key'] || undefined,
497
+ contextKey: args.flags['context-key'] || undefined,
498
+ topicKey: args.flags['topic-key'] || undefined,
499
+ });
500
+
501
+ if (args.flags.json) {
502
+ console.log(JSON.stringify(result, null, 2));
503
+ return;
504
+ }
505
+
506
+ const sessionCount = Array.isArray(archiveSnapshot.sessions) ? archiveSnapshot.sessions.length : 0;
507
+ console.log(`Archive distill planned from ${sessionCount} sessions.`);
508
+ console.log(`Candidates: ${result.candidates.length}`);
509
+ console.log('Visibility: recall/bootstrap hidden until a separate promotion step.');
510
+ return;
511
+ }
512
+
513
+ if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
514
+ console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
515
+ process.exit(1);
516
+ }
517
+
518
+ const cadence = args.flags.cadence
519
+ || (cadenceVerbs.has(operatorVerb) ? operatorVerb : args._[2])
520
+ || 'manual';
521
+ const result = await aquifer.memory.consolidation.runJob({
522
+ job: 'compaction',
523
+ cadence,
524
+ periodStart: args.flags['period-start'] || undefined,
525
+ periodEnd: args.flags['period-end'] || undefined,
526
+ policyVersion: args.flags['policy-version'] || undefined,
527
+ workerId: args.flags['worker-id'] || undefined,
528
+ applyToken: args.flags['apply-token'] || undefined,
529
+ claimLeaseSeconds: args.flags['claim-lease-seconds']
530
+ ? parsePositiveInt(args.flags['claim-lease-seconds'], undefined)
531
+ : undefined,
532
+ snapshotAsOf: args.flags['snapshot-as-of'] || undefined,
533
+ scopeKeys: parseCsvList(args.flags['scope-keys'] || args.flags['scope-key']),
534
+ limit: parsePositiveInt(args.flags.limit, 1000),
535
+ apply: args.flags.apply === true,
536
+ });
537
+
538
+ if (args.flags.json) {
539
+ console.log(JSON.stringify(result, null, 2));
540
+ return;
541
+ }
542
+
543
+ console.log(`${result.dryRun ? 'Planned' : 'Executed'} ${result.cadence} compaction window ${result.periodStart} -> ${result.periodEnd}`);
544
+ console.log(`Snapshot: ${result.snapshotCount} active rows${result.snapshotTruncated ? ' (snapshot limit reached)' : ''}`);
545
+ console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} aggregate candidates`);
546
+ if (result.dryRun) {
547
+ console.log('Mode: dry-run only. Re-run with --apply to write compaction_runs and lifecycle changes.');
548
+ return;
549
+ }
550
+ if (result.run) {
551
+ console.log(`Run: #${result.run.id} status=${result.run.status}`);
552
+ } else if (result.existingRun) {
553
+ console.log(`Existing run: #${result.existingRun.id} status=${result.existingRun.status} reason=${result.skipReason || 'claim_not_acquired'}`);
554
+ } else {
555
+ console.log(`No run claimed: ${result.skipReason || 'claim_not_acquired'}`);
556
+ }
557
+ }
558
+
279
559
  async function cmdExport(aquifer, args) {
280
560
  const output = args.flags.output || null;
281
561
  const limit = parsePositiveInt(args.flags.limit, 1000);
@@ -304,6 +584,10 @@ async function cmdExport(aquifer, args) {
304
584
  }
305
585
  }
306
586
 
587
+ async function cmdCompact(aquifer, args) {
588
+ return cmdOperator(aquifer, { ...args, _: ['operator', 'compaction', ...args._.slice(1)] });
589
+ }
590
+
307
591
  // ---------------------------------------------------------------------------
308
592
  // Main
309
593
  // ---------------------------------------------------------------------------
@@ -314,16 +598,21 @@ async function main() {
314
598
  console.log(`Usage: aquifer <command> [options]
315
599
 
316
600
  Commands:
317
- quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
318
- migrate Run database migrations
601
+ quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
602
+ migrate Run database migrations
319
603
  recall <query> Search sessions (requires embed config)
604
+ evidence-recall <query> Search legacy session/evidence plane explicitly
320
605
  feedback Record trust feedback on a session
606
+ memory-feedback Record curated memory feedback
607
+ feedback-stats Show trust feedback statistics and coverage
321
608
  backfill Enrich pending sessions
609
+ operator ... Run operator-safe consolidation jobs
610
+ compact Plan or apply curated memory compaction
322
611
  stats Show database statistics
323
612
  export Export sessions as JSONL
324
613
  bootstrap Show recent session context (for new session start)
325
- ingest-opencode Import sessions from OpenCode's local SQLite DB
326
- mcp Start MCP server
614
+ ingest-opencode Import sessions from OpenCode's local SQLite DB
615
+ mcp Start MCP server
327
616
 
328
617
  Options:
329
618
  --limit N Limit results
@@ -334,22 +623,43 @@ Options:
334
623
  --entities A,B,C Entity names (comma-separated, recall)
335
624
  --entity-mode any|all Entity match mode (recall, default: any)
336
625
  --session-id ID Session ID (feedback)
626
+ --memory-id ID Curated memory record ID (memory-feedback)
627
+ --canonical-key KEY Active curated memory canonical key (memory-feedback)
337
628
  --verdict helpful|unhelpful Feedback verdict (feedback)
629
+ --feedback-type TYPE Curated memory feedback type
338
630
  --note TEXT Feedback note (feedback)
631
+ --explain Show score breakdown per result (recall)
632
+ --allow-unsafe-debug Allow broad evidence-recall without audit boundary
339
633
  --json JSON output
340
634
  --dry-run Preview only (backfill)
341
635
  --output PATH Output file (export)
342
636
  --config PATH Config file path
343
637
  --lookback-days N How far back in days (bootstrap, default: 14)
344
638
  --max-chars N Max output characters (bootstrap, default: 4000)
639
+ --active-scope-key KEY Active curated memory scope key
640
+ --active-scope-path A,B Ordered curated scope path
641
+ --cadence manual|daily|weekly|monthly
642
+ --period-start ISO Compaction window start
643
+ --period-end ISO Compaction window end
644
+ --apply Apply compaction; default is dry-run
645
+ --scope-key A,B Limit compaction snapshot to specific scope keys
646
+ --snapshot-as-of ISO Read active snapshot as of a specific instant
647
+ --claim-lease-seconds N Override compaction apply lease
648
+ --input PATH Archive distill input JSON path
345
649
  --db PATH OpenCode SQLite path (ingest-opencode)
346
650
  --since YYYY-MM-DD Only ingest sessions after date (ingest-opencode)
347
- --min-messages N Min user messages to ingest (ingest-opencode, default: 3)`);
651
+ --min-messages N Min user messages to ingest (ingest-opencode, default: 3)
652
+
653
+ Operator examples:
654
+ aquifer operator compaction daily --json
655
+ aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
656
+ aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
348
657
  process.exit(0);
349
658
  }
350
659
 
351
660
  const command = argv[0];
352
661
  const args = parseArgs(argv);
662
+ let quickstartDetected = {};
353
663
 
354
664
  // MCP: delegate to mcp.js
355
665
  if (command === 'mcp') {
@@ -383,10 +693,10 @@ Options:
383
693
  // the operator to have set env explicitly.
384
694
  if (command === 'quickstart') {
385
695
  const { autodetectForQuickstart } = require('./shared/autodetect');
386
- const detected = await autodetectForQuickstart(process.env);
387
- if (Object.keys(detected).length > 0) {
696
+ quickstartDetected = await autodetectForQuickstart(process.env);
697
+ if (Object.keys(quickstartDetected).length > 0) {
388
698
  console.log('Autodetected localhost services (env not set):');
389
- for (const [k, v] of Object.entries(detected)) {
699
+ for (const [k, v] of Object.entries(quickstartDetected)) {
390
700
  console.log(` ${k}=${v}`);
391
701
  process.env[k] = v;
392
702
  }
@@ -394,7 +704,17 @@ Options:
394
704
  }
395
705
  }
396
706
 
397
- const aquifer = createAquiferFromConfig(configOverrides);
707
+ let aquifer;
708
+ try {
709
+ aquifer = createAquiferFromConfig(configOverrides);
710
+ } catch (err) {
711
+ if (command === 'quickstart') {
712
+ printQuickstartFailure('setup check failed before quickstart could start.', buildQuickstartSetupHints(process.env, quickstartDetected, err));
713
+ process.exit(1);
714
+ return;
715
+ }
716
+ throw err;
717
+ }
398
718
 
399
719
  try {
400
720
  switch (command) {
@@ -407,12 +727,27 @@ Options:
407
727
  case 'recall':
408
728
  await cmdRecall(aquifer, args);
409
729
  break;
730
+ case 'evidence-recall':
731
+ await cmdEvidenceRecall(aquifer, args);
732
+ break;
410
733
  case 'feedback':
411
734
  await cmdFeedback(aquifer, args);
412
735
  break;
736
+ case 'memory-feedback':
737
+ await cmdMemoryFeedback(aquifer, args);
738
+ break;
739
+ case 'feedback-stats':
740
+ await cmdFeedbackStats(aquifer, args);
741
+ break;
413
742
  case 'backfill':
414
743
  await cmdBackfill(aquifer, args);
415
744
  break;
745
+ case 'operator':
746
+ await cmdOperator(aquifer, args);
747
+ break;
748
+ case 'compact':
749
+ await cmdCompact(aquifer, args);
750
+ break;
416
751
  case 'stats':
417
752
  await cmdStats(aquifer, args);
418
753
  break;