@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
package/core/storage.js CHANGED
@@ -32,6 +32,33 @@ const TURN_NOISE_RE = [
32
32
  ];
33
33
 
34
34
  const VALID_STATUSES = new Set(['pending', 'processing', 'succeeded', 'partial', 'failed', 'skipped']);
35
+ const FINALIZATION_STATUSES = new Set([
36
+ 'pending',
37
+ 'processing',
38
+ 'finalized',
39
+ 'failed',
40
+ 'skipped',
41
+ 'declined',
42
+ 'deferred',
43
+ ]);
44
+ const FINALIZATION_TERMINAL_STATUSES = new Set(['finalized', 'skipped', 'declined', 'deferred']);
45
+ const FINALIZATION_MODES = new Set([
46
+ 'handoff',
47
+ 'session_end',
48
+ 'session_start_recovery',
49
+ 'afterburn',
50
+ 'manual',
51
+ ]);
52
+
53
+ function requireField(obj, field) {
54
+ if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
55
+ throw new Error(`${field} is required`);
56
+ }
57
+ }
58
+
59
+ function toJson(value, fallback) {
60
+ return JSON.stringify(value === undefined ? fallback : value);
61
+ }
35
62
 
36
63
  // ---------------------------------------------------------------------------
37
64
  // upsertSession
