@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.
- package/.env.example +23 -0
- package/README.md +96 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +374 -39
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +131 -7
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +44 -4
- package/consumers/shared/config.js +28 -0
- package/consumers/shared/factory.js +2 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +53 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +384 -18
- package/core/finalization-review.js +319 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +69 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +456 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +31 -9
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -119
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Backfill canonical_key_v2 for legacy insights rows.
|
|
6
|
+
*
|
|
7
|
+
* Pre-1.5.3 rows (those predating the Phase 2 C1 canonical-identity
|
|
8
|
+
* layer) carry `canonical_key_v2 IS NULL`, so they never match the
|
|
9
|
+
* canonical lookup inside commitInsight and never participate in the
|
|
10
|
+
* revision/supersede path. This script fills the key deterministically
|
|
11
|
+
* from `title` using the same normalization and hashing functions the
|
|
12
|
+
* writer uses, so backfilled rows behave identically to a freshly
|
|
13
|
+
* written row whose LLM extractor happened to emit a canonicalClaim
|
|
14
|
+
* equal to its title.
|
|
15
|
+
*
|
|
16
|
+
* Why JS not SQL: pgcrypto is NOT a default-installed extension in
|
|
17
|
+
* our production PG (verified 2026-04-20). Even with pgcrypto, matching
|
|
18
|
+
* JS's Unicode NFKC normalization in pure SQL is fragile. Single source
|
|
19
|
+
* of truth lives in core/insights.js; this script reuses it.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent: every UPDATE is guarded by WHERE canonical_key_v2 IS NULL,
|
|
22
|
+
* so reruns and concurrent live writers converge cleanly.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { Pool } = require('pg');
|
|
26
|
+
const { defaultCanonicalKey } = require('../core/insights');
|
|
27
|
+
|
|
28
|
+
const BACKFILL_METADATA_PATCH = { canonicalBackfill: 'title_deterministic' };
|
|
29
|
+
|
|
30
|
+
function printUsageAndExit(code = 0) {
|
|
31
|
+
const usage = [
|
|
32
|
+
'Usage: node scripts/backfill-canonical-key.js --schema <name> [options]',
|
|
33
|
+
'',
|
|
34
|
+
'Required:',
|
|
35
|
+
' --schema <name> Target schema (e.g. miranda, jenny)',
|
|
36
|
+
' --agent <id> Limit to one agent (or use --all-agents)',
|
|
37
|
+
'',
|
|
38
|
+
'Optional:',
|
|
39
|
+
' --all-agents Backfill across all agents in the tenant',
|
|
40
|
+
' (mutually exclusive with --agent)',
|
|
41
|
+
' --tenant-id <id> Default: $AQUIFER_TENANT_ID or "default"',
|
|
42
|
+
' --batch-size <N> Rows per batch (1..1000, default 50)',
|
|
43
|
+
' --dry-run Print would-updates, do not execute',
|
|
44
|
+
' -h, --help Show this help',
|
|
45
|
+
'',
|
|
46
|
+
'Env:',
|
|
47
|
+
' DATABASE_URL Postgres connection string (required)',
|
|
48
|
+
' AQUIFER_TENANT_ID Fallback tenant id',
|
|
49
|
+
'',
|
|
50
|
+
].join('\n');
|
|
51
|
+
(code === 0 ? console.log : console.error)(usage);
|
|
52
|
+
process.exit(code);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const args = {
|
|
57
|
+
schema: null,
|
|
58
|
+
agent: null,
|
|
59
|
+
allAgents: false,
|
|
60
|
+
tenantId: process.env.AQUIFER_TENANT_ID || 'default',
|
|
61
|
+
batchSize: 50,
|
|
62
|
+
dryRun: false,
|
|
63
|
+
help: false,
|
|
64
|
+
};
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i], v = argv[i + 1];
|
|
67
|
+
if (a === '--schema') { args.schema = v; i++; }
|
|
68
|
+
else if (a === '--agent') { args.agent = v; i++; }
|
|
69
|
+
else if (a === '--all-agents') { args.allAgents = true; }
|
|
70
|
+
else if (a === '--tenant-id') { args.tenantId = v; i++; }
|
|
71
|
+
else if (a === '--batch-size') {
|
|
72
|
+
const n = parseInt(v, 10);
|
|
73
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
74
|
+
console.error(`--batch-size must be an integer >= 1, got: ${v}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
args.batchSize = Math.min(n, 1000);
|
|
78
|
+
i++;
|
|
79
|
+
} else if (a === '--dry-run') { args.dryRun = true; }
|
|
80
|
+
else if (a === '-h' || a === '--help') { args.help = true; }
|
|
81
|
+
else {
|
|
82
|
+
console.error(`Unknown argument: ${a}`);
|
|
83
|
+
printUsageAndExit(2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return args;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validate(args) {
|
|
90
|
+
if (args.help) printUsageAndExit(0);
|
|
91
|
+
if (!args.schema) {
|
|
92
|
+
console.error('Missing required --schema');
|
|
93
|
+
printUsageAndExit(2);
|
|
94
|
+
}
|
|
95
|
+
if (!args.agent && !args.allAgents) {
|
|
96
|
+
console.error('Must specify --agent <id> or --all-agents');
|
|
97
|
+
printUsageAndExit(2);
|
|
98
|
+
}
|
|
99
|
+
if (args.agent && args.allAgents) {
|
|
100
|
+
console.error('--agent and --all-agents are mutually exclusive');
|
|
101
|
+
printUsageAndExit(2);
|
|
102
|
+
}
|
|
103
|
+
if (!process.env.DATABASE_URL) {
|
|
104
|
+
console.error('DATABASE_URL is required');
|
|
105
|
+
printUsageAndExit(2);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Safe schema identifier quoting — same pattern as
|
|
110
|
+
// scripts/extract-insights-from-recent-sessions.js:218-219.
|
|
111
|
+
const qi = (s) => `"${String(s).replace(/"/g, '""')}"`;
|
|
112
|
+
|
|
113
|
+
function truncateForLog(s, n = 60) {
|
|
114
|
+
if (typeof s !== 'string') return '';
|
|
115
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
const args = parseArgs(process.argv.slice(2));
|
|
120
|
+
validate(args);
|
|
121
|
+
|
|
122
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
123
|
+
|
|
124
|
+
const schemaIdent = qi(args.schema);
|
|
125
|
+
const agentLabel = args.allAgents ? '(all)' : args.agent;
|
|
126
|
+
console.log(
|
|
127
|
+
`[backfill] tenant=${args.tenantId} schema=${args.schema} `
|
|
128
|
+
+ `agent=${agentLabel} batch_size=${args.batchSize} dry_run=${args.dryRun}`
|
|
129
|
+
);
|
|
130
|
+
if (args.dryRun) {
|
|
131
|
+
console.log('[backfill] DRY RUN — no updates will be executed.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const whereClauses = [
|
|
135
|
+
'canonical_key_v2 IS NULL',
|
|
136
|
+
`status = 'active'`,
|
|
137
|
+
'tenant_id = $1',
|
|
138
|
+
];
|
|
139
|
+
const whereParams = [args.tenantId];
|
|
140
|
+
if (!args.allAgents) {
|
|
141
|
+
whereClauses.push(`agent_id = $${whereParams.length + 1}`);
|
|
142
|
+
whereParams.push(args.agent);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let totalBackfilled = 0;
|
|
146
|
+
let totalSkipped = 0;
|
|
147
|
+
let totalAlreadySet = 0;
|
|
148
|
+
let batchNum = 0;
|
|
149
|
+
// Id watermark: prevents infinite loop when a batch yields no state
|
|
150
|
+
// transitions (dry-run always, or when every row has empty title, or
|
|
151
|
+
// when races cause 0 UPDATEs). WHERE id > $N advances the cursor even
|
|
152
|
+
// if the current rows aren't removed from the candidate set.
|
|
153
|
+
let lastId = 0;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
while (true) {
|
|
157
|
+
batchNum += 1;
|
|
158
|
+
|
|
159
|
+
const selectSql =
|
|
160
|
+
`SELECT id, tenant_id, agent_id, insight_type, title
|
|
161
|
+
FROM ${schemaIdent}.insights
|
|
162
|
+
WHERE ${whereClauses.join(' AND ')}
|
|
163
|
+
AND id > $${whereParams.length + 1}
|
|
164
|
+
ORDER BY id ASC
|
|
165
|
+
LIMIT $${whereParams.length + 2}`;
|
|
166
|
+
const res = await pool.query(selectSql, [...whereParams, lastId, args.batchSize]);
|
|
167
|
+
|
|
168
|
+
if (res.rowCount === 0) break;
|
|
169
|
+
lastId = Number(res.rows[res.rows.length - 1].id);
|
|
170
|
+
|
|
171
|
+
let batchBackfilled = 0;
|
|
172
|
+
let batchSkipped = 0;
|
|
173
|
+
let batchAlreadySet = 0;
|
|
174
|
+
|
|
175
|
+
for (const row of res.rows) {
|
|
176
|
+
const title = typeof row.title === 'string' ? row.title.trim() : '';
|
|
177
|
+
if (!title) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[backfill] skip id=${row.id} empty or whitespace title`
|
|
180
|
+
);
|
|
181
|
+
batchSkipped += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const canonicalKey = defaultCanonicalKey({
|
|
186
|
+
tenantId: row.tenant_id,
|
|
187
|
+
agentId: row.agent_id,
|
|
188
|
+
type: row.insight_type,
|
|
189
|
+
canonicalClaim: title,
|
|
190
|
+
entities: [],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (args.dryRun) {
|
|
194
|
+
console.log(
|
|
195
|
+
`[backfill] would_update id=${row.id} agent=${row.agent_id} `
|
|
196
|
+
+ `type=${row.insight_type} title="${truncateForLog(title)}"`
|
|
197
|
+
);
|
|
198
|
+
batchBackfilled += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const updSql =
|
|
203
|
+
`UPDATE ${schemaIdent}.insights
|
|
204
|
+
SET canonical_key_v2 = $1,
|
|
205
|
+
metadata = metadata || $2::jsonb,
|
|
206
|
+
updated_at = now()
|
|
207
|
+
WHERE id = $3 AND canonical_key_v2 IS NULL`;
|
|
208
|
+
const upd = await pool.query(updSql, [
|
|
209
|
+
canonicalKey,
|
|
210
|
+
JSON.stringify(BACKFILL_METADATA_PATCH),
|
|
211
|
+
row.id,
|
|
212
|
+
]);
|
|
213
|
+
if (upd.rowCount === 0) {
|
|
214
|
+
batchAlreadySet += 1;
|
|
215
|
+
} else {
|
|
216
|
+
batchBackfilled += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
totalBackfilled += batchBackfilled;
|
|
221
|
+
totalSkipped += batchSkipped;
|
|
222
|
+
totalAlreadySet += batchAlreadySet;
|
|
223
|
+
|
|
224
|
+
console.log(
|
|
225
|
+
`[backfill] batch ${batchNum}: selected=${res.rowCount} `
|
|
226
|
+
+ `${args.dryRun ? 'would_backfill' : 'backfilled'}=${batchBackfilled} `
|
|
227
|
+
+ `skipped=${batchSkipped} already_set=${batchAlreadySet}`
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// No all-skip-break guard needed: the `id > lastId` cursor
|
|
231
|
+
// advances past skipped rows each iteration, so an empty-title
|
|
232
|
+
// row in an otherwise healthy batch doesn't trap the loop.
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const verb = args.dryRun ? 'would_backfill' : 'backfilled';
|
|
236
|
+
console.log(
|
|
237
|
+
`[backfill] DONE${args.dryRun ? ' dry_run' : ''} total: `
|
|
238
|
+
+ `${verb}=${totalBackfilled} skipped=${totalSkipped} `
|
|
239
|
+
+ `already_set=${totalAlreadySet}`
|
|
240
|
+
);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error('[backfill] fatal:', e.stack || e.message);
|
|
243
|
+
await pool.end().catch(() => {});
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await pool.end().catch(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main();
|