@shadowforge0/aquifer-memory 1.5.12 → 1.7.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 +84 -73
  3. package/README_CN.md +676 -0
  4. package/README_TW.md +684 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +421 -53
  8. package/consumers/codex-handoff.js +258 -0
  9. package/consumers/codex.js +1676 -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 +380 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/mcp-manifest.js +52 -2
  25. package/core/memory-bootstrap.js +200 -0
  26. package/core/memory-consolidation.js +1590 -0
  27. package/core/memory-promotion.js +544 -0
  28. package/core/memory-recall.js +247 -0
  29. package/core/memory-records.js +797 -0
  30. package/core/memory-safety-gate.js +224 -0
  31. package/core/session-finalization.js +365 -0
  32. package/core/storage.js +385 -2
  33. package/docs/getting-started.md +105 -0
  34. package/docs/postprocess-contract.md +2 -2
  35. package/docs/setup.md +92 -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 +672 -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
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,130 @@ 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 readJsonFlagValue(value, label) {
55
+ if (!value || value === true) {
56
+ throw new Error(`${label} requires a JSON value`);
57
+ }
58
+ try {
59
+ return JSON.parse(String(value));
60
+ } catch (err) {
61
+ throw new Error(`${label} must be valid JSON: ${err.message}`);
62
+ }
63
+ }
64
+
65
+ function readJsonFlagFile(filePath, label) {
66
+ if (!filePath || filePath === true) {
67
+ throw new Error(`${label} requires a file path`);
68
+ }
69
+ try {
70
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
71
+ } catch (err) {
72
+ throw new Error(`${label} must point to valid JSON: ${err.message}`);
73
+ }
74
+ }
75
+
76
+ function readSynthesisSummaryFromFlags(flags = {}) {
77
+ if (flags['synthesis-summary-file']) {
78
+ return readJsonFlagFile(flags['synthesis-summary-file'], '--synthesis-summary-file');
79
+ }
80
+ if (flags['synthesis-summary']) {
81
+ return readJsonFlagValue(flags['synthesis-summary'], '--synthesis-summary');
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ function hasQuickstartEmbedConfig(env) {
87
+ return !!(
88
+ env.EMBED_PROVIDER
89
+ || (env.AQUIFER_EMBED_BASE_URL && env.AQUIFER_EMBED_MODEL)
90
+ );
91
+ }
92
+
93
+ function printQuickstartFailure(title, detailLines = []) {
94
+ console.error(` FAIL — ${title}`);
95
+ for (const line of detailLines) {
96
+ if (line) console.error(` ${line}`);
97
+ }
98
+ }
99
+
100
+ function buildQuickstartSetupHints(env, detected, err) {
101
+ const hints = [];
102
+ const message = err && err.message ? err.message : String(err || 'Unknown error');
103
+ const hasDb = !!(env.DATABASE_URL || env.AQUIFER_DB_URL || detected.DATABASE_URL);
104
+ const hasEmbed = hasQuickstartEmbedConfig(env)
105
+ || !!detected.EMBED_PROVIDER;
106
+
107
+ if (/Database URL is required/i.test(message)) {
108
+ hints.push('Quickstart could not find a PostgreSQL connection.');
109
+ if (!hasDb) {
110
+ hints.push('If you expect local defaults, make sure PostgreSQL is running on localhost:5432.');
111
+ hints.push('Otherwise set DATABASE_URL or AQUIFER_DB_URL explicitly and run quickstart again.');
112
+ }
113
+ return hints;
114
+ }
115
+
116
+ if (/OPENAI_API_KEY/i.test(message)) {
117
+ hints.push('OpenAI embeddings were selected, but OPENAI_API_KEY is not set.');
118
+ hints.push('Export OPENAI_API_KEY or switch EMBED_PROVIDER back to ollama for local quickstart.');
119
+ return hints;
120
+ }
121
+
122
+ if (!hasDb || !hasEmbed) {
123
+ hints.push('Quickstart is missing part of the local setup.');
124
+ if (!hasDb) hints.push('PostgreSQL was not autodetected and no DATABASE_URL is set.');
125
+ if (!hasEmbed) hints.push('No embedding provider was autodetected and no embed env is set.');
126
+ hints.push('Try `docker compose up -d`, then run `npx aquifer quickstart` again.');
127
+ }
128
+
129
+ hints.push(`Raw error: ${message}`);
130
+ return hints;
131
+ }
132
+
133
+ function buildQuickstartRecallHints(err) {
134
+ const message = err && err.message ? err.message : String(err || 'Unknown error');
135
+
136
+ if (/requires config\.embed\.fn|EMBED_PROVIDER/i.test(message)) {
137
+ return [
138
+ 'Quickstart reached recall, but embeddings are not configured.',
139
+ 'Set EMBED_PROVIDER=ollama for local Ollama, or EMBED_PROVIDER=openai with OPENAI_API_KEY.',
140
+ `Raw error: ${message}`,
141
+ ];
142
+ }
143
+
144
+ if (/OPENAI_API_KEY/i.test(message)) {
145
+ return [
146
+ 'Recall is configured to use OpenAI embeddings, but OPENAI_API_KEY is missing.',
147
+ 'Export OPENAI_API_KEY and rerun quickstart.',
148
+ `Raw error: ${message}`,
149
+ ];
150
+ }
151
+
152
+ if (/ECONNREFUSED|ENOTFOUND|fetch failed|connect/i.test(message)) {
153
+ return [
154
+ 'Aquifer could not reach the embedding service during recall.',
155
+ 'If you expect local Ollama, make sure it is running and the model is available.',
156
+ `Raw error: ${message}`,
157
+ ];
158
+ }
159
+
160
+ return [
161
+ 'Aquifer could not recall the quickstart test session.',
162
+ `Raw error: ${message}`,
163
+ ];
164
+ }
165
+
39
166
  // ---------------------------------------------------------------------------
40
167
  // Argument parser (minimal, no deps)
41
168
  // ---------------------------------------------------------------------------
@@ -43,7 +170,15 @@ function parsePositiveInt(value, fallback) {
43
170
  function parseArgs(argv) {
44
171
  const args = { _: [], flags: {} };
45
172
  // 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']);
173
+ const VALUE_FLAGS = new Set([
174
+ 'limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status',
175
+ 'concurrency', 'entities', 'entity-mode', 'mode', 'session-id', 'memory-id', 'canonical-key',
176
+ 'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
177
+ 'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
178
+ 'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
179
+ 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
180
+ 'synthesis-summary', 'synthesis-summary-file',
181
+ ]);
47
182
  for (let i = 0; i < argv.length; i++) {
48
183
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
49
184
  if (argv[i].startsWith('--')) {
@@ -76,13 +211,13 @@ async function cmdRecall(aquifer, args) {
76
211
  process.exit(1);
77
212
  }
78
213
 
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
- };
214
+ const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
215
+ if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
216
+ if (args.flags.source) recallOpts.source = args.flags.source;
217
+ if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
218
+ if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
219
+ if (args.flags['active-scope-key']) recallOpts.activeScopeKey = args.flags['active-scope-key'];
220
+ if (args.flags['active-scope-path']) recallOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
86
221
  if (args.flags.entities) {
87
222
  recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
88
223
  recallOpts.entityMode = args.flags['entity-mode'] || 'any';
@@ -94,39 +229,20 @@ async function cmdRecall(aquifer, args) {
94
229
  return;
95
230
  }
96
231
 
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
- }
232
+ console.log(formatRecallResults(results, {
233
+ query,
234
+ showScore: true,
235
+ showExplain: !!args.flags.explain,
236
+ }));
125
237
  }
126
238
 
127
239
  async function cmdFeedback(aquifer, args) {
128
240
  const sessionId = args.flags['session-id'] || args._[1];
129
241
  const verdict = args.flags.verdict;
242
+ if (args.flags['memory-id'] || args.flags['canonical-key'] || args.flags['feedback-type']) {
243
+ console.error('Use `aquifer memory-feedback` for curated memory feedback.');
244
+ process.exit(1);
245
+ }
130
246
 
131
247
  if (!sessionId || !verdict) {
132
248
  console.error('Usage: aquifer feedback --session-id ID --verdict helpful|unhelpful [--note TEXT] [--agent-id ID]');
@@ -146,6 +262,66 @@ async function cmdFeedback(aquifer, args) {
146
262
  }
147
263
  }
148
264
 
265
+ async function cmdMemoryFeedback(aquifer, args) {
266
+ const memoryId = args.flags['memory-id'];
267
+ const canonicalKey = args.flags['canonical-key'];
268
+ const feedbackType = args.flags['feedback-type'];
269
+
270
+ if ((!memoryId && !canonicalKey) || !feedbackType) {
271
+ 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]');
272
+ process.exit(1);
273
+ }
274
+
275
+ const result = await aquifer.memoryFeedback({
276
+ memoryId: memoryId || undefined,
277
+ canonicalKey: canonicalKey || undefined,
278
+ }, {
279
+ feedbackType,
280
+ agentId: args.flags['agent-id'] || undefined,
281
+ note: args.flags.note || undefined,
282
+ });
283
+
284
+ if (args.flags.json) {
285
+ console.log(JSON.stringify(result, null, 2));
286
+ } else {
287
+ const target = result.canonicalKey || result.memoryId;
288
+ console.log(`Memory feedback: ${result.feedbackType} (${target})`);
289
+ }
290
+ }
291
+
292
+ async function cmdEvidenceRecall(aquifer, args) {
293
+ const query = args._.slice(1).join(' ');
294
+ if (!query) {
295
+ console.error('Usage: aquifer evidence-recall <query> [--limit N] [--agent-id ID] [--allow-unsafe-debug] [--json]');
296
+ process.exit(1);
297
+ }
298
+
299
+ const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
300
+ if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
301
+ if (args.flags.source) recallOpts.source = args.flags.source;
302
+ if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
303
+ if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
304
+ if (args.flags.entities) {
305
+ recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
306
+ recallOpts.entityMode = args.flags['entity-mode'] || 'any';
307
+ }
308
+ if (args.flags.mode) recallOpts.mode = args.flags.mode;
309
+ if (args.flags['allow-unsafe-debug']) recallOpts.allowUnsafeDebug = true;
310
+
311
+ const results = await aquifer.evidenceRecall(query, recallOpts);
312
+
313
+ if (args.flags.json) {
314
+ console.log(JSON.stringify(results, null, 2));
315
+ return;
316
+ }
317
+
318
+ console.log(formatRecallResults(results, {
319
+ query,
320
+ showScore: true,
321
+ showExplain: !!args.flags.explain,
322
+ }));
323
+ }
324
+
149
325
  async function cmdFeedbackStats(aquifer, args) {
150
326
  const stats = await aquifer.feedbackStats({
151
327
  agentId: args.flags['agent-id'] || undefined,
@@ -238,13 +414,39 @@ async function cmdQuickstart(aquifer) {
238
414
  skipSummary: true,
239
415
  skipEntities: true,
240
416
  });
417
+ if (Array.isArray(enrichResult.warnings) && enrichResult.warnings.length > 0) {
418
+ printQuickstartFailure('embedding step returned warnings.', [
419
+ 'Quickstart expects turn embeddings to succeed cleanly.',
420
+ ...enrichResult.warnings.map(w => `Warning: ${w}`),
421
+ ]);
422
+ process.exitCode = 1;
423
+ return;
424
+ }
425
+ if (!Number.isFinite(enrichResult.turnsEmbedded) || enrichResult.turnsEmbedded <= 0) {
426
+ printQuickstartFailure('0 turns were embedded.', [
427
+ 'The quickstart test session contains user turns, so this usually means the embedding setup is not working.',
428
+ 'Check EMBED_PROVIDER / AQUIFER_EMBED_* settings, or make sure Ollama/OpenAI is reachable.',
429
+ ]);
430
+ process.exitCode = 1;
431
+ return;
432
+ }
241
433
  console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded\n`);
242
434
 
243
435
  // 4. Recall
244
436
  console.log('4/5 Recalling "PostgreSQL memory store"...');
245
- const results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
437
+ let results;
438
+ try {
439
+ results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
440
+ } catch (err) {
441
+ printQuickstartFailure('recall step failed.', buildQuickstartRecallHints(err));
442
+ process.exitCode = 1;
443
+ return;
444
+ }
246
445
  if (results.length === 0) {
247
- console.error(' FAIL no results returned. Check your embedding config.');
446
+ printQuickstartFailure('quickstart could not recall its own test session.', [
447
+ 'The write step succeeded, but the test query returned no matches.',
448
+ 'This usually means the embedding path is misconfigured or the embed service is not reachable.',
449
+ ]);
248
450
  process.exitCode = 1;
249
451
  return;
250
452
  }
@@ -282,14 +484,17 @@ async function cmdQuickstart(aquifer) {
282
484
  }
283
485
 
284
486
  async function cmdBootstrap(aquifer, args) {
285
- const result = await aquifer.bootstrap({
286
- agentId: args.flags['agent-id'] || undefined,
287
- source: args.flags.source || undefined,
487
+ const bootstrapOpts = {
288
488
  limit: parsePositiveInt(args.flags.limit, 5),
289
- lookbackDays: parsePositiveInt(args.flags['lookback-days'], 14),
290
489
  maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
291
490
  format: args.flags.json ? 'structured' : 'text',
292
- });
491
+ };
492
+ if (args.flags['agent-id']) bootstrapOpts.agentId = args.flags['agent-id'];
493
+ if (args.flags.source) bootstrapOpts.source = args.flags.source;
494
+ if (args.flags['lookback-days']) bootstrapOpts.lookbackDays = parsePositiveInt(args.flags['lookback-days'], 14);
495
+ if (args.flags['active-scope-key']) bootstrapOpts.activeScopeKey = args.flags['active-scope-key'];
496
+ if (args.flags['active-scope-path']) bootstrapOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
497
+ const result = await aquifer.bootstrap(bootstrapOpts);
293
498
 
294
499
  if (args.flags.json) {
295
500
  console.log(JSON.stringify(result, null, 2));
@@ -305,6 +510,102 @@ async function cmdBootstrap(aquifer, args) {
305
510
  }
306
511
  }
307
512
 
513
+ async function cmdOperator(aquifer, args) {
514
+ const operatorVerb = args._[1] || 'compaction';
515
+ const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
516
+
517
+ if (operatorVerb === 'archive-distill') {
518
+ const inputPath = args.flags.input || args._[2];
519
+ if (!inputPath) {
520
+ console.error('Usage: aquifer operator archive-distill --input /path/to/archive.json [--authority verified_summary] [--json]');
521
+ process.exit(1);
522
+ }
523
+ const archiveSnapshot = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
524
+ const result = await aquifer.memory.consolidation.runJob({
525
+ job: 'archive-distill',
526
+ archiveSnapshot,
527
+ authority: args.flags.authority || undefined,
528
+ scopeKind: args.flags['scope-kind'] || undefined,
529
+ scopeKey: args.flags['scope-key'] || undefined,
530
+ contextKey: args.flags['context-key'] || undefined,
531
+ topicKey: args.flags['topic-key'] || undefined,
532
+ });
533
+
534
+ if (args.flags.json) {
535
+ console.log(JSON.stringify(result, null, 2));
536
+ return;
537
+ }
538
+
539
+ const sessionCount = Array.isArray(archiveSnapshot.sessions) ? archiveSnapshot.sessions.length : 0;
540
+ console.log(`Archive distill planned from ${sessionCount} sessions.`);
541
+ console.log(`Candidates: ${result.candidates.length}`);
542
+ console.log('Visibility: recall/bootstrap hidden until a separate promotion step.');
543
+ return;
544
+ }
545
+
546
+ if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
547
+ console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
548
+ process.exit(1);
549
+ }
550
+
551
+ const cadence = args.flags.cadence
552
+ || (cadenceVerbs.has(operatorVerb) ? operatorVerb : args._[2])
553
+ || 'manual';
554
+ const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
555
+ const result = await aquifer.memory.consolidation.runJob({
556
+ job: 'compaction',
557
+ cadence,
558
+ periodStart: args.flags['period-start'] || undefined,
559
+ periodEnd: args.flags['period-end'] || undefined,
560
+ policyVersion: args.flags['policy-version'] || undefined,
561
+ workerId: args.flags['worker-id'] || undefined,
562
+ applyToken: args.flags['apply-token'] || undefined,
563
+ claimLeaseSeconds: args.flags['claim-lease-seconds']
564
+ ? parsePositiveInt(args.flags['claim-lease-seconds'], undefined)
565
+ : undefined,
566
+ snapshotAsOf: args.flags['snapshot-as-of'] || undefined,
567
+ scopeKeys: parseCsvList(args.flags['scope-keys'] || args.flags['scope-key']),
568
+ scopeKind: args.flags['scope-kind'] || undefined,
569
+ scopeKey: args.flags['scope-key'] || undefined,
570
+ contextKey: args.flags['context-key'] || undefined,
571
+ topicKey: args.flags['topic-key'] || undefined,
572
+ activeScopeKey: args.flags['active-scope-key'] || undefined,
573
+ activeScopePath: parseScopePath(args.flags['active-scope-path']),
574
+ limit: parsePositiveInt(args.flags.limit, 1000),
575
+ apply: args.flags.apply === true,
576
+ promoteCandidates: args.flags['promote-candidates'] === true,
577
+ includeSynthesisPrompt: args.flags['include-synthesis-prompt'] === true,
578
+ synthesisSummary,
579
+ });
580
+
581
+ if (args.flags.json) {
582
+ console.log(JSON.stringify(result, null, 2));
583
+ return;
584
+ }
585
+
586
+ console.log(`${result.dryRun ? 'Planned' : 'Executed'} ${result.cadence} compaction window ${result.periodStart} -> ${result.periodEnd}`);
587
+ console.log(`Snapshot: ${result.snapshotCount} active rows${result.snapshotTruncated ? ' (snapshot limit reached)' : ''}`);
588
+ console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} candidates`);
589
+ if (result.synthesisPrompt) {
590
+ console.log('\nSynthesis prompt:\n');
591
+ console.log(result.synthesisPrompt);
592
+ }
593
+ if (result.promotionReview) {
594
+ console.log(`\n${result.promotionReview}`);
595
+ }
596
+ if (result.dryRun) {
597
+ console.log('Mode: dry-run only. Re-run with --apply to write compaction_runs and lifecycle changes.');
598
+ return;
599
+ }
600
+ if (result.run) {
601
+ console.log(`Run: #${result.run.id} status=${result.run.status}`);
602
+ } else if (result.existingRun) {
603
+ console.log(`Existing run: #${result.existingRun.id} status=${result.existingRun.status} reason=${result.skipReason || 'claim_not_acquired'}`);
604
+ } else {
605
+ console.log(`No run claimed: ${result.skipReason || 'claim_not_acquired'}`);
606
+ }
607
+ }
608
+
308
609
  async function cmdExport(aquifer, args) {
309
610
  const output = args.flags.output || null;
310
611
  const limit = parsePositiveInt(args.flags.limit, 1000);
@@ -333,6 +634,10 @@ async function cmdExport(aquifer, args) {
333
634
  }
334
635
  }
335
636
 
637
+ async function cmdCompact(aquifer, args) {
638
+ return cmdOperator(aquifer, { ...args, _: ['operator', 'compaction', ...args._.slice(1)] });
639
+ }
640
+
336
641
  // ---------------------------------------------------------------------------
337
642
  // Main
338
643
  // ---------------------------------------------------------------------------
@@ -343,17 +648,22 @@ async function main() {
343
648
  console.log(`Usage: aquifer <command> [options]
344
649
 
345
650
  Commands:
346
- quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
347
- migrate Run database migrations
651
+ quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
652
+ migrate Run database migrations
348
653
  recall <query> Search sessions (requires embed config)
654
+ evidence-recall <query> Search legacy session/evidence plane explicitly
349
655
  feedback Record trust feedback on a session
656
+ memory-feedback Record curated memory feedback
350
657
  feedback-stats Show trust feedback statistics and coverage
351
658
  backfill Enrich pending sessions
659
+ operator ... Run operator-safe consolidation jobs
660
+ compact Plan or apply curated memory compaction
352
661
  stats Show database statistics
353
662
  export Export sessions as JSONL
354
663
  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
664
+ codex-recovery ... Inspect or run Codex SessionStart recovery flow
665
+ ingest-opencode Import sessions from OpenCode's local SQLite DB
666
+ mcp Start MCP server
357
667
 
358
668
  Options:
359
669
  --limit N Limit results
@@ -364,23 +674,55 @@ Options:
364
674
  --entities A,B,C Entity names (comma-separated, recall)
365
675
  --entity-mode any|all Entity match mode (recall, default: any)
366
676
  --session-id ID Session ID (feedback)
677
+ --memory-id ID Curated memory record ID (memory-feedback)
678
+ --canonical-key KEY Active curated memory canonical key (memory-feedback)
367
679
  --verdict helpful|unhelpful Feedback verdict (feedback)
680
+ --feedback-type TYPE Curated memory feedback type
368
681
  --note TEXT Feedback note (feedback)
369
682
  --explain Show score breakdown per result (recall)
683
+ --allow-unsafe-debug Allow broad evidence-recall without audit boundary
370
684
  --json JSON output
371
685
  --dry-run Preview only (backfill)
372
686
  --output PATH Output file (export)
373
687
  --config PATH Config file path
374
688
  --lookback-days N How far back in days (bootstrap, default: 14)
375
689
  --max-chars N Max output characters (bootstrap, default: 4000)
690
+ --active-scope-key KEY Active curated memory scope key
691
+ --active-scope-path A,B Ordered curated scope path
692
+ --cadence manual|daily|weekly|monthly
693
+ --period-start ISO Compaction window start
694
+ --period-end ISO Compaction window end
695
+ --apply Apply compaction; default is dry-run
696
+ --promote-candidates Promote compaction/synthesis candidates when applying
697
+ --include-synthesis-prompt Include timer synthesis prompt in operator output
698
+ --synthesis-summary JSON Timer synthesis summary JSON to attach to a compaction plan
699
+ --synthesis-summary-file P Read timer synthesis summary JSON from file
700
+ --scope-key A,B Limit compaction snapshot to specific scope keys
701
+ --scope-kind KIND Explicit synthesis target scope kind
702
+ --snapshot-as-of ISO Read active snapshot as of a specific instant
703
+ --claim-lease-seconds N Override compaction apply lease
704
+ --input PATH Archive distill input JSON path
376
705
  --db PATH OpenCode SQLite path (ingest-opencode)
377
706
  --since YYYY-MM-DD Only ingest sessions after date (ingest-opencode)
378
- --min-messages N Min user messages to ingest (ingest-opencode, default: 3)`);
707
+ --min-messages N Min user messages to ingest (ingest-opencode, default: 3)
708
+
709
+ Operator examples:
710
+ aquifer operator compaction daily --json
711
+ aquifer operator compaction daily --include-synthesis-prompt --json
712
+ aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
713
+ aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json
714
+ aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
379
715
  process.exit(0);
380
716
  }
381
717
 
382
718
  const command = argv[0];
383
719
  const args = parseArgs(argv);
720
+ let quickstartDetected = {};
721
+
722
+ if (command === 'codex-recovery') {
723
+ await require('../scripts/codex-recovery').main(argv.slice(1));
724
+ return;
725
+ }
384
726
 
385
727
  // MCP: delegate to mcp.js
386
728
  if (command === 'mcp') {
@@ -414,10 +756,10 @@ Options:
414
756
  // the operator to have set env explicitly.
415
757
  if (command === 'quickstart') {
416
758
  const { autodetectForQuickstart } = require('./shared/autodetect');
417
- const detected = await autodetectForQuickstart(process.env);
418
- if (Object.keys(detected).length > 0) {
759
+ quickstartDetected = await autodetectForQuickstart(process.env);
760
+ if (Object.keys(quickstartDetected).length > 0) {
419
761
  console.log('Autodetected localhost services (env not set):');
420
- for (const [k, v] of Object.entries(detected)) {
762
+ for (const [k, v] of Object.entries(quickstartDetected)) {
421
763
  console.log(` ${k}=${v}`);
422
764
  process.env[k] = v;
423
765
  }
@@ -425,7 +767,17 @@ Options:
425
767
  }
426
768
  }
427
769
 
428
- const aquifer = createAquiferFromConfig(configOverrides);
770
+ let aquifer;
771
+ try {
772
+ aquifer = createAquiferFromConfig(configOverrides);
773
+ } catch (err) {
774
+ if (command === 'quickstart') {
775
+ printQuickstartFailure('setup check failed before quickstart could start.', buildQuickstartSetupHints(process.env, quickstartDetected, err));
776
+ process.exit(1);
777
+ return;
778
+ }
779
+ throw err;
780
+ }
429
781
 
430
782
  try {
431
783
  switch (command) {
@@ -438,15 +790,27 @@ Options:
438
790
  case 'recall':
439
791
  await cmdRecall(aquifer, args);
440
792
  break;
793
+ case 'evidence-recall':
794
+ await cmdEvidenceRecall(aquifer, args);
795
+ break;
441
796
  case 'feedback':
442
797
  await cmdFeedback(aquifer, args);
443
798
  break;
799
+ case 'memory-feedback':
800
+ await cmdMemoryFeedback(aquifer, args);
801
+ break;
444
802
  case 'feedback-stats':
445
803
  await cmdFeedbackStats(aquifer, args);
446
804
  break;
447
805
  case 'backfill':
448
806
  await cmdBackfill(aquifer, args);
449
807
  break;
808
+ case 'operator':
809
+ await cmdOperator(aquifer, args);
810
+ break;
811
+ case 'compact':
812
+ await cmdCompact(aquifer, args);
813
+ break;
450
814
  case 'stats':
451
815
  await cmdStats(aquifer, args);
452
816
  break;
@@ -471,7 +835,11 @@ Options:
471
835
  }
472
836
 
473
837
  // Export for testing; execute only when run directly
474
- module.exports = { parseArgs };
838
+ module.exports = {
839
+ parseArgs,
840
+ cmdOperator,
841
+ readSynthesisSummaryFromFlags,
842
+ };
475
843
 
476
844
  if (require.main === module) {
477
845
  main().catch(err => {