@shadowforge0/aquifer-memory 1.5.12 → 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 (60) hide show
  1. package/.env.example +23 -0
  2. package/README.md +78 -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 +353 -52
  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 +96 -5
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +1 -1
  16. package/consumers/shared/config.js +8 -0
  17. package/consumers/shared/factory.js +1 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +27 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +372 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/mcp-manifest.js +52 -2
  25. package/core/memory-bootstrap.js +188 -0
  26. package/core/memory-consolidation.js +1236 -0
  27. package/core/memory-promotion.js +544 -0
  28. package/core/memory-recall.js +247 -0
  29. package/core/memory-records.js +581 -0
  30. package/core/memory-safety-gate.js +224 -0
  31. package/core/session-finalization.js +350 -0
  32. package/core/storage.js +385 -2
  33. package/docs/getting-started.md +99 -0
  34. package/docs/postprocess-contract.md +2 -2
  35. package/docs/setup.md +51 -2
  36. package/package.json +25 -11
  37. package/pipeline/normalize/adapters/codex.js +106 -0
  38. package/pipeline/normalize/detect.js +3 -2
  39. package/schema/001-base.sql +3 -0
  40. package/schema/007-v1-foundation.sql +273 -0
  41. package/schema/008-session-finalizations.sql +50 -0
  42. package/schema/009-v1-assertion-plane.sql +193 -0
  43. package/schema/010-v1-finalization-review.sql +160 -0
  44. package/schema/011-v1-compaction-claim.sql +46 -0
  45. package/schema/012-v1-compaction-lease.sql +39 -0
  46. package/schema/013-v1-compaction-lineage.sql +193 -0
  47. package/scripts/codex-recovery.js +532 -0
  48. package/consumers/miranda/context-inject.js +0 -120
  49. package/consumers/miranda/daily-entries.js +0 -224
  50. package/consumers/miranda/index.js +0 -364
  51. package/consumers/miranda/instance.js +0 -55
  52. package/consumers/miranda/llm.js +0 -99
  53. package/consumers/miranda/profile.json +0 -145
  54. package/consumers/miranda/prompts/summary.js +0 -303
  55. package/consumers/miranda/recall-format.js +0 -76
  56. package/consumers/miranda/render-daily-md.js +0 -186
  57. package/consumers/miranda/workspace-files.js +0 -91
  58. package/scripts/drop-entity-state-history.sql +0 -17
  59. package/scripts/drop-insights.sql +0 -12
  60. package/scripts/install-openclaw.sh +0 -59
@@ -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,39 +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
- const showExplain = !!args.flags.explain;
103
- for (let i = 0; i < results.length; i++) {
104
- const r = results[i];
105
- const ss = r.structuredSummary || {};
106
- const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
107
- const date = formatDate(r.startedAt, '?');
108
- console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
109
- if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
110
- if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
111
- if (showExplain && r._debug) {
112
- const d = r._debug;
113
- const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
114
- const parts = [
115
- `rrf=${f(d.rrf)}`, `td=${f(d.timeDecay)}`, `access=${f(d.access)}`,
116
- `entity=${f(d.entityScore)}`, `trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
117
- `ol=${f(d.openLoopBoost)}`, `\u2192 hybrid=${f(d.hybridScore)}`,
118
- ];
119
- if (d.rerankApplied) parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
120
- else parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
121
- console.log(` ${parts.join(' ')}`);
122
- }
123
- console.log();
124
- }
199
+ console.log(formatRecallResults(results, {
200
+ query,
201
+ showScore: true,
202
+ showExplain: !!args.flags.explain,
203
+ }));
125
204
  }
126
205
 
127
206
  async function cmdFeedback(aquifer, args) {
128
207
  const sessionId = args.flags['session-id'] || args._[1];
129
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
+ }
130
213
 
131
214
  if (!sessionId || !verdict) {
132
215
  console.error('Usage: aquifer feedback --session-id ID --verdict helpful|unhelpful [--note TEXT] [--agent-id ID]');
@@ -146,6 +229,66 @@ async function cmdFeedback(aquifer, args) {
146
229
  }
147
230
  }
148
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
+
149
292
  async function cmdFeedbackStats(aquifer, args) {
150
293
  const stats = await aquifer.feedbackStats({
151
294
  agentId: args.flags['agent-id'] || undefined,
@@ -238,13 +381,39 @@ async function cmdQuickstart(aquifer) {
238
381
  skipSummary: true,
239
382
  skipEntities: true,
240
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
+ }
241
400
  console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded\n`);
242
401
 
243
402
  // 4. Recall
244
403
  console.log('4/5 Recalling "PostgreSQL memory store"...');
245
- 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
+ }
246
412
  if (results.length === 0) {
247
- 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
+ ]);
248
417
  process.exitCode = 1;
249
418
  return;
250
419
  }
@@ -282,14 +451,17 @@ async function cmdQuickstart(aquifer) {
282
451
  }
283
452
 
