@shadowforge0/aquifer-memory 1.8.1 → 1.9.1
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 +1 -0
- package/README.md +82 -26
- package/README_CN.md +33 -23
- package/README_TW.md +25 -24
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +587 -33
- package/consumers/codex-active-checkpoint.js +3 -1
- package/consumers/codex-current-memory.js +10 -6
- package/consumers/codex.js +6 -3
- package/consumers/default/daily-entries.js +2 -2
- package/consumers/default/index.js +40 -30
- package/consumers/default/prompts/summary.js +2 -2
- package/consumers/mcp.js +56 -46
- package/consumers/openclaw-ext/index.js +65 -7
- package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
- package/consumers/openclaw-ext/package.json +1 -1
- package/consumers/openclaw-install.js +326 -0
- package/consumers/openclaw-plugin.js +105 -24
- package/consumers/shared/compat-recall.js +101 -0
- package/consumers/shared/config.js +2 -0
- package/consumers/shared/openclaw-product-tools.js +130 -0
- package/consumers/shared/recall-format.js +2 -2
- package/core/aquifer.js +553 -41
- package/core/backends/local.js +169 -1
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/finalization-review.js +88 -42
- package/core/interface.js +629 -0
- package/core/mcp-manifest.js +11 -3
- package/core/memory-bootstrap.js +25 -27
- package/core/memory-consolidation.js +564 -42
- package/core/memory-explain.js +593 -0
- package/core/memory-promotion.js +392 -55
- package/core/memory-recall.js +75 -71
- package/core/memory-records.js +107 -108
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/memory-type-policy.js +298 -0
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +22 -0
- package/core/session-checkpoint-producer.js +3 -1
- package/core/session-checkpoints.js +1 -1
- package/core/session-finalization.js +78 -3
- package/core/storage.js +124 -8
- package/docs/getting-started.md +50 -4
- package/docs/setup.md +163 -24
- package/package.json +5 -4
- package/schema/004-completion.sql +4 -4
- package/schema/010-v1-finalization-review.sql +72 -0
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/schema/020-v1-assistant-shaping-memory.sql +30 -0
- package/scripts/backfill-canonical-key.js +1 -1
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
- package/scripts/diagnose-fts-zh.js +1 -1
- package/scripts/extract-insights-from-recent-sessions.js +4 -4
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const { sanitizeSummaryResult } = require('./memory-safety-gate');
|
|
5
|
+
const { assistantShapingPromptLines } = require('./memory-type-policy');
|
|
5
6
|
const { buildScopeEnvelope, getScopeByEnvelopeId } = require('./scope-attribution');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_POLICY_VERSION = 'session_checkpoint_producer_v1';
|
|
8
9
|
const DEFAULT_COVERAGE_COORDINATE_SYSTEM = 'codex_sanitized_view_v1';
|
|
9
|
-
const STRUCTURED_SUMMARY_SHAPE = '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}';
|
|
10
|
+
const STRUCTURED_SUMMARY_SHAPE = '{"summaryText":"...","structuredSummary":{"assistant_shaping":[],"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}';
|
|
10
11
|
|
|
11
12
|
function stableJson(value) {
|
|
12
13
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
@@ -318,6 +319,7 @@ function buildCheckpointSynthesisPrompt(synthesisInput = {}, opts = {}) {
|
|
|
318
319
|
'Return compact JSON with this shape:',
|
|
319
320
|
STRUCTURED_SUMMARY_SHAPE,
|
|
320
321
|
`Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
|
|
322
|
+
...assistantShapingPromptLines(),
|
|
321
323
|
'Preserve the coverage object so handoff can skip only the already-covered transcript range.',
|
|
322
324
|
'',
|
|
323
325
|
'<checkpoint_synthesis_input>',
|
|
@@ -44,7 +44,7 @@ function parsePositiveInt(value, fallback = 10, max = 200) {
|
|
|
44
44
|
function compactStructuredSummary(value = {}) {
|
|
45
45
|
if (!value || typeof value !== 'object') return {};
|
|
46
46
|
const out = {};
|
|
47
|
-
for (const key of ['facts', 'decisions', 'open_loops', 'openLoops', 'preferences', 'constraints', 'conclusions', 'entity_notes', 'entityNotes', 'states']) {
|
|
47
|
+
for (const key of ['assistant_shaping', 'assistant_shaping_memories', 'facts', 'decisions', 'open_loops', 'openLoops', 'preferences', 'constraints', 'conclusions', 'entity_notes', 'entityNotes', 'states']) {
|
|
48
48
|
const rows = Array.isArray(value[key]) ? value[key] : [];
|
|
49
49
|
if (rows.length > 0) out[key] = rows.slice(0, 8);
|
|
50
50
|
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const storage = require('./storage');
|
|
5
5
|
const { createMemoryRecords } = require('./memory-records');
|
|
6
|
-
const { createMemoryPromotion } = require('./memory-promotion');
|
|
6
|
+
const { createMemoryPromotion, sanitizePromotionCandidate } = require('./memory-promotion');
|
|
7
7
|
const { sanitizeSummaryResult } = require('./memory-safety-gate');
|
|
8
8
|
const { buildFinalizationReview, buildSessionStartContext } = require('./finalization-review');
|
|
9
|
+
const { buildFinalizationInspection } = require('./finalization-inspector');
|
|
9
10
|
|
|
10
11
|
function qi(identifier) { return `"${identifier}"`; }
|
|
11
12
|
|
|
@@ -15,6 +16,13 @@ function requireField(obj, field) {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function finalizationReadError(error) {
|
|
20
|
+
if (error && error.code === '42P01') {
|
|
21
|
+
return new Error('Finalization observability tables are not available. Run `aquifer migrate`, then retry this read-only command.');
|
|
22
|
+
}
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
function hasStructuredContent(value) {
|
|
19
27
|
return value && typeof value === 'object' && Object.keys(value).length > 0;
|
|
20
28
|
}
|
|
@@ -190,7 +198,73 @@ function createSessionFinalization({
|
|
|
190
198
|
|
|
191
199
|
async function list(input = {}) {
|
|
192
200
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
193
|
-
|
|
201
|
+
try {
|
|
202
|
+
return await storage.listSessionFinalizations(pool, input, { schema, tenantId });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw finalizationReadError(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function inspect(input = {}) {
|
|
209
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
210
|
+
let row = null;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
if (input.id) {
|
|
214
|
+
row = await storage.getSessionFinalizationById(pool, {
|
|
215
|
+
tenantId,
|
|
216
|
+
id: input.id,
|
|
217
|
+
}, { schema, tenantId });
|
|
218
|
+
} else {
|
|
219
|
+
requireField(input, 'sessionId');
|
|
220
|
+
requireField(input, 'agentId');
|
|
221
|
+
requireField(input, 'source');
|
|
222
|
+
const phase = input.phase || 'curated_memory_v1';
|
|
223
|
+
if (input.transcriptHash) {
|
|
224
|
+
row = await storage.getSessionFinalization(pool, {
|
|
225
|
+
tenantId,
|
|
226
|
+
sessionId: input.sessionId,
|
|
227
|
+
agentId: input.agentId,
|
|
228
|
+
source: input.source,
|
|
229
|
+
transcriptHash: input.transcriptHash,
|
|
230
|
+
phase,
|
|
231
|
+
}, { schema, tenantId });
|
|
232
|
+
} else {
|
|
233
|
+
const rows = await storage.listSessionFinalizations(pool, {
|
|
234
|
+
tenantId,
|
|
235
|
+
sessionId: input.sessionId,
|
|
236
|
+
agentId: input.agentId,
|
|
237
|
+
source: input.source,
|
|
238
|
+
phase,
|
|
239
|
+
limit: 2,
|
|
240
|
+
}, { schema, tenantId });
|
|
241
|
+
if (rows.length > 1) {
|
|
242
|
+
const matches = rows.map(match => {
|
|
243
|
+
const hash = match.transcript_hash ? String(match.transcript_hash).slice(0, 12) : '?';
|
|
244
|
+
const updatedAt = match.updated_at || '?';
|
|
245
|
+
return `#${match.id} status=${match.status} phase=${match.phase} hash=${hash} updated=${updatedAt}`;
|
|
246
|
+
}).join('; ');
|
|
247
|
+
throw new Error(`Multiple finalizations matched. Add --transcript-hash or --id to inspect one row. Matches: ${matches}`);
|
|
248
|
+
}
|
|
249
|
+
row = rows[0] || null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!row) throw new Error('Finalization not found');
|
|
254
|
+
const [candidates, lineage] = await Promise.all([
|
|
255
|
+
storage.listFinalizationCandidates(pool, {
|
|
256
|
+
tenantId,
|
|
257
|
+
finalizationId: row.id,
|
|
258
|
+
}, { schema, tenantId }),
|
|
259
|
+
storage.getFinalizationLineageSummary(pool, {
|
|
260
|
+
tenantId,
|
|
261
|
+
finalizationId: row.id,
|
|
262
|
+
}, { schema, tenantId }),
|
|
263
|
+
]);
|
|
264
|
+
return buildFinalizationInspection(row, candidates, lineage);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw finalizationReadError(error);
|
|
267
|
+
}
|
|
194
268
|
}
|
|
195
269
|
|
|
196
270
|
async function updateStatus(input = {}) {
|
|
@@ -323,7 +397,7 @@ function createSessionFinalization({
|
|
|
323
397
|
authority: input.authority || 'verified_summary',
|
|
324
398
|
evidenceRefs,
|
|
325
399
|
});
|
|
326
|
-
const candidates = decorateCandidates(rawCandidates, input);
|
|
400
|
+
const candidates = decorateCandidates(rawCandidates.map(sanitizePromotionCandidate), input);
|
|
327
401
|
const candidateEnvelope = buildCandidateEnvelope(input, candidates, {
|
|
328
402
|
transcriptHash: base.transcriptHash,
|
|
329
403
|
});
|
|
@@ -436,6 +510,7 @@ function createSessionFinalization({
|
|
|
436
510
|
createTask,
|
|
437
511
|
get,
|
|
438
512
|
list,
|
|
513
|
+
inspect,
|
|
439
514
|
updateStatus,
|
|
440
515
|
finalizeSession,
|
|
441
516
|
};
|
package/core/storage.js
CHANGED
|
@@ -614,6 +614,20 @@ async function getSessionFinalization(pool, input = {}, { schema, tenantId: defa
|
|
|
614
614
|
return result.rows[0] || null;
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
+
async function getSessionFinalizationById(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
618
|
+
requireField(input, 'id');
|
|
619
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
620
|
+
const result = await pool.query(
|
|
621
|
+
`SELECT *
|
|
622
|
+
FROM ${qi(schema)}.session_finalizations
|
|
623
|
+
WHERE tenant_id = $1
|
|
624
|
+
AND id = $2
|
|
625
|
+
LIMIT 1`,
|
|
626
|
+
[tenantId, input.id]
|
|
627
|
+
);
|
|
628
|
+
return result.rows[0] || null;
|
|
629
|
+
}
|
|
630
|
+
|
|
617
631
|
async function updateSessionFinalizationStatus(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
618
632
|
const status = normalizeFinalizationStatus(input.status);
|
|
619
633
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
@@ -660,38 +674,137 @@ async function updateSessionFinalizationStatus(pool, input = {}, { schema, tenan
|
|
|
660
674
|
|
|
661
675
|
async function listSessionFinalizations(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
662
676
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
677
|
+
const table = `${qi(schema)}.session_finalizations`;
|
|
663
678
|
const params = [tenantId];
|
|
664
|
-
const where = [
|
|
679
|
+
const where = [`${table}.tenant_id = $1`];
|
|
665
680
|
if (input.host) {
|
|
666
681
|
params.push(input.host);
|
|
667
|
-
where.push(
|
|
682
|
+
where.push(`${table}.host = $${params.length}`);
|
|
668
683
|
}
|
|
669
684
|
if (input.status) {
|
|
670
685
|
const statuses = Array.isArray(input.status) ? input.status : [input.status];
|
|
671
686
|
for (const status of statuses) normalizeFinalizationStatus(status);
|
|
672
687
|
params.push(statuses);
|
|
673
|
-
where.push(
|
|
688
|
+
where.push(`${table}.status = ANY($${params.length}::text[])`);
|
|
674
689
|
}
|
|
675
690
|
if (input.agentId) {
|
|
676
691
|
params.push(input.agentId);
|
|
677
|
-
where.push(
|
|
692
|
+
where.push(`${table}.agent_id = $${params.length}`);
|
|
678
693
|
}
|
|
679
694
|
if (input.source) {
|
|
680
695
|
params.push(input.source);
|
|
681
|
-
where.push(
|
|
696
|
+
where.push(`${table}.source = $${params.length}`);
|
|
697
|
+
}
|
|
698
|
+
if (input.sessionId) {
|
|
699
|
+
params.push(input.sessionId);
|
|
700
|
+
where.push(`${table}.session_id = $${params.length}`);
|
|
701
|
+
}
|
|
702
|
+
if (input.transcriptHash) {
|
|
703
|
+
params.push(input.transcriptHash);
|
|
704
|
+
where.push(`${table}.transcript_hash = $${params.length}`);
|
|
705
|
+
}
|
|
706
|
+
if (input.phase) {
|
|
707
|
+
params.push(input.phase);
|
|
708
|
+
where.push(`${table}.phase = $${params.length}`);
|
|
709
|
+
}
|
|
710
|
+
if (input.mode) {
|
|
711
|
+
params.push(normalizeFinalizationMode(input.mode));
|
|
712
|
+
where.push(`${table}.mode = $${params.length}`);
|
|
682
713
|
}
|
|
683
714
|
params.push(Math.max(1, Math.min(200, input.limit || 50)));
|
|
684
715
|
const result = await pool.query(
|
|
685
|
-
`SELECT
|
|
686
|
-
|
|
716
|
+
`SELECT
|
|
717
|
+
${table}.id,
|
|
718
|
+
${table}.tenant_id,
|
|
719
|
+
${table}.source,
|
|
720
|
+
${table}.host,
|
|
721
|
+
${table}.agent_id,
|
|
722
|
+
${table}.session_id,
|
|
723
|
+
${table}.transcript_hash,
|
|
724
|
+
${table}.phase,
|
|
725
|
+
${table}.mode,
|
|
726
|
+
${table}.status,
|
|
727
|
+
${table}.finalizer_model,
|
|
728
|
+
${table}.scope_kind,
|
|
729
|
+
${table}.scope_key,
|
|
730
|
+
${table}.context_key,
|
|
731
|
+
${table}.topic_key,
|
|
732
|
+
${table}.candidate_envelope_hash,
|
|
733
|
+
${table}.candidate_envelope_version,
|
|
734
|
+
${table}.error,
|
|
735
|
+
${table}.claimed_at,
|
|
736
|
+
${table}.finalized_at,
|
|
737
|
+
${table}.created_at,
|
|
738
|
+
${table}.updated_at,
|
|
739
|
+
(
|
|
740
|
+
SELECT COUNT(*)::int
|
|
741
|
+
FROM ${qi(schema)}.finalization_candidates fc
|
|
742
|
+
WHERE fc.tenant_id = ${table}.tenant_id
|
|
743
|
+
AND fc.finalization_id = ${table}.id
|
|
744
|
+
) AS candidate_count
|
|
745
|
+
FROM ${table}
|
|
687
746
|
WHERE ${where.join(' AND ')}
|
|
688
|
-
ORDER BY updated_at DESC, id DESC
|
|
747
|
+
ORDER BY ${table}.updated_at DESC, ${table}.id DESC
|
|
689
748
|
LIMIT $${params.length}`,
|
|
690
749
|
params
|
|
691
750
|
);
|
|
692
751
|
return result.rows;
|
|
693
752
|
}
|
|
694
753
|
|
|
754
|
+
async function getFinalizationLineageSummary(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
755
|
+
requireField(input, 'finalizationId');
|
|
756
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
757
|
+
const result = await pool.query(
|
|
758
|
+
`SELECT
|
|
759
|
+
COALESCE((
|
|
760
|
+
SELECT array_agg(id ORDER BY id)
|
|
761
|
+
FROM ${qi(schema)}.memory_records
|
|
762
|
+
WHERE tenant_id = $1
|
|
763
|
+
AND created_by_finalization_id = $2
|
|
764
|
+
), ARRAY[]::bigint[]) AS memory_record_ids,
|
|
765
|
+
COALESCE((
|
|
766
|
+
SELECT array_agg(id ORDER BY id)
|
|
767
|
+
FROM ${qi(schema)}.fact_assertions_v1
|
|
768
|
+
WHERE tenant_id = $1
|
|
769
|
+
AND created_by_finalization_id = $2
|
|
770
|
+
), ARRAY[]::bigint[]) AS fact_assertion_ids,
|
|
771
|
+
COALESCE((
|
|
772
|
+
SELECT COUNT(*)::int
|
|
773
|
+
FROM ${qi(schema)}.evidence_refs
|
|
774
|
+
WHERE tenant_id = $1
|
|
775
|
+
AND created_by_finalization_id = $2
|
|
776
|
+
), 0) AS evidence_ref_count,
|
|
777
|
+
COALESCE((
|
|
778
|
+
SELECT COUNT(*)::int
|
|
779
|
+
FROM ${qi(schema)}.evidence_items
|
|
780
|
+
WHERE tenant_id = $1
|
|
781
|
+
AND created_by_finalization_id = $2
|
|
782
|
+
), 0) AS evidence_item_count`,
|
|
783
|
+
[tenantId, input.finalizationId]
|
|
784
|
+
);
|
|
785
|
+
const row = result.rows[0] || {};
|
|
786
|
+
return {
|
|
787
|
+
memoryRecordIds: row.memory_record_ids || [],
|
|
788
|
+
factAssertionIds: row.fact_assertion_ids || [],
|
|
789
|
+
evidenceRefCount: row.evidence_ref_count || 0,
|
|
790
|
+
evidenceItemCount: row.evidence_item_count || 0,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function listFinalizationCandidates(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
795
|
+
requireField(input, 'finalizationId');
|
|
796
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
797
|
+
const result = await pool.query(
|
|
798
|
+
`SELECT *
|
|
799
|
+
FROM ${qi(schema)}.finalization_candidates
|
|
800
|
+
WHERE tenant_id = $1
|
|
801
|
+
AND finalization_id = $2
|
|
802
|
+
ORDER BY candidate_index ASC, id ASC`,
|
|
803
|
+
[tenantId, input.finalizationId]
|
|
804
|
+
);
|
|
805
|
+
return result.rows;
|
|
806
|
+
}
|
|
807
|
+
|
|
695
808
|
function candidateText(candidate = {}) {
|
|
696
809
|
if (typeof candidate === 'string') return candidate.trim();
|
|
697
810
|
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : null;
|
|
@@ -1236,8 +1349,11 @@ module.exports = {
|
|
|
1236
1349
|
recordAccess,
|
|
1237
1350
|
upsertSessionFinalization,
|
|
1238
1351
|
getSessionFinalization,
|
|
1352
|
+
getSessionFinalizationById,
|
|
1239
1353
|
updateSessionFinalizationStatus,
|
|
1240
1354
|
listSessionFinalizations,
|
|
1355
|
+
getFinalizationLineageSummary,
|
|
1356
|
+
listFinalizationCandidates,
|
|
1241
1357
|
upsertCheckpointRun,
|
|
1242
1358
|
updateCheckpointRunStatus,
|
|
1243
1359
|
listCheckpointRuns,
|
package/docs/getting-started.md
CHANGED
|
@@ -73,24 +73,57 @@ Or run the server directly:
|
|
|
73
73
|
DATABASE_URL=... EMBED_PROVIDER=ollama npx aquifer mcp
|
|
74
74
|
```
|
|
75
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
|
|
76
|
+
For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want compatibility `session_recall` and `session_bootstrap` to serve active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
|
|
77
|
+
|
|
78
|
+
## Connect OpenClaw
|
|
79
|
+
|
|
80
|
+
For OpenClaw, use the host installer instead of wiring each consumer by hand:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
84
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The installer links and enables the optional OpenClaw extension, updates `plugins.load.paths` / `plugins.entries["aquifer-memory"]`, and points `mcp.servers.aquifer` at the package's `consumers/mcp.js` while preserving existing MCP env values. For source checkouts, `scripts/install-openclaw.sh` first installs the current package into `$OPENCLAW_HOME/node_modules`, then runs the same packaged installer.
|
|
88
|
+
|
|
89
|
+
Run with `--dry-run --json` to verify the active package version, extension path, plugin config, and MCP target without changing files.
|
|
77
90
|
|
|
78
91
|
## Most common commands
|
|
79
92
|
|
|
80
93
|
| Goal | Command |
|
|
81
94
|
|---|---|
|
|
82
95
|
| Verify setup | `npx aquifer quickstart` |
|
|
96
|
+
| Run read-only diagnostics | `npx aquifer doctor --json` |
|
|
83
97
|
| Start MCP server | `npx aquifer mcp` |
|
|
98
|
+
| Install/update OpenClaw wiring | `node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"` |
|
|
84
99
|
| Search memory | `npx aquifer recall "auth middleware"` |
|
|
100
|
+
| Explain current-memory selection | `npx aquifer explain bootstrap --active-scope-key project:aquifer --json` |
|
|
101
|
+
| Inspect finalization ledger | `npx aquifer finalization list --status finalized --json` |
|
|
102
|
+
| Inspect operator ledgers | `npx aquifer operator status --json` |
|
|
103
|
+
| Review current-memory feedback issues | `npx aquifer review queue --scope-key project:aquifer --json` |
|
|
85
104
|
| Plan curated compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
|
|
86
105
|
| Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
|
|
87
106
|
| Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
|
|
88
|
-
|
|
|
89
|
-
|
|
|
107
|
+
| Check memory readiness | `npx aquifer stats` |
|
|
108
|
+
| Check saved-content preparation | `npx aquifer backlog --json` |
|
|
109
|
+
| Prepare saved content | `npx aquifer backfill` |
|
|
110
|
+
| Resolve a reviewed memory issue | `npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json` |
|
|
111
|
+
|
|
112
|
+
`stats`, `backlog`, MCP `memory_stats`, and MCP `memory_pending` default to the
|
|
113
|
+
same public status surface: `Aquifer status` or `Saved content status`, plus
|
|
114
|
+
`Available`, `Attention`, and `Action` where relevant. Use CLI `--diagnostics`
|
|
115
|
+
or MCP `diagnostics: true` when a host needs raw counters, buckets, guidance,
|
|
116
|
+
or samples.
|
|
90
117
|
|
|
91
118
|
Timer synthesis is an operator-reviewed candidate workflow. The prompt output
|
|
92
119
|
and summary JSON do not become active curated memory unless the apply step is
|
|
93
|
-
run with `--promote-candidates`.
|
|
120
|
+
run with `--promote-candidates`. Daily/weekly/monthly aggregate proposals are
|
|
121
|
+
source-rollup material for review and lineage only; normal promotion is blocked
|
|
122
|
+
until a reviewed synthesis summary is attached. Reviewed synthesis items must
|
|
123
|
+
also pass the temporal distillation standard: each item needs `mergeKey`,
|
|
124
|
+
`scopeClass`, `durability`, `promotionTarget`, and `sourceCanonicalKeys` from
|
|
125
|
+
the prompt's `sourceCurrentMemory`, and runtime state also needs `staleAfter`
|
|
126
|
+
or `validTo`.
|
|
94
127
|
|
|
95
128
|
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
129
|
|
|
@@ -102,4 +135,17 @@ If you see `type "vector" does not exist`, `pgvector` is not installed.
|
|
|
102
135
|
|
|
103
136
|
If recall returns no results, the embedding endpoint is usually unreachable or misconfigured.
|
|
104
137
|
|
|
138
|
+
For continuity or operator questions, use the read-only governance commands
|
|
139
|
+
first: `doctor`, `finalization list|inspect`, `explain bootstrap|memory`, and
|
|
140
|
+
`operator status|inspect`. Use `review queue|inspect` when curated memory
|
|
141
|
+
feedback has marked visible current-memory rows as `incorrect`, `stale`,
|
|
142
|
+
`scope_mismatch`, or similar issue types. These commands diagnose the DB and
|
|
143
|
+
serving state without changing memory truth, finalization status, MCP tools, or
|
|
144
|
+
operator leases. The review surface does not print raw transcripts, feedback
|
|
145
|
+
notes, feedback metadata, or memory payloads; `review queue|inspect` still uses
|
|
146
|
+
the normal Aquifer migration gate because it depends on the resolution ledger
|
|
147
|
+
schema. After human review, `review resolve` appends a resolution ledger row
|
|
148
|
+
only; it does not edit current memory truth, and newer issue feedback makes the
|
|
149
|
+
item show up in the queue again.
|
|
150
|
+
|
|
105
151
|
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).
|
package/docs/setup.md
CHANGED
|
@@ -49,7 +49,7 @@ 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. `
|
|
52
|
+
Default public serving mode is `legacy`. Opt into `curated` only when you want compatibility `session_recall` and `session_bootstrap` to read active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
|
|
53
53
|
|
|
54
54
|
Backend profiles are explicit. `postgres` is the full backend and remains required for semantic recall, migrations, curated memory, and operator workflows. `local` is a zero-config starter profile with JSON-file persistence, raw session writes, lexical recall, bootstrap, stats, and export. It is intentionally degraded and does not create embeddings or run operator workflows:
|
|
55
55
|
|
|
@@ -76,7 +76,8 @@ AQUIFER_BACKEND=local npx aquifer backend-info --json
|
|
|
76
76
|
"memory": {
|
|
77
77
|
"servingMode": "legacy",
|
|
78
78
|
"activeScopeKey": "project:aquifer",
|
|
79
|
-
"activeScopePath": ["global", "project:aquifer"]
|
|
79
|
+
"activeScopePath": ["global", "project:aquifer"],
|
|
80
|
+
"allowedScopeKeys": ["global", "project:aquifer"]
|
|
80
81
|
},
|
|
81
82
|
"embed": {
|
|
82
83
|
"baseUrl": "http://localhost:11434/v1",
|
|
@@ -113,6 +114,7 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
|
|
|
113
114
|
# export AQUIFER_MEMORY_SERVING_MODE="curated"
|
|
114
115
|
# export AQUIFER_MEMORY_ACTIVE_SCOPE_KEY="project:aquifer"
|
|
115
116
|
# export AQUIFER_MEMORY_ACTIVE_SCOPE_PATH="global,project:aquifer"
|
|
117
|
+
# export AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS="global,project:aquifer"
|
|
116
118
|
|
|
117
119
|
# Optional Codex active-session checkpoint heartbeat policy.
|
|
118
120
|
# Command flags still take precedence over these env vars.
|
|
@@ -123,6 +125,34 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
|
|
|
123
125
|
|
|
124
126
|
Copy `.env.example` from the repo root for a full annotated list.
|
|
125
127
|
|
|
128
|
+
### Curated scope contract
|
|
129
|
+
|
|
130
|
+
Current-memory serving uses two separate scope concepts. `activeScopePath` is
|
|
131
|
+
the ordered inheritance path used by bootstrap, recall, and explain. Broader
|
|
132
|
+
entries can feed defaults into narrower scopes only when they appear in that
|
|
133
|
+
path. `allowedScopeKeys` is the caller boundary. If a runtime request asks for
|
|
134
|
+
an `activeScopeKey`, `activeScopePath`, `scopeKey`, or resolved `scopeId`
|
|
135
|
+
outside `allowedScopeKeys`, Aquifer rejects the request before reading current
|
|
136
|
+
memory rows. When `activeScopePath` is omitted, it defaults to `global` plus the
|
|
137
|
+
configured `activeScopeKey`, or to `global` alone when no active scope is
|
|
138
|
+
configured. When `allowedScopeKeys` is omitted, it defaults to that active scope
|
|
139
|
+
path.
|
|
140
|
+
|
|
141
|
+
Serving inheritance is deterministic:
|
|
142
|
+
|
|
143
|
+
- `defaultable`: broader rows are defaults, narrower rows with the same
|
|
144
|
+
`canonicalKey` win.
|
|
145
|
+
- `exclusive`: currently an explicit narrowest-wins alias for serving. It keeps
|
|
146
|
+
the producer label visible in explain output but does not add a separate
|
|
147
|
+
runtime behavior.
|
|
148
|
+
- `additive`: all applicable rows are merged after scope filtering.
|
|
149
|
+
- `non_inheritable`: only the exact active scope can serve the row.
|
|
150
|
+
|
|
151
|
+
`aquifer explain bootstrap|memory --json` returns selected/excluded reasons
|
|
152
|
+
and a `scopeInheritance` block for each row. Non-selected rows retain safe
|
|
153
|
+
identity and reason fields but redact `canonicalKey`, `title`, and `summary`,
|
|
154
|
+
so explain remains diagnostic instead of becoming a cross-scope content probe.
|
|
155
|
+
|
|
126
156
|
## Step 4: Verify everything works
|
|
127
157
|
|
|
128
158
|
```bash
|
|
@@ -141,6 +171,25 @@ npx aquifer mcp
|
|
|
141
171
|
|
|
142
172
|
The server starts on stdio and waits for MCP client connections. There is no visible output on success — the server is ready when the process stays running without error.
|
|
143
173
|
|
|
174
|
+
### OpenClaw host install/update
|
|
175
|
+
|
|
176
|
+
OpenClaw should be updated through one host-level installer, not by editing each Aquifer consumer path separately:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
180
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The command links `$OPENCLAW_HOME/extensions/aquifer-memory` to the package's OpenClaw extension, enables `plugins.entries["aquifer-memory"]`, adds the extension to `plugins.load.paths`, and updates `openclaw.json` so `mcp.servers.aquifer` runs the same package's `consumers/mcp.js`. Existing `mcp.servers.aquifer.env` values are preserved, and `openclaw.json` is backed up before writing.
|
|
184
|
+
|
|
185
|
+
For a source checkout, run the repo wrapper:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
bash scripts/install-openclaw.sh "$OPENCLAW_HOME"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
That wrapper installs the current package into `$OPENCLAW_HOME/node_modules` before delegating to the packaged installer. Use `--dry-run --json` to inspect the package version, extension link, plugin config, and MCP target without changing files. `--link-current-package` is a development-only escape hatch for wiring a source checkout directly.
|
|
192
|
+
|
|
144
193
|
### Verify with the library API (optional)
|
|
145
194
|
|
|
146
195
|
If you want to test the library directly instead of the CLI:
|
|
@@ -203,33 +252,25 @@ Add to `.claude.json` (project-level) or user-level MCP config:
|
|
|
203
252
|
}
|
|
204
253
|
```
|
|
205
254
|
|
|
206
|
-
Tools appear as `
|
|
255
|
+
Tools appear as `mcp__aquifer__memory_recall`, `mcp__aquifer__historical_recall`, `mcp__aquifer__session_recall`, `mcp__aquifer__evidence_recall`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_feedback`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`, `mcp__aquifer__feedback_stats`, `mcp__aquifer__session_bootstrap`.
|
|
207
256
|
|
|
208
|
-
`
|
|
257
|
+
Use `memory_recall` for explicit current-memory lookup, `historical_recall` for older session detail, and compatibility `session_recall` when the host should follow the configured serving mode. `evidence_recall` is the explicit audit/debug tool; broad evidence searches require an audit boundary filter such as `agentId`, `source`, or `dateFrom/dateTo`, unless the caller explicitly opts into unsafe debug mode.
|
|
209
258
|
|
|
210
259
|
### OpenClaw
|
|
211
260
|
|
|
212
|
-
|
|
261
|
+
Install or update Aquifer inside the OpenClaw host root, then let the installer
|
|
262
|
+
wire both the MCP server and the optional extension from the same package root:
|
|
213
263
|
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"servers": {
|
|
218
|
-
"aquifer": {
|
|
219
|
-
"command": "node",
|
|
220
|
-
"args": ["/absolute/path/to/aquifer/consumers/mcp.js"],
|
|
221
|
-
"env": {
|
|
222
|
-
"DATABASE_URL": "postgresql://...",
|
|
223
|
-
"AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
|
|
224
|
-
"AQUIFER_EMBED_MODEL": "bge-m3"
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
264
|
+
```bash
|
|
265
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
266
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
230
267
|
```
|
|
231
268
|
|
|
232
|
-
|
|
269
|
+
The installer preserves existing `mcp.servers.aquifer.env` values and backs up
|
|
270
|
+
`openclaw.json` before writing. Use `--dry-run --json` to inspect package
|
|
271
|
+
version, MCP target, and extension link without changing files.
|
|
272
|
+
|
|
273
|
+
Tools materialize as `aquifer__memory_recall`, `aquifer__historical_recall`, `aquifer__session_recall`, `aquifer__evidence_recall`, `aquifer__session_feedback`, `aquifer__memory_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending`, `aquifer__feedback_stats`, `aquifer__session_bootstrap`.
|
|
233
274
|
|
|
234
275
|
Do **not** use the OpenClaw plugin (`consumers/openclaw-plugin.js`) for tool delivery. The plugin is retained for session capture via `before_reset` only.
|
|
235
276
|
|
|
@@ -262,7 +303,14 @@ The summary file must match the normal structured summary shape, for example:
|
|
|
262
303
|
"summaryText": "Reviewed timer synthesis.",
|
|
263
304
|
"structuredSummary": {
|
|
264
305
|
"states": [
|
|
265
|
-
{
|
|
306
|
+
{
|
|
307
|
+
"state": "The reviewed state that should continue into current memory.",
|
|
308
|
+
"mergeKey": "product-state:example",
|
|
309
|
+
"scopeClass": "product_memory",
|
|
310
|
+
"durability": "durable",
|
|
311
|
+
"promotionTarget": "project_current_memory",
|
|
312
|
+
"sourceCanonicalKeys": ["memory:canonical:key-from-prompt"]
|
|
313
|
+
}
|
|
266
314
|
],
|
|
267
315
|
"decisions": [],
|
|
268
316
|
"open_loops": []
|
|
@@ -270,9 +318,100 @@ The summary file must match the normal structured summary shape, for example:
|
|
|
270
318
|
}
|
|
271
319
|
```
|
|
272
320
|
|
|
321
|
+
Assistant behavior memory uses the same reviewed synthesis path, but the
|
|
322
|
+
serving text must be behavior-level rather than project-specific wording:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"summaryText": "Reviewed assistant behavior synthesis.",
|
|
327
|
+
"structuredSummary": {
|
|
328
|
+
"assistant_shaping": [
|
|
329
|
+
{
|
|
330
|
+
"guidance": "The user dislikes paying time and token cost for rediscovery. On continuation work, start from known state before broad exploration.",
|
|
331
|
+
"shapingKind": "tool_routing",
|
|
332
|
+
"servingImpact": "changes_tool_routing",
|
|
333
|
+
"userRelevance": "This reduces repeated context work and keeps collaboration moving.",
|
|
334
|
+
"temporalSupport": { "observationCount": 3 },
|
|
335
|
+
"abstraction": {
|
|
336
|
+
"languageLevel": "user_behavior",
|
|
337
|
+
"appliesBeyondSource": true,
|
|
338
|
+
"sourceBound": false,
|
|
339
|
+
"principle": "The user values avoiding repeated context work."
|
|
340
|
+
},
|
|
341
|
+
"mergeKey": "mk_hates_repeating_context_work",
|
|
342
|
+
"scopeClass": "assistant_behavior",
|
|
343
|
+
"durability": "durable",
|
|
344
|
+
"promotionTarget": "assistant_behavior_memory",
|
|
345
|
+
"sourceCanonicalKeys": ["memory:canonical:key-from-prompt"]
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
273
352
|
Without `--promote-candidates`, synthesis output is recorded as candidate
|
|
274
353
|
ledger material only. The prompt and summary file are producer material; active
|
|
275
|
-
curated memory still requires the explicit promotion gate.
|
|
354
|
+
curated memory still requires the explicit promotion gate. The deterministic
|
|
355
|
+
aggregate proposals from a daily/weekly/monthly dry-run are source-rollup
|
|
356
|
+
review material, not active temporal memory; the normal product path blocks
|
|
357
|
+
their promotion unless a reviewed synthesis summary is attached.
|
|
358
|
+
|
|
359
|
+
Reviewed synthesis items must pass the temporal distillation standard before
|
|
360
|
+
they become promotable candidates. Each item needs `mergeKey`, `scopeClass`,
|
|
361
|
+
`durability`, `promotionTarget`, and `sourceCanonicalKeys` that point to
|
|
362
|
+
canonical keys from the prompt's `sourceCurrentMemory`; runtime state also needs
|
|
363
|
+
`staleAfter` or `validTo`. Workspace/operator policy, transient material,
|
|
364
|
+
runtime state without expiry, duplicate merge keys, invalid or missing source
|
|
365
|
+
lineage, and unsupported promotion targets are rejected before promotion.
|
|
366
|
+
`assistant_behavior_memory` additionally requires generalized user-behavior
|
|
367
|
+
abstraction metadata and normalizes serving text to the abstraction principle.
|
|
368
|
+
Candidate payloads keep only compact lineage references; per-candidate source
|
|
369
|
+
lineage stays on the candidate trace and in the compaction ledger for audit.
|
|
370
|
+
|
|
371
|
+
Consumers that should inherit user-level assistant behavior together with a
|
|
372
|
+
project scope should include the user scope in the active path, for example
|
|
373
|
+
`AQUIFER_MEMORY_ACTIVE_SCOPE_PATH=global,user:user,project:aquifer` and matching
|
|
374
|
+
`AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS`. Applicable `assistant_shaping` records are
|
|
375
|
+
pinned ahead of ordinary project current memory during bootstrap.
|
|
376
|
+
|
|
377
|
+
## Read-only governance diagnostics
|
|
378
|
+
|
|
379
|
+
Use the governance commands before running write paths when continuity or
|
|
380
|
+
operator state looks wrong:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
npx aquifer doctor --json
|
|
384
|
+
npx aquifer finalization list --status failed --json
|
|
385
|
+
npx aquifer finalization inspect --id 42 --json
|
|
386
|
+
npx aquifer explain bootstrap --active-scope-key project:aquifer --json
|
|
387
|
+
npx aquifer explain memory --query "serving contract" --active-scope-key project:aquifer --json
|
|
388
|
+
npx aquifer review queue --scope-key project:aquifer --json
|
|
389
|
+
npx aquifer review inspect --memory-id 42 --json
|
|
390
|
+
npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json
|
|
391
|
+
npx aquifer operator status --json
|
|
392
|
+
npx aquifer operator inspect --run-id 42 --kind compaction --json
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`doctor` reports package/runtime, backend, DB/schema/tenant readiness,
|
|
396
|
+
migration readiness, serving mode, MCP tool count, session/finalization
|
|
397
|
+
backlog, and stale operator claims. Host checks are scoped: OpenClaw wiring is
|
|
398
|
+
checked only with `--host openclaw` or `--openclaw-home`, and Codex checkpoint
|
|
399
|
+
hook state is checked only with `--host codex`.
|
|
400
|
+
|
|
401
|
+
The finalization, explain, review queue/inspect, and operator inspect commands
|
|
402
|
+
read committed ledger and current-memory projection material only. `review
|
|
403
|
+
queue|inspect` derives its worklist from curated memory feedback events on
|
|
404
|
+
active visible memory records and uses the normal Aquifer migration gate because
|
|
405
|
+
it depends on the resolution ledger schema. It summarizes feedback counts and
|
|
406
|
+
safe identity fields; it does not print raw transcripts, feedback notes,
|
|
407
|
+
feedback metadata, evidence text, or memory payloads. `review resolve` is
|
|
408
|
+
append-only: it records a resolution snapshot through the latest issue feedback
|
|
409
|
+
id, leaves `memory_records` and feedback history untouched, and lets newer
|
|
410
|
+
issue feedback reopen the item. These commands do not promote memory, mutate
|
|
411
|
+
finalization status, reclaim leases, or change the ten-tool MCP surface.
|
|
412
|
+
Explain output is also redacted for non-selected rows: it reports safe identity,
|
|
413
|
+
selection reason, and scope inheritance details, but not hidden, shadowed,
|
|
414
|
+
out-of-scope, inactive, or trimmed row content.
|
|
276
415
|
|
|
277
416
|
## Release verification gates
|
|
278
417
|
|