@shadowforge0/aquifer-memory 1.8.0 → 1.9.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.
package/consumers/cli.js CHANGED
@@ -11,6 +11,8 @@
11
11
  * aquifer backfill [options] Enrich pending sessions
12
12
  * aquifer stats [options] Show database statistics
13
13
  * aquifer backend-info [--json] Show selected backend capabilities
14
+ * aquifer install-openclaw Install/update OpenClaw MCP + extension wiring
15
+ * aquifer finalization ... Inspect read-only finalization ledger
14
16
  * aquifer export [options] Export sessions
15
17
  * aquifer operator ... Run operator-safe consolidation jobs
16
18
  * aquifer mcp Start MCP server
@@ -21,6 +23,7 @@ const { createAquiferFromConfig } = require('./shared/factory');
21
23
  const { loadConfig } = require('./shared/config');
22
24
  const { formatRecallResults } = require('./shared/recall-format');
23
25
  const { backendCapabilities } = require('../core/backends/capabilities');
26
+ const { summarizeFinalizationListRow } = require('../core/finalization-inspector');
24
27
 
25
28
  function formatDate(value, fallback) {
26
29
  if (!value) return fallback;
@@ -183,6 +186,9 @@ function parseArgs(argv) {
183
186
  'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
184
187
  'scope-id', 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
185
188
  'synthesis-summary', 'synthesis-summary-file', 'min-finalizations', 'checkpoint-key',
189
+ 'openclaw-home', 'id', 'transcript-hash', 'phase', 'host', 'run-id', 'kind', 'query',
190
+ 'memory-type', 'visibility', 'as-of', 'resolution', 'reason', 'actor-kind', 'actor-id',
191
+ 'defer-until', 'expected-latest-issue-feedback-id', 'expected-latest-issue-feedback-at',
186
192
  ]);