284
453
  async function cmdBootstrap(aquifer, args) {
285
- const result = await aquifer.bootstrap({
286
- agentId: args.flags['agent-id'] || undefined,
287
- source: args.flags.source || undefined,
454
+ const bootstrapOpts = {
288
455
  limit: parsePositiveInt(args.flags.limit, 5),
289
- lookbackDays: parsePositiveInt(args.flags['lookback-days'], 14),
290
456
  maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
291
457
  format: args.flags.json ? 'structured' : 'text',
292
- });
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);
293
465
 
294
466
  if (args.flags.json) {
295
467
  console.log(JSON.stringify(result, null, 2));
@@ -305,6 +477,85 @@ async function cmdBootstrap(aquifer, args) {
305
477
  }
306
478
  }
307
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
+
308
559
  async function cmdExport(aquifer, args) {
309
560
  const output = args.flags.output || null;
310
561
  const limit = parsePositiveInt(args.flags.limit, 1000);
@@ -333,6 +584,10 @@ async function cmdExport(aquifer, args) {
333
584
  }
334
585
  }
335
586
 
587
+ async function cmdCompact(aquifer, args) {
588
+ return cmdOperator(aquifer, { ...args, _: ['operator', 'compaction', ...args._.slice(1)] });
589
+ }
590
+
336
591
  // ---------------------------------------------------------------------------
337
592
  // Main
338
593
  // ---------------------------------------------------------------------------
@@ -343,17 +598,21 @@ async function main() {
343
598
  console.log(`Usage: aquifer <command> [options]
344
599
 
345
600
  Commands:
346
- quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
347
- migrate Run database migrations
601
+ quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
602
+ migrate Run database migrations
348
603
  recall <query> Search sessions (requires embed config)
604
+ evidence-recall <query> Search legacy session/evidence plane explicitly
349
605
  feedback Record trust feedback on a session
606
+ memory-feedback Record curated memory feedback
350
607
  feedback-stats Show trust feedback statistics and coverage
351
608
  backfill Enrich pending sessions
609
+ operator ... Run operator-safe consolidation jobs
610
+ compact Plan or apply curated memory compaction
352
611
  stats Show database statistics
353
612
  export Export sessions as JSONL
354
613
  bootstrap Show recent session context (for new session start)
355
- ingest-opencode Import sessions from OpenCode's local SQLite DB
356
- mcp Start MCP server
614
+ ingest-opencode Import sessions from OpenCode's local SQLite DB
615
+ mcp Start MCP server
357
616
 
358
617
  Options:
359
618
  --limit N Limit results
@@ -364,23 +623,43 @@ Options:
364
623
  --entities A,B,C Entity names (comma-separated, recall)
365
624
  --entity-mode any|all Entity match mode (recall, default: any)
366
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)
367
628
  --verdict helpful|unhelpful Feedback verdict (feedback)
629
+ --feedback-type TYPE Curated memory feedback type
368
630
  --note TEXT Feedback note (feedback)
369
631
  --explain Show score breakdown per result (recall)
632
+ --allow-unsafe-debug Allow broad evidence-recall without audit boundary
370
633
  --json JSON output
371
634
  --dry-run Preview only (backfill)
372
635
  --output PATH Output file (export)
373
636
  --config PATH Config file path
374
637
  --lookback-days N How far back in days (bootstrap, default: 14)
375
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
376
649
  --db PATH OpenCode SQLite path (ingest-opencode)
377
650
  --since YYYY-MM-DD Only ingest sessions after date (ingest-opencode)
378
- --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`);
379
657
  process.exit(0);
380
658
  }
381
659
 
382
660
  const command = argv[0];
383
661
  const args = parseArgs(argv);
662
+ let quickstartDetected = {};
384
663
 
385
664
  // MCP: delegate to mcp.js
386
665
  if (command === 'mcp') {
@@ -414,10 +693,10 @@ Options:
414
693
  // the operator to have set env explicitly.
415
694
  if (command === 'quickstart') {
416
695
  const { autodetectForQuickstart } = require('./shared/autodetect');
417
- const detected = await autodetectForQuickstart(process.env);
418
- if (Object.keys(detected).length > 0) {
696
+ quickstartDetected = await autodetectForQuickstart(process.env);
697
+ if (Object.keys(quickstartDetected).length > 0) {
419
698
  console.log('Autodetected localhost services (env not set):');
420
- for (const [k, v] of Object.entries(detected)) {
699
+ for (const [k, v] of Object.entries(quickstartDetected)) {
421
700
  console.log(` ${k}=${v}`);
422
701
  process.env[k] = v;
423
702
  }
@@ -425,7 +704,17 @@ Options:
425
704
  }
426
705
  }
427
706
 
428
- 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
+ }
429
718
 
430
719
  try {
431
720
  switch (command) {
@@ -438,15 +727,27 @@ Options:
438
727
  case 'recall':
439
728
  await cmdRecall(aquifer, args);
440
729
  break;
730
+ case 'evidence-recall':
731
+ await cmdEvidenceRecall(aquifer, args);
732
+ break;
441
733
  case 'feedback':
442
734
  await cmdFeedback(aquifer, args);
443
735
  break;
736
+ case 'memory-feedback':
737
+ await cmdMemoryFeedback(aquifer, args);
738
+ break;
444
739
  case 'feedback-stats':
445
740
  await cmdFeedbackStats(aquifer, args);
446
741
  break;
447
742
  case 'backfill':
448
743
  await cmdBackfill(aquifer, args);
449
744
  break;
745
+ case 'operator':
746
+ await cmdOperator(aquifer, args);
747
+ break;
748
+ case 'compact':
749
+ await cmdCompact(aquifer, args);
750
+ break;
450
751
  case 'stats':
451
752
  await cmdStats(aquifer, args);
452
753
  break;