@shadowforge0/aquifer-memory 1.9.0 → 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 (43) hide show
  1. package/README.md +33 -4
  2. package/README_CN.md +9 -1
  3. package/README_TW.md +5 -2
  4. package/consumers/cli.js +55 -34
  5. package/consumers/codex-active-checkpoint.js +3 -1
  6. package/consumers/codex-current-memory.js +10 -6
  7. package/consumers/codex.js +5 -2
  8. package/consumers/default/daily-entries.js +2 -2
  9. package/consumers/default/index.js +40 -30
  10. package/consumers/default/prompts/summary.js +2 -2
  11. package/consumers/mcp.js +56 -49
  12. package/consumers/openclaw-ext/index.js +1 -1
  13. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  14. package/consumers/openclaw-ext/package.json +1 -1
  15. package/consumers/openclaw-plugin.js +66 -23
  16. package/consumers/shared/compat-recall.js +101 -0
  17. package/consumers/shared/openclaw-product-tools.js +130 -0
  18. package/consumers/shared/recall-format.js +2 -2
  19. package/core/aquifer.js +385 -20
  20. package/core/backends/local.js +60 -1
  21. package/core/finalization-review.js +88 -42
  22. package/core/interface.js +629 -0
  23. package/core/mcp-manifest.js +11 -3
  24. package/core/memory-bootstrap.js +25 -27
  25. package/core/memory-consolidation.js +564 -42
  26. package/core/memory-explain.js +20 -51
  27. package/core/memory-promotion.js +392 -55
  28. package/core/memory-recall.js +26 -48
  29. package/core/memory-records.js +91 -103
  30. package/core/memory-type-policy.js +298 -0
  31. package/core/postgres-migrations.js +9 -0
  32. package/core/session-checkpoint-producer.js +3 -1
  33. package/core/session-checkpoints.js +1 -1
  34. package/core/session-finalization.js +2 -2
  35. package/docs/getting-started.md +16 -3
  36. package/docs/setup.md +61 -2
  37. package/package.json +2 -2
  38. package/schema/004-completion.sql +4 -4
  39. package/schema/010-v1-finalization-review.sql +72 -0
  40. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  41. package/scripts/backfill-canonical-key.js +1 -1
  42. package/scripts/diagnose-fts-zh.js +1 -1
  43. package/scripts/extract-insights-from-recent-sessions.js +4 -4
package/README.md CHANGED
@@ -93,12 +93,41 @@ Curated serving is scope-bound. `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH` is the ordere
93
93
  | Heartbeat-check an active Codex session for checkpoint work | `npx aquifer codex-recovery checkpoint-heartbeat --hook-stdin --scope-key project:aquifer` |
94
94
  | Inspect pending Codex checkpoint spool files | `npx aquifer codex-recovery checkpoint-spool-status --json --limit 10` |
95
95
  | Preview a Codex UserPromptSubmit heartbeat hook install | `npx aquifer codex-recovery checkpoint-heartbeat-hook --scope-key project:aquifer --hooks-path "$CODEX_HOME/hooks.json" --json` |
96
- | Inspect storage health | `npx aquifer stats` |
96
+ | Check memory readiness | `npx aquifer stats` |
97
+ | Check saved-content preparation | `npx aquifer backlog --json` |
98
+ | Dry-run a saved-content policy decision | `npx aquifer backlog --plan skip --status pending --source openclaw-mcp` |
97
99
  | Enrich pending sessions | `npx aquifer backfill` |
98
100
 
