@shadowforge0/aquifer-memory 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +374 -39
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
@@ -1,186 +0,0 @@
1
- 'use strict';
2
-
3
- // Miranda daily log renderer — reference implementation for the artifact
4
- // capability (spec §12).
5
- //
6
- // Pulls the canonical state for a date from Aquifer (timeline events + latest
7
- // state + active narrative + latest handoff) and renders it into a single
8
- // markdown file. Pure logic — does not write to disk; returns the rendered
9
- // string plus an artifact record declaration the caller can persist via
10
- // aq.artifacts.record().
11
- //
12
- // Shape deliberately mirrors the historical Miranda daily-log format so the
13
- // downstream consumers (CC, Discord pushes, weekly rollup) see no regression
14
- // during the cutover from render-daily-log.js to this reference impl.
15
-
16
- const crypto = require('crypto');
17
-
18
- function startOfDayIso(dateStr) {
19
- return `${dateStr}T00:00:00Z`;
20
- }
21
-
22
- function endOfDayIso(dateStr) {
23
- return `${dateStr}T23:59:59.999Z`;
24
- }
25
-
26
- function ensureDate(input) {
27
- if (!input) throw new Error('date (YYYY-MM-DD) is required');
28
- if (!/^\d{4}-\d{2}-\d{2}$/.test(input)) {
29
- throw new Error(`date must match YYYY-MM-DD, got: ${input}`);
30
- }
31
- return input;
32
- }
33
-
34
- function renderSection(title, lines) {
35
- if (!lines || lines.length === 0) return null;
36
- return `## ${title}\n\n${lines.join('\n')}\n`;
37
- }
38
-
39
- function formatTimelineLine(evt) {
40
- const ts = new Date(evt.occurredAt).toISOString().slice(11, 16);
41
- const src = evt.sessionRef ? ` (${evt.sessionRef})` : '';
42
- return `- \`${ts}\`${src} ${evt.text}`;
43
- }
44
-
45
- function formatHandoff(payload) {
46
- if (!payload) return null;
47
- const lines = [];
48
- if (payload.last_step) lines.push(`**Last step.** ${payload.last_step}`);
49
- if (payload.status) lines.push(`**Status.** ${payload.status}`);
50
- if (payload.next) lines.push(`**Next.** ${payload.next}`);
51
- if (Array.isArray(payload.blockers) && payload.blockers.length > 0) {
52
- lines.push(`**Blockers.**`);
53
- for (const b of payload.blockers) lines.push(`- ${b}`);
54
- }
55
- if (Array.isArray(payload.open_loops) && payload.open_loops.length > 0) {
56
- lines.push(`**Open loops.**`);
57
- for (const l of payload.open_loops) lines.push(`- ${l}`);
58
- }
59
- return lines.length > 0 ? lines.join('\n') + '\n' : null;
60
- }
61
-
62
- function formatState(state) {
63
- if (!state) return null;
64
- const lines = [];
65
- if (state.goal) lines.push(`**Goal.** ${state.goal}`);
66
- if (Array.isArray(state.active_work) && state.active_work.length > 0) {
67
- lines.push(`**Active work.**`);
68
- for (const w of state.active_work) lines.push(`- ${w}`);
69
- }
70
- if (state.affect && typeof state.affect === 'object') {
71
- const bits = [];
72
- if (state.affect.mood) bits.push(`mood: ${state.affect.mood}`);
73
- if (state.affect.energy) bits.push(`energy: ${state.affect.energy}`);
74
- if (state.affect.confidence) bits.push(`confidence: ${state.affect.confidence}`);
75
- if (bits.length > 0) lines.push(`**Affect.** ${bits.join(', ')}`);
76
- }
77
- return lines.length > 0 ? lines.join('\n') + '\n' : null;
78
- }
79
-
80
- // -------------------------------------------------------------------------
81
- // Public entry
82
- // -------------------------------------------------------------------------
83
- //
84
- // renderDailyMd({ aquifer, date, agentId, tenantId?, categories? }) returns:
85
- // {
86
- // markdown: string,
87
- // artifact: { producerId, type, format, destination, payload,
88
- // idempotencyKey } // ready for aq.artifacts.record()
89
- // }
90
- //
91
- // The caller decides whether to persist the artifact record and where the
92
- // rendered file lands. Aquifer itself doesn't touch disk.
93
-
94
- async function renderDailyMd({
95
- aquifer, date, agentId, tenantId, categories,
96
- destinationTemplate = 'workspace://memory/{date}.md',
97
- producerId = 'miranda.workspace.daily-log',
98
- }) {
99
- if (!aquifer) throw new Error('aquifer instance is required');
100
- if (!agentId) throw new Error('agentId is required');
101
- const day = ensureDate(date);
102
-
103
- const since = startOfDayIso(day);
104
- const until = endOfDayIso(day);
105
-
106
- const timelineResult = await aquifer.timeline.list({
107
- tenantId, agentId, categories, since, until, limit: 500,
108
- });
109
- if (!timelineResult.ok) throw new Error(`timeline.list failed: ${timelineResult.error.message}`);
110
-
111
- const stateResult = await aquifer.state.getLatest({ tenantId, agentId });
112
- if (!stateResult.ok) throw new Error(`state.getLatest failed: ${stateResult.error.message}`);
113
-
114
- const handoffResult = await aquifer.handoff.getLatest({ tenantId, agentId });
115
- if (!handoffResult.ok) throw new Error(`handoff.getLatest failed: ${handoffResult.error.message}`);
116
-
117
- const narrativeResult = await aquifer.narratives.getLatest({ tenantId, agentId });
118
- if (!narrativeResult.ok) throw new Error(`narratives.getLatest failed: ${narrativeResult.error.message}`);
119
-
120
- const events = (timelineResult.data.rows || []).slice().sort((a, b) =>
121
- new Date(a.occurredAt).getTime() - new Date(b.occurredAt).getTime());
122
-
123
- // Group timeline by category.
124
- const grouped = new Map();
125
- for (const evt of events) {
126
- if (!grouped.has(evt.category)) grouped.set(evt.category, []);
127
- grouped.get(evt.category).push(evt);
128
- }
129
-
130
- const sections = [];
131
- sections.push(`# ${day}\n`);
132
- const stateBlock = formatState(stateResult.data.state);
133
- if (stateBlock) sections.push(`## State\n\n${stateBlock}`);
134
-
135
- if (narrativeResult.data.narrative) {
136
- sections.push(`## Narrative\n\n${narrativeResult.data.narrative.text}\n`);
137
- }
138
-
139
- const categoryOrder = ['focus', 'todo', 'mood', 'handoff', 'narrative',
140
- 'organized', 'note', 'stats', 'health', 'garmin', 'weekly', 'monthly', 'cli'];
141
- const emitted = new Set();
142
- for (const cat of categoryOrder) {
143
- if (!grouped.has(cat)) continue;
144
- const lines = grouped.get(cat).map(formatTimelineLine);
145
- const sec = renderSection(cat.charAt(0).toUpperCase() + cat.slice(1), lines);
146
- if (sec) sections.push(sec);
147
- emitted.add(cat);
148
- }
149
- for (const [cat, evts] of grouped) {
150
- if (emitted.has(cat)) continue;
151
- const lines = evts.map(formatTimelineLine);
152
- const sec = renderSection(cat, lines);
153
- if (sec) sections.push(sec);
154
- }
155
-
156
- const handoffBlock = formatHandoff(handoffResult.data.handoff);
157
- if (handoffBlock) sections.push(`## Handoff\n\n${handoffBlock}`);
158
-
159
- const markdown = sections.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
160
-
161
- const destination = destinationTemplate.replace('{date}', day);
162
- const idempotencyKey = crypto.createHash('sha256')
163
- .update(`miranda:daily:${tenantId || 'default'}:${agentId}:${day}`)
164
- .digest('hex');
165
-
166
- return {
167
- markdown,
168
- artifact: {
169
- producerId,
170
- type: 'daily-log',
171
- format: 'markdown',
172
- destination,
173
- triggerPhase: 'artifact_dispatch',
174
- payload: {
175
- date: day,
176
- event_count: events.length,
177
- has_state: !!stateResult.data.state,
178
- has_handoff: !!handoffResult.data.handoff,
179
- has_narrative: !!narrativeResult.data.narrative,
180
- },
181
- idempotencyKey,
182
- },
183
- };
184
- }
185
-
186
- module.exports = { renderDailyMd };
@@ -1,91 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- // Host-owned file paths: emotional state + recap JSON + learned skills.
7
- // These are Miranda persona artifacts; other consumers won't want them.
8
-
9
- function extractFilePaths(conversationText) {
10
- const re = /(?:^|\s|["'`])((?:\/[\w.@-]+)+(?:\/[\w.@-]+)*\.[\w]+)/g;
11
- const paths = new Set();
12
- let m;
13
- while ((m = re.exec(conversationText || '')) !== null) {
14
- const p = m[1];
15
- if (p.startsWith('/bin/') || p.startsWith('/usr/') || p.startsWith('/etc/')) continue;
16
- if (p.includes('node_modules/')) continue;
17
- paths.add(p);
18
- }
19
- return [...paths].slice(0, 30);
20
- }
21
-
22
- async function writeWorkspaceFiles(sections, recap, workspaceDir, context, logger = console) {
23
- let emotionalStateWritten = false;
24
- const recapFilesWritten = [];
25
- let learnedSkillsWritten = 0;
26
-
27
- if (sections?.emotional_state) {
28
- await fs.promises.mkdir(path.join(workspaceDir, 'memory'), { recursive: true });
29
- await fs.promises.writeFile(
30
- path.join(workspaceDir, 'memory', 'emotional-state.md'),
31
- sections.emotional_state, 'utf8',
32
- );
33
- emotionalStateWritten = true;
34
- if (logger.info) logger.info('[miranda] wrote emotional state');
35
- }
36
-
37
- if (recap?.title) {
38
- const sessDir = path.join(workspaceDir, 'memory', 'sessions');
39
- await fs.promises.mkdir(sessDir, { recursive: true });
40
- if (context?.conversationText) recap.files_mentioned = extractFilePaths(context.conversationText);
41
-
42
- await fs.promises.writeFile(
43
- path.join(sessDir, 'afterburn-latest.json'),
44
- JSON.stringify(recap, null, 2), 'utf8',
45
- );
46
- recapFilesWritten.push('afterburn-latest.json');
47
-
48
- const safeId = String(context?.sessionId || '').replace(/[^a-zA-Z0-9._-]/g, '_');
49
- if (safeId) {
50
- const fname = `afterburn-${safeId}.json`;
51
- await fs.promises.writeFile(path.join(sessDir, fname), JSON.stringify(recap, null, 2), 'utf8');
52
- recapFilesWritten.push(fname);
53
- }
54
- if (logger.info) logger.info(`[miranda] wrote recap (title=${recap.title.slice(0, 40)})`);
55
- }
56
-
57
- if (Array.isArray(recap?.reusable_patterns) && recap.reusable_patterns.length > 0) {
58
- try {
59
- const skillsPath = path.join(workspaceDir, 'memory', 'topics', 'learned-skills.md');
60
- let existing = '';
61
- try { existing = await fs.promises.readFile(skillsPath, 'utf8'); } catch { /* first write */ }
62
- if (!existing) {
63
- existing = '# Learned Skills\n\n> 自動從 session 中抽取的可重用操作模式。\n> invariant = 永久規則。derived = 情境洞察(可能過期)。\n\n';
64
- }
65
- const now = new Intl.DateTimeFormat('sv-SE', {
66
- timeZone: 'Asia/Taipei',
67
- year: 'numeric', month: '2-digit', day: '2-digit',
68
- }).format(new Date());
69
- const patterns = recap.reusable_patterns.filter(p => p.pattern && p.trigger);
70
- const lines = [];
71
- for (const p of patterns) {
72
- const icon = p.durability === 'invariant' ? '🔒' : '📌';
73
- const suffix = p.durability !== 'invariant' ? '(expires ~14d)' : '';
74
- lines.push(`- ${icon} **${p.pattern}** — 觸發:${p.trigger};做法:${p.action || '(見 session)'}${suffix}`);
75
- }
76
- if (lines.length > 0) {
77
- await fs.promises.mkdir(path.dirname(skillsPath), { recursive: true });
78
- const section = `\n### ${now} (${String(context?.sessionId || '').slice(0, 8)})\n${lines.join('\n')}\n`;
79
- await fs.promises.writeFile(skillsPath, existing + section, 'utf8');
80
- learnedSkillsWritten = patterns.length;
81
- if (logger.info) logger.info(`[miranda] wrote ${patterns.length} skill(s)`);
82
- }
83
- } catch (err) {
84
- if (logger.info) logger.info(`[miranda] skill write skip: ${err.message}`);
85
- }
86
- }
87
-
88
- return { emotionalStateWritten, recapFilesWritten, learnedSkillsWritten };
89
- }
90
-
91
- module.exports = { writeWorkspaceFiles, extractFilePaths };
@@ -1,17 +0,0 @@
1
- -- DROP-clean script for entity_state_history (Q3 bitter-lesson escape hatch).
2
- --
3
- -- Run this if you decide native long-context / agentic memory has obviated the
4
- -- temporal state-change layer. Removes the table and all dependent indexes;
5
- -- nothing else in Aquifer references it directly (FK is one-way: this table
6
- -- references entities/sessions, not the reverse).
7
- --
8
- -- Usage:
9
- -- psql $DATABASE_URL -v schema=miranda -f scripts/drop-entity-state-history.sql
10
-
11
- DROP TABLE IF EXISTS :"schema".entity_state_history CASCADE;
12
-
13
- -- Verify nothing remains.
14
- SELECT to_regclass(:'schema' || '.entity_state_history') AS table_after_drop;
15
- SELECT to_regclass(:'schema' || '.idx_entity_state_history_current') AS idx_current_after_drop;
16
- SELECT to_regclass(:'schema' || '.idx_entity_state_history_idempotency') AS idx_idempotency_after_drop;
17
- -- All three should report NULL.
@@ -1,12 +0,0 @@
1
- -- DROP-clean script for insights (Q4 bitter-lesson escape hatch).
2
- --
3
- -- Removes the table and all dependent indexes. Nothing else in Aquifer
4
- -- references it directly, so DROP CASCADE is safe and complete.
5
-
6
- DROP TABLE IF EXISTS :"schema".insights CASCADE;
7
-
8
- -- Verify nothing remains.
9
- SELECT to_regclass(:'schema' || '.insights') AS table_after_drop;
10
- SELECT to_regclass(:'schema' || '.idx_insights_active') AS idx_active_after_drop;
11
- SELECT to_regclass(:'schema' || '.idx_insights_embedding') AS idx_embedding_after_drop;
12
- -- All three should report NULL.
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Aquifer — Install drop-in OpenClaw extension
3
- #
4
- # Usage:
5
- # bash scripts/install-openclaw.sh [OPENCLAW_HOME]
6
- #
7
- # Default OPENCLAW_HOME: $HOME/.openclaw
8
- #
9
- # What it does:
10
- # 1. Creates / overwrites $OPENCLAW_HOME/extensions/aquifer-memory/
11
- # as a symlink to <this_package>/consumers/openclaw-ext/
12
- # 2. Prints follow-up instructions: set the .env keys, restart the gateway.
13
- #
14
- # Idempotent; safe to re-run.
15
-
16
- set -euo pipefail
17
-
18
- OPENCLAW_HOME="${1:-${OPENCLAW_HOME:-$HOME/.openclaw}}"
19
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
- PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21
- EXT_SRC="$PKG_ROOT/consumers/openclaw-ext"
22
- EXT_DEST="$OPENCLAW_HOME/extensions/aquifer-memory"
23
-
24
- if [[ ! -d "$EXT_SRC" ]]; then
25
- echo "error: $EXT_SRC not found (expected inside the Aquifer package)" >&2
26
- exit 1
27
- fi
28
-
29
- if [[ ! -d "$OPENCLAW_HOME" ]]; then
30
- echo "error: OPENCLAW_HOME=$OPENCLAW_HOME not found" >&2
31
- exit 1
32
- fi
33
-
34
- mkdir -p "$OPENCLAW_HOME/extensions"
35
-
36
- if [[ -L "$EXT_DEST" || -e "$EXT_DEST" ]]; then
37
- echo "note: $EXT_DEST already exists — replacing"
38
- rm -rf "$EXT_DEST"
39
- fi
40
-
41
- ln -s "$EXT_SRC" "$EXT_DEST"
42
- echo "ok: linked $EXT_DEST → $EXT_SRC"
43
-
44
- cat <<'EOF'
45
-
46
- Next steps:
47
- 1. Edit $OPENCLAW_HOME/.env and set:
48
- DATABASE_URL=postgresql://user:pass@host:5432/db
49
- EMBED_PROVIDER=ollama # or openai
50
- AQUIFER_LLM_PROVIDER=minimax # or openai / openrouter / opencode
51
- MINIMAX_API_KEY=... # (or the key for your chosen provider)
52
- # Optional:
53
- AQUIFER_SCHEMA=my_namespace
54
- AQUIFER_PERSONA=/path/to/host-local/persona-module
55
- 2. Restart OpenClaw:
56
- systemctl --user restart openclaw-gateway
57
- 3. Verify:
58
- journalctl --user -u openclaw-gateway -f | grep aquifer-memory
59
- EOF
@@ -1,45 +0,0 @@
1
- {
2
- "version": 1,
3
- "queries": [
4
- {
5
- "id": "q-001",
6
- "lang": "en",
7
- "text": "How do I set up Aquifer memory storage with PostgreSQL?"
8
- },
9
- {
10
- "id": "q-002",
11
- "lang": "en",
12
- "text": "What is the difference between memory_search and session_recall in Aquifer?"
13
- },
14
- {
15
- "id": "q-003",
16
- "lang": "zh",
17
- "text": "Aquifer 的 session recall 是怎麼做 hybrid 檢索的?"
18
- },
19
- {
20
- "id": "q-004",
21
- "lang": "zh",
22
- "text": "為什麼 zhcfg 會依賴 jieba 或 zhparser?"
23
- },
24
- {
25
- "id": "q-005",
26
- "lang": "mixed",
27
- "text": "How to debug fts-zhcfg pipeline 在 jieba migration 後失敗的問題?"
28
- },
29
- {
30
- "id": "q-006",
31
- "lang": "mixed",
32
- "text": "memory_search 找不到結果時,應該先看哪個 log 或 table?"
33
- },
34
- {
35
- "id": "q-007",
36
- "lang": "en",
37
- "text": "How does hybrid-rerank differ from hybrid mode in retro recall bench?"
38
- },
39
- {
40
- "id": "q-008",
41
- "lang": "zh",
42
- "text": "Aquifer 初始化後要如何驗證 embeddings pipeline 有正常工作?"
43
- }
44
- ]
45
- }