@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.
- package/.env.example +23 -0
- package/README.md +96 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +374 -39
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +131 -7
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +44 -4
- package/consumers/shared/config.js +28 -0
- package/consumers/shared/factory.js +2 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +53 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +384 -18
- package/core/finalization-review.js +319 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +69 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +456 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +31 -9
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -119
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- 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 ||
|
|
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 (
|
|
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
|
|
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
|
|