187
193
  for (let i = 0; i < argv.length; i++) {
188
194
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
@@ -204,7 +210,29 @@ function parseArgs(argv) {
204
210
  // Commands
205
211
  // ---------------------------------------------------------------------------
206
212
 
207
- async function cmdMigrate(aquifer) {
213
+ async function cmdMigrate(aquifer, args = { flags: {} }) {
214
+ if (args.flags && args.flags.json) {
215
+ const notices = [];
216
+ const originalStderrWrite = process.stderr.write;
217
+ process.stderr.write = function writeCapturedStderr(chunk, encoding, callback) {
218
+ notices.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
219
+ if (typeof encoding === 'function') encoding();
220
+ if (typeof callback === 'function') callback();
221
+ return true;
222
+ };
223
+ try {
224
+ await aquifer.migrate();
225
+ } finally {
226
+ process.stderr.write = originalStderrWrite;
227
+ }
228
+ console.log(JSON.stringify({
229
+ ok: true,
230
+ migrated: true,
231
+ notices: notices.join('').split(/\r?\n/).map(line => line.trim()).filter(Boolean),
232
+ }, null, 2));
233
+ return;
234
+ }
235
+
208
236
  await aquifer.migrate();
209
237
  console.log('Migrations applied successfully.');
210
238
  }
@@ -390,6 +418,9 @@ async function cmdStats(aquifer, args) {
390
418
  console.log(`Serving mode: ${stats.serving?.mode || 'legacy'}`);
391
419
  console.log(`Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`);
392
420
  console.log(`Sessions: ${stats.sessionTotal} (${Object.entries(stats.sessions).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
421
+ if (stats.pendingSessions?.available) {
422
+ console.log(`Actionable pending/failed: ${stats.pendingSessions.total}`);
423
+ }
393
424
  console.log(`Summaries: ${stats.summaries}`);
394
425
  console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
395
426
  console.log(`Entities: ${stats.entities}`);
@@ -451,6 +482,149 @@ async function cmdBackendInfo(args) {
451
482
  }
452
483
  }
453
484
 
485
+ function formatDoctor(result = {}) {
486
+ const checks = Array.isArray(result.checks) ? result.checks : [];
487
+ if (checks.length === 0) return `Doctor: ${result.status || 'unknown'}`;
488
+ return checks.map(check => {
489
+ const next = check.nextAction ? ` next=${check.nextAction}` : '';
490
+ return `${check.status || 'unknown'} ${check.id || 'check'}: ${check.summary || ''}${next}`;
491
+ }).join('\n');
492
+ }
493
+
494
+ async function cmdDoctor(aquifer, args) {
495
+ const result = await aquifer.doctor.run({
496
+ host: args.flags.host || undefined,
497
+ openclawHome: args.flags['openclaw-home'] || undefined,
498
+ limit: parsePositiveInt(args.flags.limit, 20),
499
+ });
500
+ if (args.flags.json) {
501
+ console.log(JSON.stringify(result, null, 2));
502
+ return;
503
+ }
504
+ console.log(formatDoctor(result));
505
+ }
506
+
507
+ function formatTimestamp(value) {
508
+ if (!value) return '?';
509
+ const parsed = new Date(value);
510
+ return isNaN(parsed.getTime()) ? String(value) : parsed.toISOString();
511
+ }
512
+
513
+ function shortHash(value) {
514
+ if (!value) return '?';
515
+ return String(value).slice(0, 12);
516
+ }
517
+
518
+ function formatFinalizationList(rows = []) {
519
+ if (!rows || rows.length === 0) return 'No finalization rows found.';
520
+ return rows.map(row => {
521
+ const item = summarizeFinalizationListRow(row);
522
+ const session = `${item.source}/${item.agentId}/${item.sessionId}`;
523
+ const phase = item.phase ? ` phase=${item.phase}` : '';
524
+ const mode = item.mode ? ` mode=${item.mode}` : '';
525
+ const hash = item.transcriptHashPrefix ? ` hash=${item.transcriptHashPrefix}` : '';
526
+ return `#${item.id} ${item.status} ${session}${phase}${mode}${hash} updated=${formatTimestamp(item.updatedAt)}`;
527
+ }).join('\n');
528
+ }
529
+
530
+ function formatFinalizationInspect(report = {}) {
531
+ const item = report.finalization || {};
532
+ const candidateSummary = report.candidateSummary || {};
533
+ const lineage = report.lineage || {};
534
+ const envelope = report.candidateEnvelope || {};
535
+ const memoryResult = report.memoryResult || {};
536
+ const lines = [
537
+ `Finalization #${item.id || '?'} ${item.status || 'unknown'}`,
538
+ `Session: ${item.source || '?'}/${item.agentId || '?'}/${item.sessionId || '?'} phase=${item.phase || '?'} mode=${item.mode || '?'} host=${item.host || '?'}`,
539
+ `Transcript hash: ${item.transcriptHashPrefix || shortHash(item.transcriptHash)}`,
540
+ `Updated: ${formatTimestamp(item.updatedAt)} finalized=${formatTimestamp(item.finalizedAt)}`,
541
+ ];
542
+
543
+ if (item.finalizerModel) lines.push(`Finalizer: ${item.finalizerModel}`);
544
+ if (item.scopeKind || item.scopeKey || item.contextKey || item.topicKey) {
545
+ lines.push(`Scope: kind=${item.scopeKind || '?'} key=${item.scopeKey || '?'} context=${item.contextKey || '?'} topic=${item.topicKey || '?'}`);
546
+ }
547
+ if (item.error) lines.push(`Error: ${item.error}`);
548
+
549
+ lines.push(`Memory result: candidates=${memoryResult.candidates || 0} promoted=${memoryResult.promoted || 0} quarantined=${memoryResult.quarantined || 0} skipped=${memoryResult.skipped || 0}`);
550
+ lines.push(`Candidate envelope: version=${envelope.version || '?'} hash=${shortHash(envelope.hash)} candidates=${envelope.candidateCount || 0}`);
551
+ lines.push(`Candidate actions: total=${candidateSummary.total || 0} promote=${candidateSummary.promote || 0} quarantine=${candidateSummary.quarantine || 0} skip=${candidateSummary.skip || 0} supersede=${candidateSummary.supersede || 0} error=${candidateSummary.error || 0}`);
552
+ lines.push(`Lineage: memories=${(lineage.memoryRecordIds || []).join(',') || 'none'} facts=${(lineage.factAssertionIds || []).join(',') || 'none'} evidenceRefs=${lineage.evidenceRefCount || 0} evidenceItems=${lineage.evidenceItemCount || 0}`);
553
+
554
+ if (report.review?.humanReviewText) {
555
+ lines.push('\nHuman review:\n' + report.review.humanReviewText);
556
+ }
557
+ if (report.review?.sessionStartText) {
558
+ lines.push('\nSessionStart:\n' + report.review.sessionStartText);
559
+ }
560
+ if (Array.isArray(report.candidates) && report.candidates.length > 0) {
561
+ lines.push('\nCandidates:');
562
+ for (const candidate of report.candidates) {
563
+ lines.push(` [${candidate.index}] ${candidate.action}${candidate.reason ? `/${candidate.reason}` : ''} ${candidate.memoryType || '?'} ${candidate.canonicalKey || '?'}${candidate.memoryRecordId ? ` memory=${candidate.memoryRecordId}` : ''}`);
564
+ }
565
+ }
566
+ return lines.join('\n');
567
+ }
568
+
569
+ async function cmdFinalization(aquifer, args) {
570
+ const verb = args._[1] || 'list';
571
+
572
+ if (verb === 'list') {
573
+ const rows = await aquifer.finalization.list({
574
+ status: args.flags.status || undefined,
575
+ agentId: args.flags['agent-id'] || undefined,
576
+ source: args.flags.source || undefined,
577
+ host: args.flags.host || undefined,
578
+ sessionId: args.flags['session-id'] || undefined,
579
+ transcriptHash: args.flags['transcript-hash'] || undefined,
580
+ phase: args.flags.phase || undefined,
581
+ mode: args.flags.mode || undefined,
582
+ limit: parsePositiveInt(args.flags.limit, 50),
583
+ });
584
+
585
+ if (args.flags.json) {
586
+ console.log(JSON.stringify({
587
+ readOnly: true,
588
+ finalizations: rows.map(summarizeFinalizationListRow),
589
+ }, null, 2));
590
+ return;
591
+ }
592
+ console.log(formatFinalizationList(rows));
593
+ return;
594
+ }
595
+
596
+ if (verb === 'inspect') {
597
+ const id = args.flags.id || args._[2] || undefined;
598
+ const sessionId = args.flags['session-id'];
599
+ if (id && (sessionId || args.flags['agent-id'] || args.flags.source || args.flags['transcript-hash'])) {
600
+ console.error('Usage: --id is mutually exclusive with --session-id/--agent-id/--source/--transcript-hash.');
601
+ process.exit(1);
602
+ }
603
+ if (!id && (!sessionId || !args.flags['agent-id'] || !args.flags.source)) {
604
+ console.error('Usage: aquifer finalization inspect (--id ID | --session-id ID --agent-id ID --source SOURCE [--transcript-hash HASH]) [--json]');
605
+ process.exit(1);
606
+ }
607
+ const report = await aquifer.finalization.inspect({
608
+ id,
609
+ sessionId: sessionId || undefined,
610
+ agentId: args.flags['agent-id'] || undefined,
611
+ source: args.flags.source || undefined,
612
+ transcriptHash: args.flags['transcript-hash'] || undefined,
613
+ phase: args.flags.phase || undefined,
614
+ });
615
+
616
+ if (args.flags.json) {
617
+ console.log(JSON.stringify(report, null, 2));
618
+ return;
619
+ }
620
+ console.log(formatFinalizationInspect(report));
621
+ return;
622
+ }
623
+
624
+ console.error('Usage: aquifer finalization <list|inspect> [options]');
625
+ process.exit(1);
626
+ }
627
+
454
628
  async function cmdLocalQuickstart(aquifer) {
455
629
  const cfg = aquifer.getConfig();
456
630
  console.log('Aquifer quickstart — verifying local starter backend.\n');
@@ -622,10 +796,319 @@ async function cmdBootstrap(aquifer, args) {
622
796
  }
623
797
  }
624
798
 
799
+ function formatExplain(result = {}) {
800
+ const selected = Array.isArray(result.selected) ? result.selected : [];
801
+ const excluded = Array.isArray(result.excluded) ? result.excluded : [];
802
+ const lines = [
803
+ `Explain ${result.lane || 'memory'}: selected=${selected.length} excluded=${excluded.length}`,
804
+ ];
805
+ if (selected.length > 0) {
806
+ lines.push('Selected:');
807
+ for (const row of selected) {
808
+ lines.push(` ${row.memoryId || row.id || '?'} ${row.canonicalKey || '?'} reason=${row.reason || 'selected'}`);
809
+ }
810
+ }
811
+ if (excluded.length > 0) {
812
+ lines.push('Excluded:');
813
+ for (const row of excluded) {
814
+ lines.push(` ${row.memoryId || row.id || '?'} ${row.canonicalKey || '?'} reason=${row.reason || 'unknown'}`);
815
+ }
816
+ }
817
+ if (result.budget) {
818
+ lines.push(`Budget: limit=${result.budget.limit || '?'} maxChars=${result.budget.maxChars || '?'} overflow=${result.budget.overflow === true}`);
819
+ }
820
+ return lines.join('\n');
821
+ }
822
+
823
+ function formatFeedbackCounts(counts = {}) {
824
+ const entries = Object.entries(counts).filter(([, count]) => count > 0);
825
+ if (entries.length === 0) return 'none';
826
+ return entries.map(([type, count]) => `${type}=${count}`).join(' ');
827
+ }
828
+
829
+ function formatReviewResolution(resolution = null) {
830
+ if (!resolution || !resolution.resolution) return 'none';
831
+ const at = resolution.resolvedAt ? ` at=${formatTimestamp(resolution.resolvedAt)}` : '';
832
+ const defer = resolution.deferUntil ? ` deferUntil=${formatTimestamp(resolution.deferUntil)}` : '';
833
+ return `${resolution.resolution}${at}${defer}`;
834
+ }
835
+
836
+ function formatReviewQueue(result = {}) {
837
+ if (result.available === false) {
838
+ return result.error || 'Memory review queue is unavailable.';
839
+ }
840
+ const rows = Array.isArray(result.items) ? result.items : (Array.isArray(result.reviewQueue) ? result.reviewQueue : []);
841
+ if (rows.length === 0) return 'No memory review items found.';
842
+ return rows.map(row => {
843
+ const scope = row.scope?.key ? ` scope=${row.scope.key}` : '';
844
+ const latest = row.latestFeedbackAt ? ` latest=${formatTimestamp(row.latestFeedbackAt)}` : '';
845
+ const resolution = row.resolution ? ` resolution=${formatReviewResolution(row.resolution)}` : '';
846
+ const counts = formatFeedbackCounts(row.feedbackCounts);
847
+ return `#${row.memoryId} severity=${row.severityScore} issues=${row.issueCount}/${row.feedbackCount} ${row.status || '?'}${scope} ${row.canonicalKey || '?'} feedback=${counts}${latest}${resolution}`;
848
+ }).join('\n');
849
+ }
850
+
851
+ function formatReviewInspect(result = {}) {
852
+ const memory = result.memory || {};
853
+ const review = result.review || {};
854
+ const visibility = memory.visibility || {};
855
+ const lines = [
856
+ `Memory #${memory.memoryId || '?'} ${memory.status || '?'} ${memory.canonicalKey || '?'}`,
857
+ `Scope: ${memory.scope?.kind || '?'}/${memory.scope?.key || '?'}`,
858
+ `Visibility: bootstrap=${visibility.bootstrap === true} recall=${visibility.recall === true}`,
859
+ `Feedback: issues=${review.issueCount || 0}/${review.feedbackCount || 0} ${formatFeedbackCounts(review.feedbackCounts)}`,
860
+ `Latest resolution: ${formatReviewResolution(review.latestResolution)}`,
861
+ ];
862
+ const events = Array.isArray(review.recentFeedback) ? review.recentFeedback : [];
863
+ if (events.length > 0) {
864
+ lines.push('Recent feedback:');
865
+ for (const event of events) {
866
+ const actor = event.actorId ? `${event.actorKind || 'actor'}:${event.actorId}` : (event.actorKind || 'actor');
867
+ lines.push(` #${event.id} ${event.feedbackType} ${actor} at=${formatTimestamp(event.createdAt)}`);
868
+ }
869
+ }
870
+ return lines.join('\n');
871
+ }
872
+
873
+ function formatReviewResolve(result = {}) {
874
+ const resolution = result.resolution || {};
875
+ const memory = result.memory || {};
876
+ const issueCount = result.review?.issueFeedbackCount || 0;
877
+ return [
878
+ `Review resolution recorded: ${resolution.resolution || '?'} for memory #${memory.memoryId || resolution.memoryId || '?'}`,
879
+ `Canonical key: ${memory.canonicalKey || resolution.canonicalKey || '?'}`,
880
+ `Issue feedback covered: ${issueCount} ${formatFeedbackCounts(result.review?.issueFeedbackCounts)}`,
881
+ `Memory mutated: ${result.memoryMutated === true}`,
882
+ ].join('\n');
883
+ }
884
+
885
+ async function cmdReview(aquifer, args) {
886
+ const verb = args._[1] || 'queue';
887
+ if (verb === 'queue') {
888
+ const activeScopePath = parseScopePath(args.flags['active-scope-path']);
889
+ const activeScopeKey = args.flags['active-scope-key']
890
+ || args.flags['scope-key']
891
+ || (activeScopePath ? activeScopePath[activeScopePath.length - 1] : undefined);
892
+ const result = await aquifer.review.queue({
893
+ feedbackType: args.flags['feedback-type'] || undefined,
894
+ memoryType: args.flags['memory-type'] || undefined,
895
+ visibility: args.flags.visibility || undefined,
896
+ activeScopeKey,
897
+ activeScopePath,
898
+ scopeKey: args.flags['scope-key'] || activeScopeKey,
899
+ canonicalKey: args.flags['canonical-key'] || undefined,
900
+ dateFrom: args.flags['date-from'] || undefined,
901
+ dateTo: args.flags['date-to'] || undefined,
902
+ asOf: args.flags['as-of'] || args.flags['snapshot-as-of'] || undefined,
903
+ includeResolved: args.flags['include-resolved'] === true,
904
+ limit: parsePositiveInt(args.flags.limit, 20),
905
+ });
906
+ if (args.flags.json) {
907
+ console.log(JSON.stringify(result, null, 2));
908
+ return;
909
+ }
910
+ console.log(formatReviewQueue(result));
911
+ return;
912
+ }
913
+
914
+ if (verb === 'inspect') {
915
+ const activeScopePath = parseScopePath(args.flags['active-scope-path']);
916
+ const activeScopeKey = args.flags['active-scope-key']
917
+ || args.flags['scope-key']
918
+ || (activeScopePath ? activeScopePath[activeScopePath.length - 1] : undefined);
919
+ const memoryId = args.flags['memory-id'] || args.flags.id || args._[2] || undefined;
920
+ const canonicalKey = args.flags['canonical-key'] || undefined;
921
+ if (memoryId && canonicalKey) {
922
+ console.error('Usage: --memory-id/--id is mutually exclusive with --canonical-key.');
923
+ process.exit(1);
924
+ }
925
+ if (!memoryId && !canonicalKey) {
926
+ console.error('Usage: aquifer review inspect (--memory-id ID | --canonical-key KEY) [--json]');
927
+ process.exit(1);
928
+ }
929
+ const result = await aquifer.review.inspect({
930
+ memoryId,
931
+ canonicalKey,
932
+ activeScopeKey,
933
+ activeScopePath,
934
+ scopeKey: args.flags['scope-key'] || activeScopeKey,
935
+ visibility: args.flags.visibility || undefined,
936
+ feedbackType: args.flags['feedback-type'] || undefined,
937
+ dateFrom: args.flags['date-from'] || undefined,
938
+ dateTo: args.flags['date-to'] || undefined,
939
+ asOf: args.flags['as-of'] || args.flags['snapshot-as-of'] || undefined,
940
+ limit: parsePositiveInt(args.flags.limit, 20),
941
+ });
942
+ if (args.flags.json) {
943
+ console.log(JSON.stringify(result, null, 2));
944
+ return;
945
+ }
946
+ console.log(formatReviewInspect(result));
947
+ return;
948
+ }
949
+
950
+ if (verb === 'resolve') {
951
+ const activeScopePath = parseScopePath(args.flags['active-scope-path']);
952
+ const activeScopeKey = args.flags['active-scope-key']
953
+ || args.flags['scope-key']
954
+ || (activeScopePath ? activeScopePath[activeScopePath.length - 1] : undefined);
955
+ const memoryId = args.flags['memory-id'] || args.flags.id || undefined;
956
+ const canonicalKey = args.flags['canonical-key'] || undefined;
957
+ if (memoryId && canonicalKey) {
958
+ console.error('Usage: --memory-id/--id is mutually exclusive with --canonical-key.');
959
+ process.exit(1);
960
+ }
961
+ if (!memoryId && !canonicalKey) {
962
+ console.error('Usage: aquifer review resolve (--memory-id ID | --canonical-key KEY) --resolution resolved|ignored|deferred [--json]');
963
+ process.exit(1);
964
+ }
965
+ if (!args.flags['expected-latest-issue-feedback-id'] && !args.flags['expected-latest-issue-feedback-at']) {
966
+ console.error('Usage: aquifer review resolve requires --expected-latest-issue-feedback-id or --expected-latest-issue-feedback-at.');
967
+ process.exit(1);
968
+ }
969
+ const result = await aquifer.review.resolve({
970
+ memoryId,
971
+ canonicalKey,
972
+ resolution: args.flags.resolution || undefined,
973
+ reason: args.flags.reason || args.flags.note || undefined,
974
+ actorKind: args.flags['actor-kind'] || undefined,
975
+ actorId: args.flags['actor-id'] || args.flags['agent-id'] || undefined,
976
+ deferUntil: args.flags['defer-until'] || undefined,
977
+ expectedLatestIssueFeedbackId: args.flags['expected-latest-issue-feedback-id'] || undefined,
978
+ expectedLatestIssueFeedbackAt: args.flags['expected-latest-issue-feedback-at'] || undefined,
979
+ activeScopeKey,
980
+ activeScopePath,
981
+ scopeKey: args.flags['scope-key'] || activeScopeKey,
982
+ visibility: args.flags.visibility || undefined,
983
+ asOf: args.flags['as-of'] || args.flags['snapshot-as-of'] || undefined,
984
+ });
985
+ if (args.flags.json) {
986
+ console.log(JSON.stringify(result, null, 2));
987
+ return;
988
+ }
989
+ console.log(formatReviewResolve(result));
990
+ return;
991
+ }
992
+
993
+ console.error('Usage: aquifer review <queue|inspect|resolve> [options]');
994
+ process.exit(1);
995
+ }
996
+
997
+ async function cmdExplain(aquifer, args) {
998
+ const lane = args._[1] || 'bootstrap';
999
+ if (lane === 'bootstrap') {
1000
+ const result = await aquifer.memory.explainBootstrap({
1001
+ activeScopeKey: args.flags['active-scope-key'] || undefined,
1002
+ activeScopePath: parseScopePath(args.flags['active-scope-path']),
1003
+ limit: parsePositiveInt(args.flags.limit, 5),
1004
+ maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
1005
+ });
1006
+ if (args.flags.json) {
1007
+ console.log(JSON.stringify(result, null, 2));
1008
+ return;
1009
+ }
1010
+ console.log(formatExplain(result));
1011
+ return;
1012
+ }
1013
+ if (lane === 'memory') {
1014
+ const query = args.flags.query || args._.slice(2).join(' ');
1015
+ if (!query) {
1016
+ console.error('Usage: aquifer explain memory --query TEXT --active-scope-key KEY [--limit N] [--json]');
1017
+ process.exit(1);
1018
+ }
1019
+ const result = await aquifer.memory.explainCurrent(query, {
1020
+ activeScopeKey: args.flags['active-scope-key'] || undefined,
1021
+ activeScopePath: parseScopePath(args.flags['active-scope-path']),
1022
+ limit: parsePositiveInt(args.flags.limit, 5),
1023
+ });
1024
+ if (args.flags.json) {
1025
+ console.log(JSON.stringify(result, null, 2));
1026
+ return;
1027
+ }
1028
+ console.log(formatExplain(result));
1029
+ return;
1030
+ }
1031
+ console.error('Usage: aquifer explain <bootstrap|memory> [options]');
1032
+ process.exit(1);
1033
+ }
1034
+
1035
+ function formatOperatorStatus(result = {}) {
1036
+ const compaction = result.compaction || {};
1037
+ const checkpoint = result.checkpoint || {};
1038
+ const lines = [
1039
+ `Compaction: ${compaction.available === false ? 'unavailable' : 'available'} statuses=${JSON.stringify(compaction.statusCounts || {})}`,
1040
+ `Checkpoint: ${checkpoint.available === false ? 'unavailable' : 'available'} statuses=${JSON.stringify(checkpoint.statusCounts || {})}`,
1041
+ ];
1042
+ if (Array.isArray(compaction.staleClaims) && compaction.staleClaims.length > 0) {
1043
+ lines.push(`Stale compaction claims: ${compaction.staleClaims.map(run => `#${run.id}/${run.cadence}`).join(', ')}`);
1044
+ }
1045
+ if (Array.isArray(compaction.latest) && compaction.latest.length > 0) {
1046
+ lines.push('Latest compaction: ' + compaction.latest.map(run => `#${run.id} ${run.status} ${run.cadence}`).join(', '));
1047
+ }
1048
+ if (Array.isArray(checkpoint.latest) && checkpoint.latest.length > 0) {
1049
+ lines.push('Latest checkpoint: ' + checkpoint.latest.map(run => `#${run.id} ${run.status} sources=${run.sourceCount}`).join(', '));
1050
+ }
1051
+ return lines.join('\n');
1052
+ }
1053
+
1054
+ function formatOperatorInspect(result = {}) {
1055
+ const run = result.run || {};
1056
+ const lines = [
1057
+ `${run.kind || 'operator'} run #${run.id || '?'} status=${run.status || '?'}`,
1058
+ ];
1059
+ if (run.kind === 'compaction') {
1060
+ lines.push(`Window: ${run.cadence || '?'} ${run.periodStart || '?'} -> ${run.periodEnd || '?'}`);
1061
+ lines.push(`Plan: candidates=${run.candidateCount || 0} statusUpdates=${run.statusUpdateCount || 0}`);
1062
+ if (run.leaseExpiresAt) lines.push(`Claim: worker=${run.workerId || '?'} leaseExpiresAt=${run.leaseExpiresAt}`);
1063
+ } else if (run.kind === 'checkpoint') {
1064
+ lines.push(`Checkpoint: scope=${run.scopeId || '?'} key=${run.checkpointKey || '?'}`);
1065
+ lines.push(`Range: ${run.fromFinalizationIdExclusive || 0} -> ${run.toFinalizationIdInclusive || '?'}`);
1066
+ lines.push(`Sources: ${run.sourceCount || 0}`);
1067
+ }
1068
+ if (run.error) lines.push(`Error: ${run.error}`);
1069
+ if (Array.isArray(result.sources) && result.sources.length > 0) {
1070
+ lines.push('Sources:');
1071
+ for (const source of result.sources) {
1072
+ lines.push(` [${source.sourceIndex}] finalization=${source.finalizationId} ${source.source || '?'}/${source.agentId || '?'}/${source.sessionId || '?'}`);
1073
+ }
1074
+ }
1075
+ return lines.join('\n');
1076
+ }
1077
+
625
1078
  async function cmdOperator(aquifer, args) {
626
1079
  const operatorVerb = args._[1] || 'compaction';
627
1080
  const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
628
1081
 
1082
+ if (operatorVerb === 'status') {
1083
+ const result = await aquifer.operator.status({
1084
+ limit: parsePositiveInt(args.flags.limit, 10),
1085
+ });
1086
+ if (args.flags.json) {
1087
+ console.log(JSON.stringify(result, null, 2));
1088
+ return;
1089
+ }
1090
+ console.log(formatOperatorStatus(result));
1091
+ return;
1092
+ }
1093
+
1094
+ if (operatorVerb === 'inspect') {
1095
+ const runId = args.flags['run-id'] || args.flags.id || args._[2];
1096
+ if (!runId) {
1097
+ console.error('Usage: aquifer operator inspect --run-id ID [--kind compaction|checkpoint] [--json]');
1098
+ process.exit(1);
1099
+ }
1100
+ const result = await aquifer.operator.inspect({
1101
+ runId,
1102
+ kind: args.flags.kind || 'compaction',
1103
+ });
1104
+ if (args.flags.json) {
1105
+ console.log(JSON.stringify(result, null, 2));
1106
+ return;
1107
+ }
1108
+ console.log(formatOperatorInspect(result));
1109
+ return;
1110
+ }
1111
+
629
1112
  if (operatorVerb === 'checkpoint') {
630
1113
  const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
631
1114
  const result = await aquifer.checkpoints.runProducer({
@@ -808,13 +1291,18 @@ Commands:
808
1291
  quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
809
1292
  migrate Run database migrations
810
1293
  backend-info Show selected backend capabilities without connecting to a database
1294
+ install-openclaw Install/update OpenClaw MCP + extension wiring
1295
+ doctor Run read-only health diagnostics
811
1296
  recall <query> Search sessions (requires embed config)
812
1297
  evidence-recall <query> Search legacy session/evidence plane explicitly
813
1298
  feedback Record trust feedback on a session
814
1299
  memory-feedback Record curated memory feedback
815
1300
  feedback-stats Show trust feedback statistics and coverage
1301
+ review ... Inspect or resolve memory feedback review queue
816
1302
  backfill Enrich pending sessions
817
1303
  operator ... Run operator-safe consolidation jobs
1304
+ finalization ... Inspect read-only finalization ledger
1305
+ explain ... Explain current-memory serving decisions
818
1306
  compact Plan or apply curated memory compaction
819
1307
  stats Show database statistics
820
1308
  export Export sessions as JSONL
@@ -831,11 +1319,32 @@ Options:
831
1319
  --date-to YYYY-MM-DD End date
832
1320
  --entities A,B,C Entity names (comma-separated, recall)
833
1321
  --entity-mode any|all Entity match mode (recall, default: any)
1322
+ --id ID Finalization row id
1323
+ --host HOST Finalization host filter
1324
+ --status STATUS Finalization status filter
1325
+ --phase PHASE Finalization phase filter
1326
+ --transcript-hash HASH Finalization transcript hash filter
1327
+ --run-id ID Operator run id
1328
+ --kind KIND Operator inspect kind: compaction|checkpoint
1329
+ --query TEXT Explain memory query
834
1330
  --session-id ID Session ID (feedback)
835
- --memory-id ID Curated memory record ID (memory-feedback)
836
- --canonical-key KEY Active curated memory canonical key (memory-feedback)
1331
+ --memory-id ID Curated memory record ID (memory-feedback, review inspect/resolve)
1332
+ --canonical-key KEY Active curated memory canonical key (memory-feedback, review queue/inspect/resolve)
837
1333
  --verdict helpful|unhelpful Feedback verdict (feedback)
838
1334
  --feedback-type TYPE Curated memory feedback type
1335
+ --memory-type TYPE Curated memory type filter (review queue)
1336
+ --visibility MODE Review visibility filter: either|both|bootstrap|recall
1337
+ --as-of ISO Review queue memory validity instant
1338
+ --include-resolved Include currently resolved review items in review queue
1339
+ --resolution ACTION Review resolution: resolved|ignored|deferred
1340
+ --reason TEXT Operator reason for review resolution
1341
+ --actor-kind KIND Review resolution actor kind: user|agent|system|curator
1342
+ --actor-id ID Review resolution actor id
1343
+ --defer-until ISO Re-open deferred review item after this instant
1344
+ --expected-latest-issue-feedback-id ID
1345
+ Reject review resolution if newer issue feedback exists
1346
+ --expected-latest-issue-feedback-at ISO
1347
+ Timestamp guard for review resolution snapshots
839
1348
  --note TEXT Feedback note (feedback)
840
1349
  --explain Show score breakdown per result (recall)
841
1350
  --allow-unsafe-debug Allow broad evidence-recall without audit boundary
@@ -843,6 +1352,11 @@ Options:
843
1352
  --dry-run Preview only (backfill)
844
1353
  --output PATH Output file (export)
845
1354
  --config PATH Config file path
1355
+ --openclaw-home PATH OpenClaw home path (install-openclaw)
1356
+ --skip-extension Do not wire the OpenClaw extension/plugin (install-openclaw)
1357
+ --skip-mcp Do not update mcp.servers.aquifer (install-openclaw)
1358
+ --force Move aside existing extension path if needed (install-openclaw)
1359
+ --link-current-package Development only: wire the current checkout instead of OpenClaw node_modules
846
1360
  --lookback-days N How far back in days (bootstrap, default: 14)
847
1361
  --max-chars N Max output characters (bootstrap, default: 4000)
848
1362
  --active-scope-key KEY Active curated memory scope key
@@ -868,6 +1382,17 @@ Options:
868
1382
  --min-messages N Min user messages to ingest (ingest-opencode, default: 3)
869
1383
 
870
1384
  Operator examples:
1385
+ aquifer doctor --json
1386
+ aquifer finalization list --status finalized --json
1387
+ aquifer finalization inspect --id 42 --json
1388
+ aquifer finalization inspect --session-id SESSION --agent-id main --source codex --transcript-hash HASH
1389
+ aquifer explain bootstrap --active-scope-key project:aquifer --json
1390
+ aquifer explain memory --query "serving contract" --active-scope-key project:aquifer --json
1391
+ aquifer review queue --scope-key project:aquifer --feedback-type incorrect --json
1392
+ aquifer review inspect --memory-id 42 --json
1393
+ aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --json
1394
+ aquifer operator status --json
1395
+ aquifer operator inspect --run-id 42 --kind compaction --json
871
1396
  aquifer operator compaction daily --json
872
1397
  aquifer operator compaction daily --include-synthesis-prompt --json
873
1398
  aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
@@ -876,7 +1401,9 @@ Operator examples:
876
1401
  aquifer operator checkpoint --scope-id 7 --synthesis-summary-file /tmp/checkpoint-summary.json --apply --finalize --json
877
1402
  AQUIFER_BACKEND=local aquifer backend-info --json
878
1403
  aquifer codex-recovery checkpoint-heartbeat --hook-stdin --scope-key project:aquifer
1404
+ aquifer codex-recovery checkpoint-spool-status --json --limit 10
879
1405
  aquifer codex-recovery checkpoint-heartbeat-hook --scope-key project:aquifer --hooks-path "$CODEX_HOME/hooks.json" --json
1406
+ aquifer install-openclaw --openclaw-home "$OPENCLAW_HOME"
880
1407
  aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
881
1408
  process.exit(0);
882
1409
  }
@@ -917,6 +1444,11 @@ Operator examples:
917
1444
  return;
918
1445
  }
919
1446
 
1447
+ if (command === 'install-openclaw') {
1448
+ await require('./openclaw-install').cmdInstallOpenClaw(args);
1449
+ return;
1450
+ }
1451
+
920
1452
  // All other commands need an Aquifer instance
921
1453
  const configOverrides = {};
922
1454
  if (args.flags.config) {
@@ -962,7 +1494,10 @@ Operator examples:
962
1494
  await cmdQuickstart(aquifer);
963
1495
  break;
964
1496
  case 'migrate':
965
- await cmdMigrate(aquifer);
1497
+ await cmdMigrate(aquifer, args);
1498
+ break;
1499
+ case 'doctor':
1500
+ await cmdDoctor(aquifer, args);
966
1501
  break;
967
1502
  case 'recall':
968
1503
  await cmdRecall(aquifer, args);
@@ -979,12 +1514,21 @@ Operator examples:
979
1514
  case 'feedback-stats':
980
1515
  await cmdFeedbackStats(aquifer, args);
981
1516
  break;
1517
+ case 'review':
1518
+ await cmdReview(aquifer, args);
1519
+ break;
982
1520
  case 'backfill':
983
1521
  await cmdBackfill(aquifer, args);
984
1522
  break;
985
1523
  case 'operator':
986
1524
  await cmdOperator(aquifer, args);
987
1525
  break;
1526
+ case 'finalization':
1527
+ await cmdFinalization(aquifer, args);
1528
+ break;
1529
+ case 'explain':
1530
+ await cmdExplain(aquifer, args);
1531
+ break;
988
1532
  case 'compact':
989
1533
  await cmdCompact(aquifer, args);
990
1534
  break;
@@ -1016,8 +1560,20 @@ module.exports = {
1016
1560
  parseArgs,
1017
1561
  selectedBackendInfo,
1018
1562
  cmdBackendInfo,
1563
+ cmdMigrate,
1564
+ cmdDoctor,
1019
1565
  cmdLocalQuickstart,
1020
1566
  cmdOperator,
1567
+ cmdFinalization,
1568
+ cmdExplain,
1569
+ cmdReview,
1570
+ formatDoctor,
1571
+ formatFinalizationList,
1572
+ formatFinalizationInspect,
1573
+ formatExplain,
1574
+ formatReviewQueue,
1575
+ formatReviewInspect,
1576
+ cmdInstallOpenClaw: require('./openclaw-install').cmdInstallOpenClaw,
1021
1577
  readSynthesisSummaryFromFlags,
1022
1578
  };
1023
1579