@shadowforge0/aquifer-memory 1.5.12 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/README.md +84 -73
- package/README_CN.md +676 -0
- package/README_TW.md +684 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +421 -53
- package/consumers/codex-handoff.js +258 -0
- package/consumers/codex.js +1676 -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 +96 -5
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +1 -1
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +27 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +380 -18
- package/core/finalization-review.js +319 -0
- package/core/mcp-manifest.js +52 -2
- package/core/memory-bootstrap.js +200 -0
- package/core/memory-consolidation.js +1590 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +797 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +365 -0
- package/core/storage.js +385 -2
- package/docs/getting-started.md +105 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +92 -2
- package/package.json +25 -11
- 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/codex-recovery.js +672 -0
- package/consumers/miranda/context-inject.js +0 -120
- 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/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
|
// ---------------------------------------------------------------------------
|
|
@@ -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,105 @@
|
|
|
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
|
+
| Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
|
|
87
|
+
| Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
|
|
88
|
+
| Show stats | `npx aquifer stats` |
|
|
89
|
+
| Enrich pending sessions | `npx aquifer backfill` |
|
|
90
|
+
|
|
91
|
+
Timer synthesis is an operator-reviewed candidate workflow. The prompt output
|
|
92
|
+
and summary JSON do not become active curated memory unless the apply step is
|
|
93
|
+
run with `--promote-candidates`.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
## If something fails
|
|
98
|
+
|
|
99
|
+
If `quickstart` cannot connect to PostgreSQL, make sure the database is running and `DATABASE_URL` is correct.
|
|
100
|
+
|
|
101
|
+
If you see `type "vector" does not exist`, `pgvector` is not installed.
|
|
102
|
+
|
|
103
|
+
If recall returns no results, the embedding endpoint is usually unreachable or misconfigured.
|
|
104
|
+
|
|
105
|
+
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
|
|
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,70 @@ 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
|
+
## Operator compaction and timer synthesis
|
|
217
|
+
|
|
218
|
+
Compaction jobs are operator-safe by default. A dry-run plans lifecycle updates
|
|
219
|
+
and candidate output without writing active memory:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx aquifer operator compaction daily --include-synthesis-prompt --json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
If an operator or external model reviews that prompt and returns timer synthesis
|
|
226
|
+
JSON, attach it back to the plan with:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx aquifer operator compaction daily \
|
|
230
|
+
--synthesis-summary-file /tmp/timer-summary.json \
|
|
231
|
+
--apply \
|
|
232
|
+
--promote-candidates \
|
|
233
|
+
--json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The summary file must match the normal structured summary shape, for example:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"summaryText": "Reviewed timer synthesis.",
|
|
241
|
+
"structuredSummary": {
|
|
242
|
+
"states": [
|
|
243
|
+
{ "state": "The reviewed state that should continue into current memory." }
|
|
244
|
+
],
|
|
245
|
+
"decisions": [],
|
|
246
|
+
"open_loops": []
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Without `--promote-candidates`, synthesis output is recorded as candidate
|
|
252
|
+
ledger material only. The prompt and summary file are producer material; active
|
|
253
|
+
curated memory still requires the explicit promotion gate.
|
|
254
|
+
|
|
255
|
+
## Release verification gates
|
|
256
|
+
|
|
257
|
+
For the publish-surface checks:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
node --test test/package-surface.test.js test/mcp-manifest.test.js
|
|
261
|
+
npm pack --dry-run --json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
For the real DB-backed release gate:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
AQUIFER_TEST_DB_URL="postgresql://..." npm run test:release:db
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
That DB-backed test is the release proof that the stdio MCP server, CLI
|
|
271
|
+
consumer, Codex finalization serving path, current MCP manifest, and PostgreSQL
|
|
272
|
+
path still line up on a live database.
|
|
273
|
+
|
|
184
274
|
## Troubleshooting
|
|
185
275
|
|
|
186
276
|
**`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).
|