@@ -127,7 +154,7 @@ async function upsertSummary(pool, sessionRowId, {
127
154
  tenant_id = EXCLUDED.tenant_id,
128
155
  agent_id = EXCLUDED.agent_id,
129
156
  session_id = EXCLUDED.session_id,
130
- model = COALESCE(EXCLUDED.model, ${qi(schema)}.session_summaries.model),
157
+ model = COALESCE(NULLIF(EXCLUDED.model, 'unknown'), ${qi(schema)}.session_summaries.model),
131
158
  source_hash = COALESCE(EXCLUDED.source_hash, ${qi(schema)}.session_summaries.source_hash),
132
159
  message_count = COALESCE(EXCLUDED.message_count, ${qi(schema)}.session_summaries.message_count),
133
160
  user_message_count = COALESCE(EXCLUDED.user_message_count, ${qi(schema)}.session_summaries.user_message_count),
@@ -141,7 +168,7 @@ async function upsertSummary(pool, sessionRowId, {
141
168
  RETURNING session_row_id, tenant_id, agent_id, session_id, model`,
142
169
  [
143
170
  sessionRowId, tenantId, agentId || null, sessionId || null,
144
- model || null, sourceHash || null,
171
+ model || 'unknown', sourceHash || null,
145
172
  msgCount || 0, userCount || 0, assistantCount || 0,
146
173
  startedAt || null, endedAt || null,
147
174
  structuredSummary ? JSON.stringify(structuredSummary) : null,
@@ -314,6 +341,357 @@ async function recordAccess(pool, sessionRowIds, { schema } = {}) {
314
341
  );
315
342
  }
316
343
 
344
+ // ---------------------------------------------------------------------------
345
+ // Session finalization ledger
346
+ // ---------------------------------------------------------------------------
347
+
348
+ function normalizeFinalizationStatus(status) {
349
+ const out = status || 'pending';
350
+ if (!FINALIZATION_STATUSES.has(out)) throw new Error(`Invalid finalization status: ${out}`);
351
+ return out;
352
+ }
353
+
354
+ function finalizationTerminalSql(alias) {
355
+ const values = [...FINALIZATION_TERMINAL_STATUSES].map(value => `'${value}'`).join(',');
356
+ return `${alias}.status IN (${values})`;
357
+ }
358
+
359
+ function normalizeFinalizationMode(mode) {
360
+ const out = mode || 'handoff';
361
+ if (!FINALIZATION_MODES.has(out)) throw new Error(`Invalid finalization mode: ${out}`);
362
+ return out;
363
+ }
364
+
365
+ async function upsertSessionFinalization(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
366
+ requireField(input, 'sessionRowId');
367
+ requireField(input, 'sessionId');
368
+ requireField(input, 'agentId');
369
+ requireField(input, 'source');
370
+ requireField(input, 'transcriptHash');
371
+ const tenantId = input.tenantId || defaultTenantId || 'default';
372
+ const status = normalizeFinalizationStatus(input.status || 'pending');
373
+ const mode = normalizeFinalizationMode(input.mode || 'handoff');
374
+ const phase = input.phase || 'curated_memory_v1';
375
+ const preserveTerminal = `${finalizationTerminalSql(qi(schema) + '.session_finalizations')}
376
+ AND ${qi(schema)}.session_finalizations.status <> EXCLUDED.status`;
377
+ const result = await pool.query(
378
+ `INSERT INTO ${qi(schema)}.session_finalizations (
379
+ tenant_id, session_row_id, source, host, agent_id, session_id,
380
+ transcript_hash, phase, mode, status, finalizer_model, scope_kind,
381
+ scope_key, context_key, topic_key, summary_row_id, memory_result,
382
+ summary_text, structured_summary, human_review_text, session_start_text,
383
+ error, metadata, claimed_at, finalized_at
384
+ )
385
+ VALUES (
386
+ $1,$2,$3,COALESCE($4,'codex'),$5,$6,$7,COALESCE($8,'curated_memory_v1'),
387
+ $9,$10,$11,$12,$13,$14,$15,$16,COALESCE($17::jsonb,'{}'::jsonb),
388
+ $18,COALESCE($19::jsonb,'{}'::jsonb),$20,$21,
389
+ $22,COALESCE($23::jsonb,'{}'::jsonb),$24,$25
390
+ )
391
+ ON CONFLICT (tenant_id, source, agent_id, session_id, transcript_hash, phase)
392
+ DO UPDATE SET
393
+ session_row_id = CASE
394
+ WHEN ${preserveTerminal}
395
+ THEN ${qi(schema)}.session_finalizations.session_row_id
396
+ ELSE EXCLUDED.session_row_id
397
+ END,
398
+ host = CASE
399
+ WHEN ${preserveTerminal}
400
+ THEN ${qi(schema)}.session_finalizations.host
401
+ ELSE EXCLUDED.host
402
+ END,
403
+ mode = CASE
404
+ WHEN ${preserveTerminal}
405
+ THEN ${qi(schema)}.session_finalizations.mode
406
+ ELSE EXCLUDED.mode
407
+ END,
408
+ status = CASE
409
+ WHEN ${preserveTerminal}
410
+ THEN ${qi(schema)}.session_finalizations.status
411
+ ELSE EXCLUDED.status
412
+ END,
413
+ finalizer_model = CASE
414
+ WHEN ${preserveTerminal}
415
+ THEN ${qi(schema)}.session_finalizations.finalizer_model
416
+ ELSE COALESCE(EXCLUDED.finalizer_model, ${qi(schema)}.session_finalizations.finalizer_model)
417
+ END,
418
+ scope_kind = CASE
419
+ WHEN ${preserveTerminal}
420
+ THEN ${qi(schema)}.session_finalizations.scope_kind
421
+ ELSE COALESCE(EXCLUDED.scope_kind, ${qi(schema)}.session_finalizations.scope_kind)
422
+ END,
423
+ scope_key = CASE
424
+ WHEN ${preserveTerminal}
425
+ THEN ${qi(schema)}.session_finalizations.scope_key
426
+ ELSE COALESCE(EXCLUDED.scope_key, ${qi(schema)}.session_finalizations.scope_key)
427
+ END,
428
+ context_key = CASE
429
+ WHEN ${preserveTerminal}
430
+ THEN ${qi(schema)}.session_finalizations.context_key
431
+ ELSE COALESCE(EXCLUDED.context_key, ${qi(schema)}.session_finalizations.context_key)
432
+ END,
433
+ topic_key = CASE
434
+ WHEN ${preserveTerminal}
435
+ THEN ${qi(schema)}.session_finalizations.topic_key
436
+ ELSE COALESCE(EXCLUDED.topic_key, ${qi(schema)}.session_finalizations.topic_key)
437
+ END,
438
+ summary_row_id = CASE
439
+ WHEN ${preserveTerminal}
440
+ THEN ${qi(schema)}.session_finalizations.summary_row_id
441
+ ELSE COALESCE(EXCLUDED.summary_row_id, ${qi(schema)}.session_finalizations.summary_row_id)
442
+ END,
443
+ memory_result = CASE
444
+ WHEN ${preserveTerminal}
445
+ THEN ${qi(schema)}.session_finalizations.memory_result
446
+ ELSE COALESCE(NULLIF(EXCLUDED.memory_result, '{}'::jsonb), ${qi(schema)}.session_finalizations.memory_result)
447
+ END,
448
+ summary_text = CASE
449
+ WHEN ${preserveTerminal}
450
+ THEN ${qi(schema)}.session_finalizations.summary_text
451
+ ELSE COALESCE(EXCLUDED.summary_text, ${qi(schema)}.session_finalizations.summary_text)
452
+ END,
453
+ structured_summary = CASE
454
+ WHEN ${preserveTerminal}
455
+ THEN ${qi(schema)}.session_finalizations.structured_summary
456
+ ELSE COALESCE(NULLIF(EXCLUDED.structured_summary, '{}'::jsonb), ${qi(schema)}.session_finalizations.structured_summary)
457
+ END,
458
+ human_review_text = CASE
459
+ WHEN ${preserveTerminal}
460
+ THEN ${qi(schema)}.session_finalizations.human_review_text
461
+ ELSE COALESCE(EXCLUDED.human_review_text, ${qi(schema)}.session_finalizations.human_review_text)
462
+ END,
463
+ session_start_text = CASE
464
+ WHEN ${preserveTerminal}
465
+ THEN ${qi(schema)}.session_finalizations.session_start_text
466
+ ELSE COALESCE(EXCLUDED.session_start_text, ${qi(schema)}.session_finalizations.session_start_text)
467
+ END,
468
+ error = CASE
469
+ WHEN ${preserveTerminal}
470
+ THEN ${qi(schema)}.session_finalizations.error
471
+ ELSE EXCLUDED.error
472
+ END,
473
+ metadata = CASE
474
+ WHEN ${preserveTerminal}
475
+ THEN ${qi(schema)}.session_finalizations.metadata
476
+ ELSE COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${qi(schema)}.session_finalizations.metadata)
477
+ END,
478
+ claimed_at = CASE
479
+ WHEN ${preserveTerminal}
480
+ THEN ${qi(schema)}.session_finalizations.claimed_at
481
+ ELSE COALESCE(EXCLUDED.claimed_at, ${qi(schema)}.session_finalizations.claimed_at)
482
+ END,
483
+ finalized_at = CASE
484
+ WHEN ${preserveTerminal}
485
+ THEN ${qi(schema)}.session_finalizations.finalized_at
486
+ ELSE COALESCE(EXCLUDED.finalized_at, ${qi(schema)}.session_finalizations.finalized_at)
487
+ END,
488
+ updated_at = CASE
489
+ WHEN ${preserveTerminal}
490
+ THEN ${qi(schema)}.session_finalizations.updated_at
491
+ ELSE now()
492
+ END
493
+ RETURNING *`,
494
+ [
495
+ tenantId,
496
+ input.sessionRowId,
497
+ input.source,
498
+ input.host || 'codex',
499
+ input.agentId,
500
+ input.sessionId,
501
+ input.transcriptHash,
502
+ phase,
503
+ mode,
504
+ status,
505
+ input.finalizerModel || null,
506
+ input.scopeKind || null,
507
+ input.scopeKey || null,
508
+ input.contextKey || null,
509
+ input.topicKey || null,
510
+ input.summaryRowId || null,
511
+ toJson(input.memoryResult, {}),
512
+ input.summaryText || null,
513
+ toJson(input.structuredSummary, {}),
514
+ input.humanReviewText || null,
515
+ input.sessionStartText || null,
516
+ input.error || null,
517
+ toJson(input.metadata, {}),
518
+ input.claimedAt || (status === 'processing' ? new Date().toISOString() : null),
519
+ input.finalizedAt || (status === 'finalized' ? new Date().toISOString() : null),
520
+ ]
521
+ );
522
+ return result.rows[0] || null;
523
+ }
524
+
525
+ async function getSessionFinalization(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
526
+ requireField(input, 'sessionId');
527
+ requireField(input, 'agentId');
528
+ requireField(input, 'source');
529
+ requireField(input, 'transcriptHash');
530
+ const tenantId = input.tenantId || defaultTenantId || 'default';
531
+ const phase = input.phase || 'curated_memory_v1';
532
+ const result = await pool.query(
533
+ `SELECT *
534
+ FROM ${qi(schema)}.session_finalizations
535
+ WHERE tenant_id = $1
536
+ AND source = $2
537
+ AND agent_id = $3
538
+ AND session_id = $4
539
+ AND transcript_hash = $5
540
+ AND phase = $6
541
+ LIMIT 1`,
542
+ [tenantId, input.source, input.agentId, input.sessionId, input.transcriptHash, phase]
543
+ );
544
+ return result.rows[0] || null;
545
+ }
546
+
547
+ async function updateSessionFinalizationStatus(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
548
+ const status = normalizeFinalizationStatus(input.status);
549
+ const tenantId = input.tenantId || defaultTenantId || 'default';
550
+ const params = [
551
+ tenantId,
552
+ status,
553
+ input.error || null,
554
+ input.finalizerModel || null,
555
+ toJson(input.memoryResult, {}),
556
+ toJson(input.metadata, {}),
557
+ ];
558
+ let where;
559
+ if (input.id) {
560
+ params.push(input.id);
561
+ where = `id = $${params.length}`;
562
+ } else {
563
+ requireField(input, 'sessionId');
564
+ requireField(input, 'agentId');
565
+ requireField(input, 'source');
566
+ requireField(input, 'transcriptHash');
567
+ params.push(input.source, input.agentId, input.sessionId, input.transcriptHash, input.phase || 'curated_memory_v1');
568
+ where = `source = $7 AND agent_id = $8 AND session_id = $9 AND transcript_hash = $10 AND phase = $11`;
569
+ }
570
+ const result = await pool.query(
571
+ `UPDATE ${qi(schema)}.session_finalizations
572
+ SET status = $2,
573
+ error = $3,
574
+ finalizer_model = COALESCE($4, finalizer_model),
575
+ memory_result = COALESCE(NULLIF($5::jsonb, '{}'::jsonb), memory_result),
576
+ metadata = COALESCE(NULLIF($6::jsonb, '{}'::jsonb), metadata),
577
+ finalized_at = CASE WHEN $2 = 'finalized' THEN COALESCE(finalized_at, now()) ELSE finalized_at END,
578
+ updated_at = now()
579
+ WHERE tenant_id = $1
580
+ AND ${where}
581
+ AND (
582
+ status NOT IN (${[...FINALIZATION_TERMINAL_STATUSES].map(value => `'${value}'`).join(',')})
583
+ OR status = $2
584
+ )
585
+ RETURNING *`,
586
+ params
587
+ );
588
+ return result.rows[0] || null;
589
+ }
590
+
591
+ async function listSessionFinalizations(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
592
+ const tenantId = input.tenantId || defaultTenantId || 'default';
593
+ const params = [tenantId];
594
+ const where = [`tenant_id = $1`];
595
+ if (input.host) {
596
+ params.push(input.host);
597
+ where.push(`host = $${params.length}`);
598
+ }
599
+ if (input.status) {
600
+ const statuses = Array.isArray(input.status) ? input.status : [input.status];
601
+ for (const status of statuses) normalizeFinalizationStatus(status);
602
+ params.push(statuses);
603
+ where.push(`status = ANY($${params.length}::text[])`);
604
+ }
605
+ if (input.agentId) {
606
+ params.push(input.agentId);
607
+ where.push(`agent_id = $${params.length}`);
608
+ }
609
+ if (input.source) {
610
+ params.push(input.source);
611
+ where.push(`source = $${params.length}`);
612
+ }
613
+ params.push(Math.max(1, Math.min(200, input.limit || 50)));
614
+ const result = await pool.query(
615
+ `SELECT *
616
+ FROM ${qi(schema)}.session_finalizations
617
+ WHERE ${where.join(' AND ')}
618
+ ORDER BY updated_at DESC, id DESC
619
+ LIMIT $${params.length}`,
620
+ params
621
+ );
622
+ return result.rows;
623
+ }
624
+
625
+ function candidateText(candidate = {}) {
626
+ if (typeof candidate === 'string') return candidate.trim();
627
+ const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : null;
628
+ for (const key of ['summary', 'title', 'decision', 'item', 'conclusion', 'statement', 'fact', 'text', 'note']) {
629
+ const text = String(candidate[key] || '').trim();
630
+ if (text) return text;
631
+ }
632
+ if (payload) {
633
+ for (const key of ['summary', 'title', 'decision', 'item', 'conclusion', 'statement', 'fact', 'text', 'note']) {
634
+ const text = String(payload[key] || '').trim();
635
+ if (text) return text;
636
+ }
637
+ }
638
+ return '';
639
+ }
640
+
641
+ async function upsertFinalizationCandidates(pool, rows = [], input = {}, { schema, tenantId: defaultTenantId } = {}) {
642
+ if (!Array.isArray(rows) || rows.length === 0) return [];
643
+ requireField(input, 'finalizationId');
644
+ const tenantId = input.tenantId || defaultTenantId || 'default';
645
+ const out = [];
646
+ for (let i = 0; i < rows.length; i++) {
647
+ const row = rows[i] || {};
648
+ const candidate = row.candidate || {};
649
+ const memory = row.memory || {};
650
+ const backingFact = row.backingFact || {};
651
+ const evidenceRefs = candidate.evidenceRefs || candidate.evidence_refs || [];
652
+ const result = await pool.query(
653
+ `INSERT INTO ${qi(schema)}.finalization_candidates (
654
+ tenant_id, finalization_id, session_id, candidate_index, action, reason,
655
+ memory_type, canonical_key, summary, payload, provenance,
656
+ memory_record_id, fact_assertion_id
657
+ )
658
+ VALUES (
659
+ $1,$2,$3,$4,$5,$6,$7,$8,$9,COALESCE($10::jsonb,'{}'::jsonb),COALESCE($11::jsonb,'{}'::jsonb),$12,$13
660
+ )
661
+ ON CONFLICT (tenant_id, finalization_id, candidate_index)
662
+ DO UPDATE SET
663
+ action = EXCLUDED.action,
664
+ reason = EXCLUDED.reason,
665
+ memory_type = COALESCE(EXCLUDED.memory_type, ${qi(schema)}.finalization_candidates.memory_type),
666
+ canonical_key = COALESCE(EXCLUDED.canonical_key, ${qi(schema)}.finalization_candidates.canonical_key),
667
+ summary = COALESCE(EXCLUDED.summary, ${qi(schema)}.finalization_candidates.summary),
668
+ payload = COALESCE(NULLIF(EXCLUDED.payload, '{}'::jsonb), ${qi(schema)}.finalization_candidates.payload),
669
+ provenance = COALESCE(NULLIF(EXCLUDED.provenance, '{}'::jsonb), ${qi(schema)}.finalization_candidates.provenance),
670
+ memory_record_id = COALESCE(EXCLUDED.memory_record_id, ${qi(schema)}.finalization_candidates.memory_record_id),
671
+ fact_assertion_id = COALESCE(EXCLUDED.fact_assertion_id, ${qi(schema)}.finalization_candidates.fact_assertion_id),
672
+ updated_at = now()
673
+ RETURNING *`,
674
+ [
675
+ tenantId,
676
+ input.finalizationId,
677
+ input.sessionId || null,
678
+ i,
679
+ row.action || 'skipped',
680
+ row.reason || null,
681
+ candidate.memoryType || candidate.memory_type || memory.memory_type || memory.memoryType || null,
682
+ candidate.canonicalKey || candidate.canonical_key || memory.canonical_key || memory.canonicalKey || null,
683
+ candidateText(candidate) || candidateText(memory) || null,
684
+ toJson(candidate.payload || candidate, {}),
685
+ toJson({ evidenceRefs }, {}),
686
+ memory.id || memory.memory_id || null,
687
+ backingFact.id || memory.backing_fact_id || null,
688
+ ]
689
+ );
690
+ out.push(result.rows[0] || null);
691
+ }
692
+ return out;
693
+ }
694
+
317
695
  // ---------------------------------------------------------------------------
318
696
  // extractUserTurns
319
697
  // ---------------------------------------------------------------------------
@@ -748,6 +1126,11 @@ module.exports = {
748
1126
  getMessages,
749
1127
  searchSessions,
750
1128
  recordAccess,
1129
+ upsertSessionFinalization,
1130
+ getSessionFinalization,
1131
+ updateSessionFinalizationStatus,
1132
+ listSessionFinalizations,
1133
+ upsertFinalizationCandidates,
751
1134
  extractUserTurns,
752
1135
  upsertTurnEmbeddings,
753
1136
  searchTurnEmbeddings,
@@ -0,0 +1,99 @@
1
+ # Aquifer Getting Started
2
+
3
+ This guide is the shortest path from zero to a working Aquifer memory backend.
4
+
5
+ By the end, you will have verified a full write -> enrich -> recall cycle and connected Aquifer to an MCP client.
6
+
7
+ ## What you need
8
+
9
+ - Node.js 18+
10
+ - PostgreSQL 15+ with `pgvector`
11
+ - An embedding endpoint such as Ollama or OpenAI
12
+
13
+ If you just want the default local path, the repo already includes a Docker stack for PostgreSQL + pgvector and Ollama.
14
+
15
+ ## Fast path: local Docker stack
16
+
17
+ From the repo root:
18
+
19
+ ```bash
20
+ docker compose up -d
21
+ npx --yes @shadowforge0/aquifer-memory quickstart
22
+ ```
23
+
24
+ `quickstart` runs migrations, writes a test session, embeds it, recalls it, and removes the test data.
25
+
26
+ If you see `✓ Aquifer is working`, the backend is ready.
27
+
28
+ ## If you already have PostgreSQL and embeddings
29
+
30
+ Set the minimum environment variables and run the same verification command:
31
+
32
+ ```bash
33
+ export DATABASE_URL="postgresql://aquifer:aquifer@localhost:5432/aquifer"
34
+ export AQUIFER_EMBED_BASE_URL="http://localhost:11434/v1"
35
+ export AQUIFER_EMBED_MODEL="bge-m3"
36
+
37
+ npx --yes @shadowforge0/aquifer-memory quickstart
38
+ ```
39
+
40
+ If you prefer OpenAI embeddings:
41
+
42
+ ```bash
43
+ export DATABASE_URL="postgresql://aquifer:aquifer@localhost:5432/aquifer"
44
+ export EMBED_PROVIDER="openai"
45
+ export OPENAI_API_KEY="sk-..."
46
+
47
+ npx --yes @shadowforge0/aquifer-memory quickstart
48
+ ```
49
+
50
+ ## Connect an MCP client
51
+
52
+ Once `quickstart` passes, point your MCP client at Aquifer:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "aquifer": {
58
+ "command": "npx",
59
+ "args": ["--yes", "@shadowforge0/aquifer-memory", "mcp"],
60
+ "env": {
61
+ "DATABASE_URL": "postgresql://aquifer:aquifer@localhost:5432/aquifer",
62
+ "EMBED_PROVIDER": "ollama",
63
+ "AQUIFER_MEMORY_SERVING_MODE": "legacy"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ Or run the server directly:
71
+
72
+ ```bash
73
+ DATABASE_URL=... EMBED_PROVIDER=ollama npx aquifer mcp
74
+ ```
75
+
76
+ For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want `session_recall` and `session_bootstrap` to serve active curated memory; `evidence_recall` remains the explicit evidence/debug tool in both modes. Rollback is just setting env or config back to `legacy`.
77
+
78
+ ## Most common commands
79
+
80
+ | Goal | Command |
81
+ |---|---|
82
+ | Verify setup | `npx aquifer quickstart` |
83
+ | Start MCP server | `npx aquifer mcp` |
84
+ | Search memory | `npx aquifer recall "auth middleware"` |
85
+ | Plan curated compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
86
+ | Show stats | `npx aquifer stats` |
87
+ | Enrich pending sessions | `npx aquifer backfill` |
88
+
89
+ The default public serving mode is `legacy`. To test scoped curated memory serving, set `AQUIFER_MEMORY_SERVING_MODE=curated` plus `AQUIFER_MEMORY_ACTIVE_SCOPE_KEY` or `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH`. Rollback is config-only: set the serving mode back to `legacy` and restart the MCP/CLI process.
90
+
91
+ ## If something fails
92
+
93
+ If `quickstart` cannot connect to PostgreSQL, make sure the database is running and `DATABASE_URL` is correct.
94
+
95
+ If you see `type "vector" does not exist`, `pgvector` is not installed.
96
+
97
+ If recall returns no results, the embedding endpoint is usually unreachable or misconfigured.
98
+
99
+ If you want the full setup matrix, host-specific examples, and advanced configuration for summarization, entities, reranking, or operations, continue to [docs/setup.md](setup.md).
@@ -21,7 +21,7 @@ postProcess?: (ctx: PostProcessContext) => Promise<void>
21
21
  ```ts
22
22
  interface PostProcessContext {
23
23
  session: {
24
- id: number; // DB primary key (miranda.sessions.id)
24
+ id: number; // DB primary key (<schema>.sessions.id)
25
25
  sessionId: string; // caller-provided session key
26
26
  agentId: string;
27
27
  model: string | null;
@@ -123,7 +123,7 @@ if (result.postProcessError) {
123
123
 
124
124
  - Don't throw as a signal of "enrich should have failed" — enrich is already committed. Use warnings or a separate audit table.
125
125
  - Don't mutate `ctx.normalized`, `ctx.parsedEntities`, or `ctx.warnings`. They're shared-reference with the enrich return; defensive copy if you need to modify.
126
- - Don't rely on postProcess running quickly — it's outside the tx. Long-running work should be fire-and-forget (see Miranda's `setImmediate` consolidation) or queued.
126
+ - Don't rely on postProcess running quickly — it's outside the tx. Long-running work should be fire-and-forget or queued by the consumer.
127
127
 
128
128
  ## What Aquifer guarantees
129
129
 
package/docs/setup.md CHANGED
@@ -49,12 +49,34 @@ Aquifer reads configuration from three sources (in priority order):
49
49
  2. Environment variables (see below)
50
50
  3. Programmatic overrides via `createAquifer()`
51
51
 
52
+ Default public serving mode is `legacy`. Opt into `curated` only when you want `session_recall` and `session_bootstrap` to read active curated memory. `evidence_recall` remains the explicit audit/debug lane in both modes, and rollback is just setting env or config back to `legacy`.
53
+
54
+ ### Example config file
55
+
56
+ ```json
57
+ {
58
+ "db": {
59
+ "url": "postgresql://aquifer:aquifer@localhost:5432/aquifer"
60
+ },
61
+ "memory": {
62
+ "servingMode": "legacy",
63
+ "activeScopeKey": "project:aquifer",
64
+ "activeScopePath": ["global", "project:aquifer"]
65
+ },
66
+ "embed": {
67
+ "baseUrl": "http://localhost:11434/v1",
68
+ "model": "bge-m3"
69
+ }
70
+ }
71
+ ```
72
+
52
73
  ### Minimum env vars for MCP recall
53
74
 
54
75
  ```bash
55
76
  export DATABASE_URL="postgresql://aquifer:aquifer@localhost:5432/aquifer"
56
77
  export AQUIFER_EMBED_BASE_URL="http://localhost:11434/v1"
57
78
  export AQUIFER_EMBED_MODEL="bge-m3"
79
+ export AQUIFER_MEMORY_SERVING_MODE="legacy"
58
80
  ```
59
81
 
60
82
  ### Optional but common
@@ -69,6 +91,12 @@ export AQUIFER_LLM_MODEL="llama3.1"
69
91
 
70
92
  # Knowledge graph
71
93
  export AQUIFER_ENTITIES_ENABLED="true"
94
+
95
+ # Optional curated serving rollout. Default remains legacy.
96
+ export AQUIFER_MEMORY_SERVING_MODE="legacy"
97
+ # export AQUIFER_MEMORY_SERVING_MODE="curated"
98
+ # export AQUIFER_MEMORY_ACTIVE_SCOPE_KEY="project:aquifer"
99
+ # export AQUIFER_MEMORY_ACTIVE_SCOPE_PATH="global,project:aquifer"
72
100
  ```
73
101
 
74
102
  Copy `.env.example` from the repo root for a full annotated list.
@@ -153,7 +181,9 @@ Add to `.claude.json` (project-level) or user-level MCP config:
153
181
  }
154
182
  ```
155
183
 
156
- Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`.
184
+ Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__evidence_recall`, `mcp__aquifer__session_bootstrap`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_feedback`, `mcp__aquifer__feedback_stats`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`.
185
+
186
+ `evidence_recall` is an explicit audit/debug tool. Use `session_recall` for normal memory lookup; broad evidence searches require an audit boundary filter such as `agentId`, `source`, or `dateFrom/dateTo`, unless the caller explicitly opts into unsafe debug mode.
157
187
 
158
188
  ### OpenClaw
159
189
 
@@ -177,10 +207,29 @@ Add to `openclaw.json`:
177
207
  }
178
208
  ```
179
209
 
180
- Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending`.
210
+ Tools materialize as `aquifer__session_recall`, `aquifer__evidence_recall`, `aquifer__session_bootstrap`, `aquifer__session_feedback`, `aquifer__memory_feedback`, `aquifer__feedback_stats`, `aquifer__memory_stats`, `aquifer__memory_pending`.
181
211
 
182
212
  Do **not** use the OpenClaw plugin (`consumers/openclaw-plugin.js`) for tool delivery. The plugin is retained for session capture via `before_reset` only.
183
213
 
214
+ Curated serving rollback is config-only: set `AQUIFER_MEMORY_SERVING_MODE=legacy` and restart the MCP/CLI process. No destructive database rollback is required.
215
+
216
+ ## Release verification gates
217
+
218
+ For the publish-surface checks:
219
+
220
+ ```bash
221
+ node --test test/package-surface.test.js test/mcp-manifest.test.js
222
+ npm pack --dry-run --json
223
+ ```
224
+
225
+ For the real DB-backed MCP integration gate:
226
+
227
+ ```bash
228
+ AQUIFER_TEST_DB_URL="postgresql://..." node --test test/consumer-mcp.integration.test.js
229
+ ```
230
+
231
+ That DB-backed test is the release proof that the stdio MCP server, current MCP manifest, and PostgreSQL path still line up on a live database.
232
+
184
233
  ## Troubleshooting
185
234
 
186
235
  **`error: type "vector" does not exist`** — pgvector is not installed. Use the `pgvector/pgvector` Docker image, or install the extension manually: `CREATE EXTENSION IF NOT EXISTS vector;` (requires superuser).