@shadowforge0/aquifer-memory 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +374 -39
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
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
  // ---------------------------------------------------------------------------
@@ -666,6 +1044,76 @@ async function recordFeedback(pool, {
666
1044
  }
667
1045
  }
668
1046
 
1047
+ // ---------------------------------------------------------------------------
1048
+ // getFeedbackStats — aggregate feedback and trust score metrics
1049
+ // ---------------------------------------------------------------------------
1050
+
1051
+ async function getFeedbackStats(pool, { schema, tenantId, agentId, dateFrom, dateTo }) {
1052
+ const params = [tenantId];
1053
+ let sessionClause = '';
1054
+ if (agentId) {
1055
+ params.push(agentId);
1056
+ sessionClause += ` AND s.agent_id = $${params.length}`;
1057
+ }
1058
+ if (dateFrom) {
1059
+ params.push(dateFrom);
1060
+ sessionClause += ` AND s.started_at >= $${params.length}::date`;
1061
+ }
1062
+ if (dateTo) {
1063
+ params.push(dateTo);
1064
+ sessionClause += ` AND s.started_at < ($${params.length}::date + interval '1 day')`;
1065
+ }
1066
+
1067
+ const fbQuery = `
1068
+ WITH scoped_sessions AS (
1069
+ SELECT s.id
1070
+ FROM ${qi(schema)}.sessions s
1071
+ WHERE s.tenant_id = $1${sessionClause}
1072
+ )
1073
+ SELECT
1074
+ COUNT(sf.*)::int AS total,
1075
+ COUNT(*) FILTER (WHERE sf.verdict = 'helpful')::int AS helpful,
1076
+ COUNT(*) FILTER (WHERE sf.verdict = 'unhelpful')::int AS unhelpful,
1077
+ COUNT(DISTINCT sf.session_row_id)::int AS rated_sessions
1078
+ FROM scoped_sessions ss
1079
+ LEFT JOIN ${qi(schema)}.session_feedback sf
1080
+ ON sf.session_row_id = ss.id`;
1081
+
1082
+ const ssQuery = `
1083
+ WITH scoped_sessions AS (
1084
+ SELECT s.id
1085
+ FROM ${qi(schema)}.sessions s
1086
+ WHERE s.tenant_id = $1${sessionClause}
1087
+ )
1088
+ SELECT
1089
+ COUNT(scoped_sessions.id)::int AS total_sessions,
1090
+ ROUND(AVG(summary.trust_score)::numeric, 3) AS avg_ts,
1091
+ MIN(summary.trust_score) AS min_ts,
1092
+ MAX(summary.trust_score) AS max_ts
1093
+ FROM scoped_sessions
1094
+ LEFT JOIN ${qi(schema)}.session_summaries summary
1095
+ ON summary.session_row_id = scoped_sessions.id`;
1096
+
1097
+ const [fbResult, ssResult] = await Promise.all([
1098
+ pool.query(fbQuery, params),
1099
+ pool.query(ssQuery, params),
1100
+ ]);
1101
+
1102
+ const fb = fbResult.rows[0];
1103
+ const ss = ssResult.rows[0];
1104
+
1105
+ return {
1106
+ totalFeedback: fb.total,
1107
+ helpfulCount: fb.helpful,
1108
+ unhelpfulCount: fb.unhelpful,
1109
+ feedbackSessions: fb.rated_sessions,
1110
+ totalSessions: ss.total_sessions,
1111
+ trustScoreAvg: (ss.avg_ts !== null && ss.avg_ts !== undefined) ? parseFloat(ss.avg_ts) : 0.5,
1112
+ trustScoreMin: (ss.min_ts !== null && ss.min_ts !== undefined) ? parseFloat(ss.min_ts) : 0.5,
1113
+ trustScoreMax: (ss.max_ts !== null && ss.max_ts !== undefined) ? parseFloat(ss.max_ts) : 0.5,
1114
+ };
1115
+ }
1116
+
669
1117
  // ---------------------------------------------------------------------------
670
1118
  // Exports
671
1119
  // ---------------------------------------------------------------------------
@@ -678,9 +1126,15 @@ module.exports = {
678
1126
  getMessages,
679
1127
  searchSessions,
680
1128
  recordAccess,
1129
+ upsertSessionFinalization,
1130
+ getSessionFinalization,
1131
+ updateSessionFinalizationStatus,
1132
+ listSessionFinalizations,
1133
+ upsertFinalizationCandidates,
681
1134
  extractUserTurns,
682
1135
  upsertTurnEmbeddings,
683
1136
  searchTurnEmbeddings,
684
1137
  searchSummaryEmbeddings,
685
1138
  recordFeedback,
1139
+ getFeedbackStats,
686
1140
  };
@@ -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