@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.
Files changed (57) hide show
  1. package/.env.example +1 -0
  2. package/README.md +82 -26
  3. package/README_CN.md +33 -23
  4. package/README_TW.md +25 -24
  5. package/aquifer.config.example.json +2 -1
  6. package/consumers/cli.js +587 -33
  7. package/consumers/codex-active-checkpoint.js +3 -1
  8. package/consumers/codex-current-memory.js +10 -6
  9. package/consumers/codex.js +6 -3
  10. package/consumers/default/daily-entries.js +2 -2
  11. package/consumers/default/index.js +40 -30
  12. package/consumers/default/prompts/summary.js +2 -2
  13. package/consumers/mcp.js +56 -46
  14. package/consumers/openclaw-ext/index.js +65 -7
  15. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  16. package/consumers/openclaw-ext/package.json +1 -1
  17. package/consumers/openclaw-install.js +326 -0
  18. package/consumers/openclaw-plugin.js +105 -24
  19. package/consumers/shared/compat-recall.js +101 -0
  20. package/consumers/shared/config.js +2 -0
  21. package/consumers/shared/openclaw-product-tools.js +130 -0
  22. package/consumers/shared/recall-format.js +2 -2
  23. package/core/aquifer.js +553 -41
  24. package/core/backends/local.js +169 -1
  25. package/core/doctor.js +924 -0
  26. package/core/finalization-inspector.js +164 -0
  27. package/core/finalization-review.js +88 -42
  28. package/core/interface.js +629 -0
  29. package/core/mcp-manifest.js +11 -3
  30. package/core/memory-bootstrap.js +25 -27
  31. package/core/memory-consolidation.js +564 -42
  32. package/core/memory-explain.js +593 -0
  33. package/core/memory-promotion.js +392 -55
  34. package/core/memory-recall.js +75 -71
  35. package/core/memory-records.js +107 -108
  36. package/core/memory-review.js +891 -0
  37. package/core/memory-serving.js +61 -4
  38. package/core/memory-type-policy.js +298 -0
  39. package/core/operator-observability.js +249 -0
  40. package/core/postgres-migrations.js +22 -0
  41. package/core/session-checkpoint-producer.js +3 -1
  42. package/core/session-checkpoints.js +1 -1
  43. package/core/session-finalization.js +78 -3
  44. package/core/storage.js +124 -8
  45. package/docs/getting-started.md +50 -4
  46. package/docs/setup.md +163 -24
  47. package/package.json +5 -4
  48. package/schema/004-completion.sql +4 -4
  49. package/schema/010-v1-finalization-review.sql +72 -0
  50. package/schema/019-v1-memory-review-resolutions.sql +53 -0
  51. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  52. package/scripts/backfill-canonical-key.js +1 -1
  53. package/scripts/codex-checkpoint-commands.js +28 -0
  54. package/scripts/codex-checkpoint-runtime.js +109 -0
  55. package/scripts/codex-recovery.js +16 -4
  56. package/scripts/diagnose-fts-zh.js +1 -1
  57. 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
- return storage.listSessionFinalizations(pool, input, { schema, tenantId });
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 = [`tenant_id = $1`];
679
+ const where = [`${table}.tenant_id = $1`];
665
680
  if (input.host) {
666
681
  params.push(input.host);
667
- where.push(`host = $${params.length}`);
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(`status = ANY($${params.length}::text[])`);
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(`agent_id = $${params.length}`);
692
+ where.push(`${table}.agent_id = $${params.length}`);
678
693
  }
679
694
  if (input.source) {
680
695
  params.push(input.source);
681
- where.push(`source = $${params.length}`);
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
- FROM ${qi(schema)}.session_finalizations
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,
@@ -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; `evidence_recall` remains the explicit evidence/debug tool in both modes. Rollback is just setting env or config back to `legacy`.
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
- | Show stats | `npx aquifer stats` |
89
- | Enrich pending sessions | `npx aquifer backfill` |
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. `evidence_recall` remains the explicit audit/debug lane in both modes, and rollback is just setting env or config back to `legacy`.
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 `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`.
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
- `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.
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
- Add to `openclaw.json`:
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
- ```json
215
- {
216
- "mcp": {
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
- 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`.
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
- { "state": "The reviewed state that should continue into current memory." }
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