99
- Timer synthesis output is candidate material until an operator applies it with
100
- `--promote-candidates`; it does not become active curated memory from the
101
- prompt or summary file alone.
101
+ `stats`, `backlog`, MCP `memory_stats`, and MCP `memory_pending` default to the
102
+ same public status surface: `Aquifer status` or `Saved content status`, plus
103
+ `Available`, `Attention`, and `Action` where relevant. Use `stats --diagnostics`,
104
+ `backlog --diagnostics`, or MCP `diagnostics: true` for raw counters, buckets,
105
+ guidance, and samples.
106
+
107
+ Reviewed timer synthesis is gated before promotion. Candidate items must carry
108
+ `mergeKey`, `scopeClass`, `durability`, `promotionTarget`, and
109
+ `sourceCanonicalKeys` that reference `sourceCurrentMemory`; runtime state also
110
+ needs `staleAfter` or `validTo`. The temporal distillation gate rejects
111
+ workspace/operator policy, transient material, unsupported promotion targets,
112
+ invalid or missing source lineage, and duplicate merge keys before current
113
+ memory promotion. `assistant_shaping` may target `assistant_behavior_memory`
114
+ only when the item carries behavior-level abstraction metadata:
115
+ `languageLevel=user_behavior`, `appliesBeyondSource=true`, `sourceBound=false`,
116
+ and a `principle`. Project-specific source wording stays lineage/context
117
+ material; serving memory uses the behavior principle.
118
+
119
+ To serve user-level assistant behavior alongside a project, include the user
120
+ scope in the active path, for example
121
+ `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH=global,user:mk,project:aquifer` with matching
122
+ allowed scope keys. Applicable `assistant_shaping` records are pinned ahead of
123
+ ordinary project current memory in bootstrap.
124
+
125
+ Timer synthesis output is candidate material until an operator applies a
126
+ reviewed synthesis summary with `--promote-candidates`; it does not become
127
+ active curated memory from the prompt or summary file alone. The deterministic
128
+ daily/weekly/monthly aggregate proposals in a dry-run are source-rollup
129
+ material for review and ledger lineage only, and are blocked from normal active
130
+ promotion unless a reviewed synthesis summary is attached.
102
131
 
103
132
  Checkpoint output follows the same boundary. `operator checkpoint` plans from
104
133
  finalized session summaries and only writes `checkpoint_runs` when you pass an
package/README_CN.md CHANGED
@@ -126,9 +126,17 @@ Claude Code、Claude Desktop 或任何支持 MCP 的 client——放进 `.mcp.js
126
126
  | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
127
127
  | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
128
128
  | Inspect pending Codex checkpoint spool files | `npx aquifer codex-recovery checkpoint-spool-status --json --limit 10` |
129
- | Inspect storage health | `npx aquifer stats` |
129
+ | Check memory readiness | `npx aquifer stats` |
130
+ | Check saved-content preparation | `npx aquifer backlog --json` |
131
+ | Dry-run a saved-content policy decision | `npx aquifer backlog --plan skip --status pending --source openclaw-mcp` |
130
132
  | Enrich pending sessions | `npx aquifer backfill` |
131
133
 
134
+ `stats`, `backlog`, MCP `memory_stats`, and MCP `memory_pending` default to the
135
+ same public status surface: `Aquifer status` or `Saved content status`, plus
136
+ `Available`, `Attention`, and `Action` where relevant. Use CLI `--diagnostics`
137
+ or MCP `diagnostics: true` when a host needs raw counters, buckets, guidance,
138
+ or samples.
139
+
132
140
  Timer synthesis output is candidate material until an operator applies it with
133
141
  `--promote-candidates`; it does not become active curated memory from the
134
142
  prompt or summary file alone.
package/README_TW.md CHANGED
@@ -86,8 +86,11 @@ Curated serving 受 scope 邊界保護。`AQUIFER_MEMORY_ACTIVE_SCOPE_PATH` 是
86
86
  | 產生 timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
87
87
  | 套用已審核的 timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
88
88
  | 檢查 Codex checkpoint spool 待處理檔 | `npx aquifer codex-recovery checkpoint-spool-status --json --limit 10` |
89
- | 看儲存狀態 | `npx aquifer stats` |
90
- | 補跑 pending session | `npx aquifer backfill` |
89
+ | 看記憶是否可用 | `npx aquifer stats` |
90
+ | 檢查已儲存內容準備狀態 | `npx aquifer backlog --json` |
91
+ | 準備已儲存內容 | `npx aquifer backfill` |
92
+
93
+ `stats`、`backlog`、MCP `memory_stats`、MCP `memory_pending` 預設共用同一個 public status surface:`Aquifer status` 或 `Saved content status`,必要時再加 `Available`、`Attention`、`Action`。需要 raw counters、source / agent / status 分桶、guidance 或 samples 時,CLI 用 `--diagnostics`,MCP 傳 `diagnostics: true`。
91
94
 
92
95
  Timer synthesis output 在 operator 用 `--promote-candidates` apply 前都只是 candidate material;光有 prompt 或 summary file 不會變成 active curated memory。
93
96
  `codex-recovery checkpoint-spool-status --json` 只列出本機 spool 檔的 session、coverage、大小與修改時間,不會印出 checkpoint prompt 內容。
package/consumers/cli.js CHANGED
@@ -9,7 +9,8 @@
9
9
  * aquifer migrate Run database migrations
10
10
  * aquifer recall <query> [options] Search sessions
11
11
  * aquifer backfill [options] Enrich pending sessions
12
- * aquifer stats [options] Show database statistics
12
+ * aquifer backlog [options] Explain saved-content preparation work
13
+ * aquifer stats [options] Check memory readiness and diagnostics
13
14
  * aquifer backend-info [--json] Show selected backend capabilities
14
15
  * aquifer install-openclaw Install/update OpenClaw MCP + extension wiring
15
16
  * aquifer finalization ... Inspect read-only finalization ledger
@@ -24,12 +25,12 @@ const { loadConfig } = require('./shared/config');
24
25
  const { formatRecallResults } = require('./shared/recall-format');
25
26
  const { backendCapabilities } = require('../core/backends/capabilities');
26
27
  const { summarizeFinalizationListRow } = require('../core/finalization-inspector');
27
-
28
- function formatDate(value, fallback) {
29
- if (!value) return fallback;
30
- const parsed = new Date(value);
31
- return isNaN(parsed.getTime()) ? fallback : parsed.toISOString().slice(0, 10);
32
- }
28
+ const {
29
+ buildBacklogEnvelope,
30
+ buildStatsEnvelope,
31
+ formatMemoryStatsInterface,
32
+ formatPendingWorkInterface,
33
+ } = require('../core/interface');
33
34
 
34
35
  function quoteIdentifier(identifier) {
35
36
  if (!/^[a-zA-Z_]\w{0,62}$/.test(identifier)) {
@@ -45,6 +46,12 @@ function parsePositiveInt(value, fallback) {
45
46
  return Math.max(1, parsed);
46
47
  }
47
48
 
49
+ function printTextBlock(text) {
50
+ for (const line of String(text).split(/\r?\n/)) {
51
+ console.log(line);
52
+ }
53
+ }
54
+
48
55
  function parseScopePath(value) {
49
56
  if (!value) return undefined;
50
57
  const parts = String(value).split(',').map(s => s.trim()).filter(Boolean);
@@ -180,6 +187,7 @@ function parseArgs(argv) {
180
187
  // Flags that take a value (not boolean)
181
188
  const VALUE_FLAGS = new Set([
182
189
  'limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status',
190
+ 'plan', 'action',
183
191
  'concurrency', 'entities', 'entity-mode', 'mode', 'session-id', 'memory-id', 'canonical-key',
184
192
  'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
185
193
  'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
@@ -408,37 +416,42 @@ async function cmdBackfill(aquifer, args) {
408
416
  if (failed > 0) process.exitCode = 2;
409
417
  }
410
418
 
419
+ async function cmdBacklog(aquifer, args) {
420
+ const action = args.flags.plan || args.flags.action || 'inspect';
421
+ const opts = {
422
+ limit: parsePositiveInt(args.flags.limit, 50),
423
+ source: args.flags.source || undefined,
424
+ agentId: args.flags['agent-id'] || undefined,
425
+ status: args.flags.status || undefined,
426
+ action,
427
+ };
428
+ const report = await aquifer.getPendingWork(opts);
429
+
430
+ if (args.flags.json) {
431
+ console.log(JSON.stringify(buildBacklogEnvelope(report, {
432
+ diagnostics: args.flags.diagnostics === true,
433
+ includePlan: true,
434
+ }), null, 2));
435
+ return;
436
+ }
437
+
438
+ printTextBlock(formatPendingWorkInterface(report, {
439
+ diagnostics: args.flags.diagnostics === true,
440
+ includePlan: true,
441
+ }));
442
+ }
443
+
411
444
  async function cmdStats(aquifer, args) {
412
445
  const stats = await aquifer.getStats();
413
446
 
414
447
  if (args.flags.json) {
415
- console.log(JSON.stringify(stats, null, 2));
448
+ console.log(JSON.stringify(buildStatsEnvelope(stats, {
449
+ diagnostics: args.flags.diagnostics === true,
450
+ }), null, 2));
416
451
  } else {
417
- console.log(`Backend: ${stats.backendKind || 'unknown'} (${stats.backendProfile || 'unknown'})`);
418
- console.log(`Serving mode: ${stats.serving?.mode || 'legacy'}`);
419
- console.log(`Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`);
420
- console.log(`Sessions: ${stats.sessionTotal} (${Object.entries(stats.sessions).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
421
- if (stats.pendingSessions?.available) {
422
- console.log(`Actionable pending/failed: ${stats.pendingSessions.total}`);
423
- }
424
- console.log(`Summaries: ${stats.summaries}`);
425
- console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
426
- console.log(`Entities: ${stats.entities}`);
427
- if (stats.memoryRecords) {
428
- console.log(`Memory records: ${stats.memoryRecords.total} (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
429
- if (stats.memoryRecords.latest) console.log(`Memory record range: ${formatDate(stats.memoryRecords.earliest, '?')} — ${formatDate(stats.memoryRecords.latest, '?')}`);
430
- }
431
- if (stats.sessionFinalizations?.available) {
432
- const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
433
- .map(([status, count]) => `${status}: ${count}`)
434
- .join(', ') || 'none';
435
- console.log(`Session finalizations: ${stats.sessionFinalizations.total} (${statusText})`);
436
- if (stats.sessionFinalizations.latestFinalizedAt) console.log(`Latest finalization: ${formatDate(stats.sessionFinalizations.latestFinalizedAt, '?')}`);
437
- }
438
- if (stats.earliest) console.log(`Range: ${formatDate(stats.earliest, '?')} — ${formatDate(stats.latest, '?')}`);
439
- if ((stats.serving?.mode || 'legacy') !== 'curated') {
440
- console.log('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
441
- }
452
+ printTextBlock(formatMemoryStatsInterface(stats, {
453
+ diagnostics: args.flags.diagnostics === true,
454
+ }));
442
455
  }
443
456
  }
444
457
 
@@ -1300,11 +1313,12 @@ Commands:
1300
1313
  feedback-stats Show trust feedback statistics and coverage
1301
1314
  review ... Inspect or resolve memory feedback review queue
1302
1315
  backfill Enrich pending sessions
1316
+ backlog Explain and plan saved-content preparation work
1303
1317
  operator ... Run operator-safe consolidation jobs
1304
1318
  finalization ... Inspect read-only finalization ledger
1305
1319
  explain ... Explain current-memory serving decisions
1306
1320
  compact Plan or apply curated memory compaction
1307
- stats Show database statistics
1321
+ stats Check memory readiness
1308
1322
  export Export sessions as JSONL
1309
1323
  bootstrap Show recent session context (for new session start)
1310
1324
  codex-recovery ... Inspect or run Codex recovery/checkpoint flows
@@ -1322,6 +1336,9 @@ Options:
1322
1336
  --id ID Finalization row id
1323
1337
  --host HOST Finalization host filter
1324
1338
  --status STATUS Finalization status filter
1339
+ --plan inspect|backfill|skip
1340
+ Saved-content dry-run action plan
1341
+ --diagnostics Include raw counters, buckets, samples, and serving diagnostics
1325
1342
  --phase PHASE Finalization phase filter
1326
1343
  --transcript-hash HASH Finalization transcript hash filter
1327
1344
  --run-id ID Operator run id
@@ -1520,6 +1537,9 @@ Operator examples:
1520
1537
  case 'backfill':
1521
1538
  await cmdBackfill(aquifer, args);
1522
1539
  break;
1540
+ case 'backlog':
1541
+ await cmdBacklog(aquifer, args);
1542
+ break;
1523
1543
  case 'operator':
1524
1544
  await cmdOperator(aquifer, args);
1525
1545
  break;
@@ -1564,6 +1584,7 @@ module.exports = {
1564
1584
  cmdDoctor,
1565
1585
  cmdLocalQuickstart,
1566
1586
  cmdOperator,
1587
+ cmdBacklog,
1567
1588
  cmdFinalization,
1568
1589
  cmdExplain,
1569
1590
  cmdReview,
@@ -6,6 +6,7 @@ const {
6
6
  promptSafeSynthesisInput,
7
7
  stableJson,
8
8
  } = require('../core/session-checkpoint-producer');
9
+ const { assistantShapingPromptLines } = require('../core/memory-type-policy');
9
10
  const { compactCurrentMemorySnapshot } = require('./codex-current-memory');
10
11
 
11
12
  function positiveInt(value, fallback, max = 100000) {
@@ -122,8 +123,9 @@ function buildActiveSessionCheckpointPrompt(checkpointInput = {}, opts = {}) {
122
123
  'Use only the <active_checkpoint_input> block. Do not use hidden tool output, injected context, or debug material.',
123
124
  'This checkpoint is process material for later handoff. It is not active current memory and must not be treated as final truth.',
124
125
  'Return compact JSON with this shape:',
125
- '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}',
126
+ '{"summaryText":"...","structuredSummary":{"assistant_shaping":[],"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}',
126
127
  `Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
128
+ ...assistantShapingPromptLines(),
127
129
  'Preserve the coverage object so a later handoff can skip the already-covered transcript prefix.',
128
130
  '',
129
131
  '<active_checkpoint_input>',
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { formatRuntimeMemoryLine } = require('../core/memory-type-policy');
4
+
3
5
  function compactCurrentMemoryRow(row = {}) {
4
6
  const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
5
7
  const confidence = payload.confidence || payload.currentMemoryConfidence || null;
@@ -10,6 +12,9 @@ function compactCurrentMemoryRow(row = {}) {
10
12
  summary: String(row.summary || row.title || '').replace(/\s+/g, ' ').trim(),
11
13
  authority: row.authority || null,
12
14
  confidence,
15
+ policyKey: payload.policyKey || payload.policy_key || row.policyKey || row.policy_key || null,
16
+ shapingKind: payload.shapingKind || payload.shaping_kind || payload.kind || payload.category || row.shapingKind || row.shaping_kind || null,
17
+ servingImpact: payload.servingImpact || payload.serving_impact || payload.impact || row.servingImpact || row.serving_impact || null,
13
18
  };
14
19
  }
15
20
 
@@ -35,12 +40,11 @@ function formatCurrentMemoryPromptBlock(currentMemory = null, opts = {}) {
35
40
  `truncated="${Boolean(meta.truncated || rows.length > compactRows.length)}"`,
36
41
  `degraded="${Boolean(meta.degraded || currentMemory?.error)}"`,
37
42
  ];
38
- const lines = compactRows.map(row => {
39
- const scope = row.scopeKey ? ` scope=${row.scopeKey}` : '';
40
- const authority = row.authority ? ` authority=${row.authority}` : '';
41
- const confidence = row.confidence ? ` confidence=${row.confidence}` : '';
42
- return `- ${row.memoryType}${scope}${authority}${confidence}: ${row.summary}`;
43
- });
43
+ const lines = compactRows.map(row => formatRuntimeMemoryLine(row, {
44
+ includeScope: true,
45
+ includeAuthority: true,
46
+ includeConfidence: true,
47
+ }));
44
48
  if (currentMemory && currentMemory.error && lines.length === 0) {
45
49
  lines.push(`- degraded: ${String(currentMemory.error).replace(/\s+/g, ' ').trim()}`);
46
50
  }
@@ -22,6 +22,7 @@ const crypto = require('crypto');
22
22
  const DEFAULT_CODEX_HOME = path.join(os.homedir(), '.codex');
23
23
  const { normalizeMessages } = require('./shared/normalize');
24
24
  const { applyEnrichSafetyGate } = require('../core/memory-safety-gate');
25
+ const { assistantShapingPromptLines } = require('../core/memory-type-policy');
25
26
  const {
26
27
  buildActiveSessionCheckpointInput,
27
28
  buildActiveSessionCheckpointPrompt,
@@ -1335,8 +1336,9 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
1335
1336
  'You are finalizing an Aquifer memory session for Codex.',
1336
1337
  'Use only the sanitized transcript below. Do not infer from hidden tool output or injected context.',
1337
1338
  'Return compact JSON with this shape:',
1338
- '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
1339
+ '{"summaryText":"...","structuredSummary":{"assistant_shaping":[],"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
1339
1340
  `Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
1341
+ ...assistantShapingPromptLines(),
1340
1342
  '',
1341
1343
  `sessionId: ${view.sessionId}`,
1342
1344
  `transcriptHash: ${view.transcriptHash}`,
@@ -1352,7 +1354,8 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
1352
1354
  0,
1353
1355
  'Use current_memory as the already-committed current state. Reconcile the transcript against it: keep valid state, supersede stale state, and mark uncertain items explicitly.',
1354
1356
  );
1355
- lines.splice(10, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
1357
+ const transcriptIndex = lines.indexOf('<sanitized_transcript>');
1358
+ lines.splice(transcriptIndex >= 0 ? transcriptIndex : lines.length, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
1356
1359
  }
1357
1360
  return lines.join('\n');
1358
1361
  }
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  // Aquifer default persona — parameterized daily_entries writer.
4
- // Schema matches miranda.daily_entries (id / event_at / source / tag / text /
4
+ // Schema matches the generic daily_entries shape (id / event_at / source / tag / text /
5
5
  // agent_id / session_id / metadata / dedupe_key) — hosts clone that DDL into
6
6
  // their own schema and set persona.dailyTable = '<schema>.daily_entries'.
7
7
  //
8
8
  // Host must create the table before use:
9
- // CREATE TABLE jenny.daily_entries (LIKE miranda.daily_entries INCLUDING ALL);
9
+ // CREATE TABLE app_memory.daily_entries (...);
10
10
 
11
11
  const crypto = require('crypto');
12
12
  const { parseHandoffSection } = require('../shared/summary-parser');
@@ -6,13 +6,13 @@
6
6
  // Entry point:
7
7
  // const persona = require('@shadowforge0/aquifer-memory/consumers/default')
8
8
  // .createPersona({
9
- // agentName: 'Dobby',
10
- // observedOwner: 'evan',
11
- // schema: 'jenny',
12
- // scope: 'jenny',
13
- // dailyTable: 'jenny.daily_entries', // or null to skip daily writes
9
+ // agentName: 'Assistant',
10
+ // observedOwner: 'operator',
11
+ // schema: 'app_memory',
12
+ // scope: 'app_memory',
13
+ // dailyTable: 'app_memory.daily_entries', // or null to skip daily writes
14
14
  // language: 'zh-TW', // or 'en'
15
- // briefingIntro: '你是 Dobby。以下是現況...', // optional context-inject preamble
15
+ // briefingIntro: '以下是現況...', // optional context-inject preamble
16
16
  // });
17
17
  //
18
18
  // Returns a persona module with the standard persona adapter shape — host
@@ -21,13 +21,15 @@
21
21
  // .createPersona({ ... });
22
22
  //
23
23
  // This is intentionally a minimal persona: summary + optional daily_entries,
24
- // no workspace-files, no consolidation, no Miranda-specific scaffolding.
24
+ // no workspace-files, no consolidation, no host-specific scaffolding.
25
25
  // Host can extend by composing with Aquifer primitives from consumers/shared.
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
28
  const { createAquifer } = require('../../index');
29
29
  const { runIngest } = require('../shared/ingest');
30
30
  const { parseEntitySection } = require('../shared/entity-parser');
31
+ const { registerOpenClawProductStatusTools } = require('../shared/openclaw-product-tools');
32
+ const { buildCompatibilityRecallRequest, runCompatibilityRecall, memoryServingMode } = require('../shared/compat-recall');
31
33
 
32
34
  const summaryModule = require('./prompts/summary');
33
35
  const dailyEntriesModule = require('./daily-entries');
@@ -129,10 +131,10 @@ function createPersona(personaOpts = {}) {
129
131
  // v1.2.0: all four are env-driven by default. Host may override any of
130
132
  // them via opts. Aquifer core throws with clear guidance if the required
131
133
  // env vars are missing, so we do not pre-validate here.
132
- const aquifer = getAquifer(opts);
133
- const pool = opts.pool || aquifer.getPool();
134
- const llmFn = opts.llmFn || aquifer.getLlmFn();
135
- const embedFn = opts.embedFn || aquifer.getEmbedFn();
134
+ const aquifer = opts.aquifer || getAquifer(opts);
135
+ const pool = opts.pool || (aquifer.getPool ? aquifer.getPool() : null);
136
+ const llmFn = opts.llmFn || (aquifer.getLlmFn ? aquifer.getLlmFn() : null);
137
+ const embedFn = opts.embedFn || (aquifer.getEmbedFn ? aquifer.getEmbedFn() : null);
136
138
  return {
137
139
  pool,
138
140
  embedFn,
@@ -205,7 +207,16 @@ function createPersona(personaOpts = {}) {
205
207
  try {
206
208
  const agentId = ctx?.agentId || defaultAgentId;
207
209
  if ((ctx?.sessionKey || '').includes('subagent')) return;
208
- const recalled = await aquifer.bootstrap({ agentId, limit: 5, maxChars: 2000, format: 'text' });
210
+ const bootstrapOpts = { limit: 5, maxChars: 2000, format: 'text' };
211
+ if (memoryServingMode(aquifer) === 'curated') {
212
+ const activeScopeKey = opts.activeScopeKey || opts.active_scope_key || ctx?.activeScopeKey || ctx?.active_scope_key;
213
+ const activeScopePath = opts.activeScopePath || opts.active_scope_path || ctx?.activeScopePath || ctx?.active_scope_path;
214
+ if (activeScopeKey) bootstrapOpts.activeScopeKey = activeScopeKey;
215
+ if (activeScopePath) bootstrapOpts.activeScopePath = activeScopePath;
216
+ } else {
217
+ bootstrapOpts.agentId = agentId;
218
+ }
219
+ const recalled = await aquifer.bootstrap(bootstrapOpts);
209
220
  const context = persona.briefingIntro + (recalled ? `\n\n${recalled}` : '');
210
221
  if (context.length > 0) return { prependSystemContext: context };
211
222
  } catch (err) {
@@ -232,33 +243,23 @@ function createPersona(personaOpts = {}) {
232
243
  source: { type: 'string' },
233
244
  date_from: { type: 'string' },
234
245
  date_to: { type: 'string' },
246
+ host: { type: 'string' },
247
+ session_id: { type: 'string' },
235
248
  entities: { type: 'array', items: { type: 'string' }, description: 'Named entities (person/project/tool/file)' },
236
249
  entity_mode: { type: 'string', enum: ['any', 'all'], description: '"any" boosts; "all" hard-filters to sessions containing every entity' },
237
250
  mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall strategy, default hybrid' },
251
+ active_scope_key: { type: 'string' },
252
+ active_scope_path: { type: 'array', items: { type: 'string' } },
238
253
  },
239
254
  },
240
255
  async execute(_toolCallId, params) {
241
256
  try {
242
- const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
243
- const recallOpts = {
244
- agentId: params?.agent_id || ctx?.agentId || undefined,
245
- source: params?.source || undefined,
246
- dateFrom: params?.date_from || undefined,
247
- dateTo: params?.date_to || undefined,
248
- limit,
249
- };
250
- if (Array.isArray(params?.entities) && params.entities.length > 0) {
251
- recallOpts.entities = params.entities;
252
- recallOpts.entityMode = params?.entity_mode || 'any';
253
- }
254
- if (params?.mode === 'fts' || params?.mode === 'hybrid' || params?.mode === 'vector') {
255
- recallOpts.mode = params.mode;
256
- }
257
- const results = await aquifer.recall(String(params?.query || ''), recallOpts);
257
+ const request = buildCompatibilityRecallRequest(aquifer, params || {}, ctx || {});
258
+ const results = await runCompatibilityRecall(aquifer, String(params?.query || ''), request);
258
259
  const lines = results.map((r, i) =>
259
260
  `${i+1}. ${r.structuredSummary?.title || r.summaryText?.slice(0, 80) || '(untitled)'}`
260
261
  );
261
- return { content: [{ type: 'text', text: lines.join('\n') || 'No matching sessions.' }] };
262
+ return { content: [{ type: 'text', text: [request.laneHeader, '', lines.join('\n') || 'No matching sessions.'].join('\n') }] };
262
263
  } catch (err) {
263
264
  return { content: [{ type: 'text', text: `session_recall error: ${err.message}` }], isError: true };
264
265
  }
@@ -269,11 +270,19 @@ function createPersona(personaOpts = {}) {
269
270
  return { aquifer };
270
271
  }
271
272
 
273
+ function registerProductStatusTools(api, opts = {}) {
274
+ const aquifer = opts.aquifer || resolveCommon(opts).aquifer;
275
+ registerOpenClawProductStatusTools(api, aquifer);
276
+ api.logger.info('[default-persona] registerProductStatusTools: memory_stats + memory_pending registered');
277
+ return { aquifer, productStatusToolsRegistered: true };
278
+ }
279
+
272
280
  function mountOnOpenClaw(api, opts = {}) {
273
281
  const r = registerAfterburn(api, opts);
282
+ registerProductStatusTools(api, { ...opts, aquifer: r.aquifer });
274
283
  registerContextInject(api, opts);
275
284
  registerRecallTool(api, opts);
276
- return r;
285
+ return { ...r, productStatusToolsRegistered: true };
277
286
  }
278
287
 
279
288
  return {
@@ -282,6 +291,7 @@ function createPersona(personaOpts = {}) {
282
291
  registerAfterburn,
283
292
  registerContextInject,
284
293
  registerRecallTool,
294
+ registerProductStatusTools,
285
295
  buildPostProcess,
286
296
  buildSummaryFn,
287
297
  buildEntityParseFn,
@@ -5,11 +5,11 @@
5
5
  // Parameterized via personaOpts:
6
6
  // agentName — human name/role the prompt addresses (default 'Assistant')
7
7
  // observedOwner — if set, the prompt asks for a short observation about
8
- // that person (matches Miranda's "對 MK 的觀察" slot).
8
+ // that person.
9
9
  // null → the section is omitted entirely.
10
10
  // language — 'en' | 'zh-TW' (default 'en')
11
11
  //
12
- // Output format mirrors Miranda's RECAP fields so downstream daily-entries
12
+ // Output format mirrors the generic RECAP fields so downstream daily-entries
13
13
  // parsing works uniformly across personas.
14
14
 
15
15
  function buildSummaryPrompt({ conversationText, agentId, now, dailyContext, persona = {} }) {