@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,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"db": {
|
|
3
|
+
"url": "postgresql://user:password@localhost:5432/mydb",
|
|
4
|
+
"max": 10
|
|
5
|
+
},
|
|
6
|
+
"schema": "aquifer",
|
|
7
|
+
"tenantId": "default",
|
|
8
|
+
"defaults": {
|
|
9
|
+
"agentId": "main",
|
|
10
|
+
"source": "api"
|
|
11
|
+
},
|
|
12
|
+
"memory": {
|
|
13
|
+
"servingMode": "legacy",
|
|
14
|
+
"activeScopeKey": null,
|
|
15
|
+
"activeScopePath": null
|
|
16
|
+
},
|
|
17
|
+
"embed": {
|
|
18
|
+
"baseUrl": "http://localhost:11434/v1",
|
|
19
|
+
"model": "bge-m3",
|
|
20
|
+
"dim": null
|
|
21
|
+
},
|
|
22
|
+
"llm": {
|
|
23
|
+
"baseUrl": null,
|
|
24
|
+
"model": null
|
|
25
|
+
},
|
|
26
|
+
"entities": {
|
|
27
|
+
"enabled": false,
|
|
28
|
+
"scope": "default"
|
|
29
|
+
},
|
|
30
|
+
"rerank": {
|
|
31
|
+
"enabled": false,
|
|
32
|
+
"provider": null
|
|
33
|
+
}
|
|
34
|
+
}
|
package/consumers/claude-code.js
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Claude Code host adapter.
|
|
5
5
|
//
|
|
6
|
-
// Generic entry points for CC-side afterburn hooks. No persona logic —
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `entityParseFn`.
|
|
6
|
+
// Generic entry points for CC-side afterburn hooks. No persona logic — callers
|
|
7
|
+
// construct any persona-specific hooks outside this package and inject them via
|
|
8
|
+
// `postProcess`, `summaryFn`, `entityParseFn`, or `contextInjector`.
|
|
10
9
|
//
|
|
11
10
|
// API:
|
|
12
11
|
// runEnrich({ aquifer, sessionId, agentId, ... })
|
|
@@ -16,9 +15,9 @@
|
|
|
16
15
|
// runBackfill({ aquifer, sessionIds, ... })
|
|
17
16
|
// Iterate enrich() over pending sessions (for catch-up after a gap).
|
|
18
17
|
//
|
|
19
|
-
// runContextInject({
|
|
20
|
-
// Return
|
|
21
|
-
//
|
|
18
|
+
// runContextInject({ contextInjector, ... })
|
|
19
|
+
// Return a host-specific system context string for a CC session start
|
|
20
|
+
// hook. The injector is supplied by the deployment, not by Aquifer core.
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -106,12 +105,13 @@ async function runBackfill({
|
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
/**
|
|
109
|
-
* Build
|
|
110
|
-
*
|
|
108
|
+
* Build a host-specific system context for a CC SessionStart hook.
|
|
109
|
+
* The caller supplies the context injector so this public adapter stays generic.
|
|
111
110
|
*/
|
|
112
111
|
async function runContextInject(opts = {}) {
|
|
113
|
-
const
|
|
114
|
-
|
|
112
|
+
const injector = opts.contextInjector || opts.computeInjection;
|
|
113
|
+
if (typeof injector !== 'function') throw new Error('runContextInject: contextInjector is required');
|
|
114
|
+
return injector(opts);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
module.exports = { runEnrich, runBackfill, runContextInject };
|
package/consumers/cli.js
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* aquifer backfill [options] Enrich pending sessions
|
|
12
12
|
* aquifer stats [options] Show database statistics
|
|
13
13
|
* aquifer export [options] Export sessions
|
|
14
|
+
* aquifer operator ... Run operator-safe consolidation jobs
|
|
14
15
|
* aquifer mcp Start MCP server
|
|
15
16
|
*/
|
|
16
17
|
|
|
18
|
+
const fs = require('fs');
|
|
17
19
|
const { createAquiferFromConfig } = require('./shared/factory');
|
|
20
|
+
const { formatRecallResults } = require('./shared/recall-format');
|
|
18
21
|
|
|
19
22
|
function formatDate(value, fallback) {
|
|
20
23
|
if (!value) return fallback;
|
|
@@ -36,6 +39,98 @@ function parsePositiveInt(value, fallback) {
|
|
|
36
39
|
return Math.max(1, parsed);
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
function parseScopePath(value) {
|
|
43
|
+
if (!value) return undefined;
|
|
44
|
+
const parts = String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
45
|
+
return parts.length > 0 ? parts : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCsvList(value) {
|
|
49
|
+
if (!value) return undefined;
|
|
50
|
+
const parts = String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
51
|
+
return parts.length > 0 ? parts : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasQuickstartEmbedConfig(env) {
|
|
55
|
+
return !!(
|
|
56
|
+
env.EMBED_PROVIDER
|
|
57
|
+
|| (env.AQUIFER_EMBED_BASE_URL && env.AQUIFER_EMBED_MODEL)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printQuickstartFailure(title, detailLines = []) {
|
|
62
|
+
console.error(` FAIL — ${title}`);
|
|
63
|
+
for (const line of detailLines) {
|
|
64
|
+
if (line) console.error(` ${line}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildQuickstartSetupHints(env, detected, err) {
|
|
69
|
+
const hints = [];
|
|
70
|
+
const message = err && err.message ? err.message : String(err || 'Unknown error');
|
|
71
|
+
const hasDb = !!(env.DATABASE_URL || env.AQUIFER_DB_URL || detected.DATABASE_URL);
|
|
72
|
+
const hasEmbed = hasQuickstartEmbedConfig(env)
|
|
73
|
+
|| !!detected.EMBED_PROVIDER;
|
|
74
|
+
|
|
75
|
+
if (/Database URL is required/i.test(message)) {
|
|
76
|
+
hints.push('Quickstart could not find a PostgreSQL connection.');
|
|
77
|
+
if (!hasDb) {
|
|
78
|
+
hints.push('If you expect local defaults, make sure PostgreSQL is running on localhost:5432.');
|
|
79
|
+
hints.push('Otherwise set DATABASE_URL or AQUIFER_DB_URL explicitly and run quickstart again.');
|
|
80
|
+
}
|
|
81
|
+
return hints;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (/OPENAI_API_KEY/i.test(message)) {
|
|
85
|
+
hints.push('OpenAI embeddings were selected, but OPENAI_API_KEY is not set.');
|
|
86
|
+
hints.push('Export OPENAI_API_KEY or switch EMBED_PROVIDER back to ollama for local quickstart.');
|
|
87
|
+
return hints;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!hasDb || !hasEmbed) {
|
|
91
|
+
hints.push('Quickstart is missing part of the local setup.');
|
|
92
|
+
if (!hasDb) hints.push('PostgreSQL was not autodetected and no DATABASE_URL is set.');
|
|
93
|
+
if (!hasEmbed) hints.push('No embedding provider was autodetected and no embed env is set.');
|
|
94
|
+
hints.push('Try `docker compose up -d`, then run `npx aquifer quickstart` again.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
hints.push(`Raw error: ${message}`);
|
|
98
|
+
return hints;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildQuickstartRecallHints(err) {
|
|
102
|
+
const message = err && err.message ? err.message : String(err || 'Unknown error');
|
|
103
|
+
|
|
104
|
+
if (/requires config\.embed\.fn|EMBED_PROVIDER/i.test(message)) {
|
|
105
|
+
return [
|
|
106
|
+
'Quickstart reached recall, but embeddings are not configured.',
|
|
107
|
+
'Set EMBED_PROVIDER=ollama for local Ollama, or EMBED_PROVIDER=openai with OPENAI_API_KEY.',
|
|
108
|
+
`Raw error: ${message}`,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (/OPENAI_API_KEY/i.test(message)) {
|
|
113
|
+
return [
|
|
114
|
+
'Recall is configured to use OpenAI embeddings, but OPENAI_API_KEY is missing.',
|
|
115
|
+
'Export OPENAI_API_KEY and rerun quickstart.',
|
|
116
|
+
`Raw error: ${message}`,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (/ECONNREFUSED|ENOTFOUND|fetch failed|connect/i.test(message)) {
|
|
121
|
+
return [
|
|
122
|
+
'Aquifer could not reach the embedding service during recall.',
|
|
123
|
+
'If you expect local Ollama, make sure it is running and the model is available.',
|
|
124
|
+
`Raw error: ${message}`,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
'Aquifer could not recall the quickstart test session.',
|
|
130
|
+
`Raw error: ${message}`,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
39
134
|
// ---------------------------------------------------------------------------
|
|
40
135
|
// Argument parser (minimal, no deps)
|
|
41
136
|
// ---------------------------------------------------------------------------
|
|
@@ -43,7 +138,14 @@ function parsePositiveInt(value, fallback) {
|
|
|
43
138
|
function parseArgs(argv) {
|
|
44
139
|
const args = { _: [], flags: {} };
|
|
45
140
|
// Flags that take a value (not boolean)
|
|
46
|
-
const VALUE_FLAGS = new Set([
|
|
141
|
+
const VALUE_FLAGS = new Set([
|
|
142
|
+
'limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status',
|
|
143
|
+
'concurrency', 'entities', 'entity-mode', 'mode', 'session-id', 'memory-id', 'canonical-key',
|
|
144
|
+
'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
|
|
145
|
+
'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
|
|
146
|
+
'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
|
|
147
|
+
'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
|
|
148
|
+
]);
|
|
47
149
|
for (let i = 0; i < argv.length; i++) {
|
|
48
150
|
if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
|
|
49
151
|
if (argv[i].startsWith('--')) {
|
|
@@ -76,13 +178,13 @@ async function cmdRecall(aquifer, args) {
|
|
|
76
178
|
process.exit(1);
|
|
77
179
|
}
|
|
78
180
|
|
|
79
|
-
const recallOpts = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
181
|
+
const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
|
|
182
|
+
if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
|
|
183
|
+
if (args.flags.source) recallOpts.source = args.flags.source;
|
|
184
|
+
if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
|
|
185
|
+
if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
|
|
186
|
+
if (args.flags['active-scope-key']) recallOpts.activeScopeKey = args.flags['active-scope-key'];
|
|
187
|
+
if (args.flags['active-scope-path']) recallOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
|
|
86
188
|
if (args.flags.entities) {
|
|
87
189
|
recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
|
|
88
190
|
recallOpts.entityMode = args.flags['entity-mode'] || 'any';
|
|
@@ -94,26 +196,20 @@ async function cmdRecall(aquifer, args) {
|
|
|
94
196
|
return;
|
|
95
197
|
}
|
|
96
198
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < results.length; i++) {
|
|
103
|
-
const r = results[i];
|
|
104
|
-
const ss = r.structuredSummary || {};
|
|
105
|
-
const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
|
|
106
|
-
const date = formatDate(r.startedAt, '?');
|
|
107
|
-
console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
|
|
108
|
-
if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
|
|
109
|
-
if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
|
|
110
|
-
console.log();
|
|
111
|
-
}
|
|
199
|
+
console.log(formatRecallResults(results, {
|
|
200
|
+
query,
|
|
201
|
+
showScore: true,
|
|
202
|
+
showExplain: !!args.flags.explain,
|
|
203
|
+
}));
|
|
112
204
|
}
|
|
113
205
|
|
|
114
206
|
async function cmdFeedback(aquifer, args) {
|
|
115
207
|
const sessionId = args.flags['session-id'] || args._[1];
|
|
116
208
|
const verdict = args.flags.verdict;
|
|
209
|
+
if (args.flags['memory-id'] || args.flags['canonical-key'] || args.flags['feedback-type']) {
|
|
210
|
+
console.error('Use `aquifer memory-feedback` for curated memory feedback.');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
117
213
|
|
|
118
214
|
if (!sessionId || !verdict) {
|
|
119
215
|
console.error('Usage: aquifer feedback --session-id ID --verdict helpful|unhelpful [--note TEXT] [--agent-id ID]');
|
|
@@ -133,6 +229,82 @@ async function cmdFeedback(aquifer, args) {
|
|
|
133
229
|
}
|
|
134
230
|
}
|
|
135
231
|
|
|
232
|
+
async function cmdMemoryFeedback(aquifer, args) {
|
|
233
|
+
const memoryId = args.flags['memory-id'];
|
|
234
|
+
const canonicalKey = args.flags['canonical-key'];
|
|
235
|
+
const feedbackType = args.flags['feedback-type'];
|
|
236
|
+
|
|
237
|
+
if ((!memoryId && !canonicalKey) || !feedbackType) {
|
|
238
|
+
console.error('Usage: aquifer memory-feedback (--memory-id ID | --canonical-key KEY) --feedback-type helpful|confirm|irrelevant|scope_mismatch|stale|incorrect [--note TEXT] [--agent-id ID]');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const result = await aquifer.memoryFeedback({
|
|
243
|
+
memoryId: memoryId || undefined,
|
|
244
|
+
canonicalKey: canonicalKey || undefined,
|
|
245
|
+
}, {
|
|
246
|
+
feedbackType,
|
|
247
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
248
|
+
note: args.flags.note || undefined,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (args.flags.json) {
|
|
252
|
+
console.log(JSON.stringify(result, null, 2));
|
|
253
|
+
} else {
|
|
254
|
+
const target = result.canonicalKey || result.memoryId;
|
|
255
|
+
console.log(`Memory feedback: ${result.feedbackType} (${target})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function cmdEvidenceRecall(aquifer, args) {
|
|
260
|
+
const query = args._.slice(1).join(' ');
|
|
261
|
+
if (!query) {
|
|
262
|
+
console.error('Usage: aquifer evidence-recall <query> [--limit N] [--agent-id ID] [--allow-unsafe-debug] [--json]');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const recallOpts = { limit: parsePositiveInt(args.flags.limit, 5) };
|
|
267
|
+
if (args.flags['agent-id']) recallOpts.agentId = args.flags['agent-id'];
|
|
268
|
+
if (args.flags.source) recallOpts.source = args.flags.source;
|
|
269
|
+
if (args.flags['date-from']) recallOpts.dateFrom = args.flags['date-from'];
|
|
270
|
+
if (args.flags['date-to']) recallOpts.dateTo = args.flags['date-to'];
|
|
271
|
+
if (args.flags.entities) {
|
|
272
|
+
recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
|
|
273
|
+
recallOpts.entityMode = args.flags['entity-mode'] || 'any';
|
|
274
|
+
}
|
|
275
|
+
if (args.flags.mode) recallOpts.mode = args.flags.mode;
|
|
276
|
+
if (args.flags['allow-unsafe-debug']) recallOpts.allowUnsafeDebug = true;
|
|
277
|
+
|
|
278
|
+
const results = await aquifer.evidenceRecall(query, recallOpts);
|
|
279
|
+
|
|
280
|
+
if (args.flags.json) {
|
|
281
|
+
console.log(JSON.stringify(results, null, 2));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log(formatRecallResults(results, {
|
|
286
|
+
query,
|
|
287
|
+
showScore: true,
|
|
288
|
+
showExplain: !!args.flags.explain,
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function cmdFeedbackStats(aquifer, args) {
|
|
293
|
+
const stats = await aquifer.feedbackStats({
|
|
294
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
295
|
+
dateFrom: args.flags['date-from'] || undefined,
|
|
296
|
+
dateTo: args.flags['date-to'] || undefined,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (args.flags.json) {
|
|
300
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
301
|
+
} else {
|
|
302
|
+
console.log(`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`);
|
|
303
|
+
console.log(`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`);
|
|
304
|
+
console.log(`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
136
308
|
async function cmdBackfill(aquifer, args) {
|
|
137
309
|
const limit = parsePositiveInt(args.flags.limit, 100);
|
|
138
310
|
const dryRun = !!args.flags['dry-run'];
|
|
@@ -209,13 +381,39 @@ async function cmdQuickstart(aquifer) {
|
|
|
209
381
|
skipSummary: true,
|
|
210
382
|
skipEntities: true,
|
|
211
383
|
});
|
|
384
|
+
if (Array.isArray(enrichResult.warnings) && enrichResult.warnings.length > 0) {
|
|
385
|
+
printQuickstartFailure('embedding step returned warnings.', [
|
|
386
|
+
'Quickstart expects turn embeddings to succeed cleanly.',
|
|
387
|
+
...enrichResult.warnings.map(w => `Warning: ${w}`),
|
|
388
|
+
]);
|
|
389
|
+
process.exitCode = 1;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!Number.isFinite(enrichResult.turnsEmbedded) || enrichResult.turnsEmbedded <= 0) {
|
|
393
|
+
printQuickstartFailure('0 turns were embedded.', [
|
|
394
|
+
'The quickstart test session contains user turns, so this usually means the embedding setup is not working.',
|
|
395
|
+
'Check EMBED_PROVIDER / AQUIFER_EMBED_* settings, or make sure Ollama/OpenAI is reachable.',
|
|
396
|
+
]);
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
212
400
|
console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded\n`);
|
|
213
401
|
|
|
214
402
|
// 4. Recall
|
|
215
403
|
console.log('4/5 Recalling "PostgreSQL memory store"...');
|
|
216
|
-
|
|
404
|
+
let results;
|
|
405
|
+
try {
|
|
406
|
+
results = await aquifer.recall('PostgreSQL memory store', { limit: 3 });
|
|
407
|
+
} catch (err) {
|
|
408
|
+
printQuickstartFailure('recall step failed.', buildQuickstartRecallHints(err));
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
217
412
|
if (results.length === 0) {
|
|
218
|
-
|
|
413
|
+
printQuickstartFailure('quickstart could not recall its own test session.', [
|
|
414
|
+
'The write step succeeded, but the test query returned no matches.',
|
|
415
|
+
'This usually means the embedding path is misconfigured or the embed service is not reachable.',
|
|
416
|
+
]);
|
|
219
417
|
process.exitCode = 1;
|
|
220
418
|
return;
|
|
221
419
|
}
|
|
@@ -253,14 +451,17 @@ async function cmdQuickstart(aquifer) {
|
|
|
253
451
|
}
|
|
254
452
|
|
|
255
453
|
async function cmdBootstrap(aquifer, args) {
|
|
256
|
-
const
|
|
257
|
-
agentId: args.flags['agent-id'] || undefined,
|
|
258
|
-
source: args.flags.source || undefined,
|
|
454
|
+
const bootstrapOpts = {
|
|
259
455
|
limit: parsePositiveInt(args.flags.limit, 5),
|
|
260
|
-
lookbackDays: parsePositiveInt(args.flags['lookback-days'], 14),
|
|
261
456
|
maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
|
|
262
457
|
format: args.flags.json ? 'structured' : 'text',
|
|
263
|
-
}
|
|
458
|
+
};
|
|
459
|
+
if (args.flags['agent-id']) bootstrapOpts.agentId = args.flags['agent-id'];
|
|
460
|
+
if (args.flags.source) bootstrapOpts.source = args.flags.source;
|
|
461
|
+
if (args.flags['lookback-days']) bootstrapOpts.lookbackDays = parsePositiveInt(args.flags['lookback-days'], 14);
|
|
462
|
+
if (args.flags['active-scope-key']) bootstrapOpts.activeScopeKey = args.flags['active-scope-key'];
|
|
463
|
+
if (args.flags['active-scope-path']) bootstrapOpts.activeScopePath = parseScopePath(args.flags['active-scope-path']);
|
|
464
|
+
const result = await aquifer.bootstrap(bootstrapOpts);
|
|
264
465
|
|
|
265
466
|
if (args.flags.json) {
|
|
266
467
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -276,6 +477,85 @@ async function cmdBootstrap(aquifer, args) {
|
|
|
276
477
|
}
|
|
277
478
|
}
|
|
278
479
|
|
|
480
|
+
async function cmdOperator(aquifer, args) {
|
|
481
|
+
const operatorVerb = args._[1] || 'compaction';
|
|
482
|
+
const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
483
|
+
|
|
484
|
+
if (operatorVerb === 'archive-distill') {
|
|
485
|
+
const inputPath = args.flags.input || args._[2];
|
|
486
|
+
if (!inputPath) {
|
|
487
|
+
console.error('Usage: aquifer operator archive-distill --input /path/to/archive.json [--authority verified_summary] [--json]');
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
const archiveSnapshot = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
491
|
+
const result = await aquifer.memory.consolidation.runJob({
|
|
492
|
+
job: 'archive-distill',
|
|
493
|
+
archiveSnapshot,
|
|
494
|
+
authority: args.flags.authority || undefined,
|
|
495
|
+
scopeKind: args.flags['scope-kind'] || undefined,
|
|
496
|
+
scopeKey: args.flags['scope-key'] || undefined,
|
|
497
|
+
contextKey: args.flags['context-key'] || undefined,
|
|
498
|
+
topicKey: args.flags['topic-key'] || undefined,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (args.flags.json) {
|
|
502
|
+
console.log(JSON.stringify(result, null, 2));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const sessionCount = Array.isArray(archiveSnapshot.sessions) ? archiveSnapshot.sessions.length : 0;
|
|
507
|
+
console.log(`Archive distill planned from ${sessionCount} sessions.`);
|
|
508
|
+
console.log(`Candidates: ${result.candidates.length}`);
|
|
509
|
+
console.log('Visibility: recall/bootstrap hidden until a separate promotion step.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
|
|
514
|
+
console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const cadence = args.flags.cadence
|
|
519
|
+
|| (cadenceVerbs.has(operatorVerb) ? operatorVerb : args._[2])
|
|
520
|
+
|| 'manual';
|
|
521
|
+
const result = await aquifer.memory.consolidation.runJob({
|
|
522
|
+
job: 'compaction',
|
|
523
|
+
cadence,
|
|
524
|
+
periodStart: args.flags['period-start'] || undefined,
|
|
525
|
+
periodEnd: args.flags['period-end'] || undefined,
|
|
526
|
+
policyVersion: args.flags['policy-version'] || undefined,
|
|
527
|
+
workerId: args.flags['worker-id'] || undefined,
|
|
528
|
+
applyToken: args.flags['apply-token'] || undefined,
|
|
529
|
+
claimLeaseSeconds: args.flags['claim-lease-seconds']
|
|
530
|
+
? parsePositiveInt(args.flags['claim-lease-seconds'], undefined)
|
|
531
|
+
: undefined,
|
|
532
|
+
snapshotAsOf: args.flags['snapshot-as-of'] || undefined,
|
|
533
|
+
scopeKeys: parseCsvList(args.flags['scope-keys'] || args.flags['scope-key']),
|
|
534
|
+
limit: parsePositiveInt(args.flags.limit, 1000),
|
|
535
|
+
apply: args.flags.apply === true,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (args.flags.json) {
|
|
539
|
+
console.log(JSON.stringify(result, null, 2));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log(`${result.dryRun ? 'Planned' : 'Executed'} ${result.cadence} compaction window ${result.periodStart} -> ${result.periodEnd}`);
|
|
544
|
+
console.log(`Snapshot: ${result.snapshotCount} active rows${result.snapshotTruncated ? ' (snapshot limit reached)' : ''}`);
|
|
545
|
+
console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} aggregate candidates`);
|
|
546
|
+
if (result.dryRun) {
|
|
547
|
+
console.log('Mode: dry-run only. Re-run with --apply to write compaction_runs and lifecycle changes.');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (result.run) {
|
|
551
|
+
console.log(`Run: #${result.run.id} status=${result.run.status}`);
|
|
552
|
+
} else if (result.existingRun) {
|
|
553
|
+
console.log(`Existing run: #${result.existingRun.id} status=${result.existingRun.status} reason=${result.skipReason || 'claim_not_acquired'}`);
|
|
554
|
+
} else {
|
|
555
|
+
console.log(`No run claimed: ${result.skipReason || 'claim_not_acquired'}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
279
559
|
async function cmdExport(aquifer, args) {
|
|
280
560
|
const output = args.flags.output || null;
|
|
281
561
|
const limit = parsePositiveInt(args.flags.limit, 1000);
|
|
@@ -304,6 +584,10 @@ async function cmdExport(aquifer, args) {
|
|
|
304
584
|
}
|
|
305
585
|
}
|
|
306
586
|
|
|
587
|
+
async function cmdCompact(aquifer, args) {
|
|
588
|
+
return cmdOperator(aquifer, { ...args, _: ['operator', 'compaction', ...args._.slice(1)] });
|
|
589
|
+
}
|
|
590
|
+
|
|
307
591
|
// ---------------------------------------------------------------------------
|
|
308
592
|
// Main
|
|
309
593
|
// ---------------------------------------------------------------------------
|
|
@@ -314,16 +598,21 @@ async function main() {
|
|
|
314
598
|
console.log(`Usage: aquifer <command> [options]
|
|
315
599
|
|
|
316
600
|
Commands:
|
|
317
|
-
|
|
318
|
-
|
|
601
|
+
quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
|
|
602
|
+
migrate Run database migrations
|
|
319
603
|
recall <query> Search sessions (requires embed config)
|
|
604
|
+
evidence-recall <query> Search legacy session/evidence plane explicitly
|
|
320
605
|
feedback Record trust feedback on a session
|
|
606
|
+
memory-feedback Record curated memory feedback
|
|
607
|
+
feedback-stats Show trust feedback statistics and coverage
|
|
321
608
|
backfill Enrich pending sessions
|
|
609
|
+
operator ... Run operator-safe consolidation jobs
|
|
610
|
+
compact Plan or apply curated memory compaction
|
|
322
611
|
stats Show database statistics
|
|
323
612
|
export Export sessions as JSONL
|
|
324
613
|
bootstrap Show recent session context (for new session start)
|
|
325
|
-
|
|
326
|
-
|
|
614
|
+
ingest-opencode Import sessions from OpenCode's local SQLite DB
|
|
615
|
+
mcp Start MCP server
|
|
327
616
|
|
|
328
617
|
Options:
|
|
329
618
|
--limit N Limit results
|
|
@@ -334,22 +623,43 @@ Options:
|
|
|
334
623
|
--entities A,B,C Entity names (comma-separated, recall)
|
|
335
624
|
--entity-mode any|all Entity match mode (recall, default: any)
|
|
336
625
|
--session-id ID Session ID (feedback)
|
|
626
|
+
--memory-id ID Curated memory record ID (memory-feedback)
|
|
627
|
+
--canonical-key KEY Active curated memory canonical key (memory-feedback)
|
|
337
628
|
--verdict helpful|unhelpful Feedback verdict (feedback)
|
|
629
|
+
--feedback-type TYPE Curated memory feedback type
|
|
338
630
|
--note TEXT Feedback note (feedback)
|
|
631
|
+
--explain Show score breakdown per result (recall)
|
|
632
|
+
--allow-unsafe-debug Allow broad evidence-recall without audit boundary
|
|
339
633
|
--json JSON output
|
|
340
634
|
--dry-run Preview only (backfill)
|
|
341
635
|
--output PATH Output file (export)
|
|
342
636
|
--config PATH Config file path
|
|
343
637
|
--lookback-days N How far back in days (bootstrap, default: 14)
|
|
344
638
|
--max-chars N Max output characters (bootstrap, default: 4000)
|
|
639
|
+
--active-scope-key KEY Active curated memory scope key
|
|
640
|
+
--active-scope-path A,B Ordered curated scope path
|
|
641
|
+
--cadence manual|daily|weekly|monthly
|
|
642
|
+
--period-start ISO Compaction window start
|
|
643
|
+
--period-end ISO Compaction window end
|
|
644
|
+
--apply Apply compaction; default is dry-run
|
|
645
|
+
--scope-key A,B Limit compaction snapshot to specific scope keys
|
|
646
|
+
--snapshot-as-of ISO Read active snapshot as of a specific instant
|
|
647
|
+
--claim-lease-seconds N Override compaction apply lease
|
|
648
|
+
--input PATH Archive distill input JSON path
|
|
345
649
|
--db PATH OpenCode SQLite path (ingest-opencode)
|
|
346
650
|
--since YYYY-MM-DD Only ingest sessions after date (ingest-opencode)
|
|
347
|
-
--min-messages N Min user messages to ingest (ingest-opencode, default: 3)
|
|
651
|
+
--min-messages N Min user messages to ingest (ingest-opencode, default: 3)
|
|
652
|
+
|
|
653
|
+
Operator examples:
|
|
654
|
+
aquifer operator compaction daily --json
|
|
655
|
+
aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
|
|
656
|
+
aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
|
|
348
657
|
process.exit(0);
|
|
349
658
|
}
|
|
350
659
|
|
|
351
660
|
const command = argv[0];
|
|
352
661
|
const args = parseArgs(argv);
|
|
662
|
+
let quickstartDetected = {};
|
|
353
663
|
|
|
354
664
|
// MCP: delegate to mcp.js
|
|
355
665
|
if (command === 'mcp') {
|
|
@@ -383,10 +693,10 @@ Options:
|
|
|
383
693
|
// the operator to have set env explicitly.
|
|
384
694
|
if (command === 'quickstart') {
|
|
385
695
|
const { autodetectForQuickstart } = require('./shared/autodetect');
|
|
386
|
-
|
|
387
|
-
if (Object.keys(
|
|
696
|
+
quickstartDetected = await autodetectForQuickstart(process.env);
|
|
697
|
+
if (Object.keys(quickstartDetected).length > 0) {
|
|
388
698
|
console.log('Autodetected localhost services (env not set):');
|
|
389
|
-
for (const [k, v] of Object.entries(
|
|
699
|
+
for (const [k, v] of Object.entries(quickstartDetected)) {
|
|
390
700
|
console.log(` ${k}=${v}`);
|
|
391
701
|
process.env[k] = v;
|
|
392
702
|
}
|
|
@@ -394,7 +704,17 @@ Options:
|
|
|
394
704
|
}
|
|
395
705
|
}
|
|
396
706
|
|
|
397
|
-
|
|
707
|
+
let aquifer;
|
|
708
|
+
try {
|
|
709
|
+
aquifer = createAquiferFromConfig(configOverrides);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
if (command === 'quickstart') {
|
|
712
|
+
printQuickstartFailure('setup check failed before quickstart could start.', buildQuickstartSetupHints(process.env, quickstartDetected, err));
|
|
713
|
+
process.exit(1);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
398
718
|
|
|
399
719
|
try {
|
|
400
720
|
switch (command) {
|
|
@@ -407,12 +727,27 @@ Options:
|
|
|
407
727
|
case 'recall':
|
|
408
728
|
await cmdRecall(aquifer, args);
|
|
409
729
|
break;
|
|
730
|
+
case 'evidence-recall':
|
|
731
|
+
await cmdEvidenceRecall(aquifer, args);
|
|
732
|
+
break;
|
|
410
733
|
case 'feedback':
|
|
411
734
|
await cmdFeedback(aquifer, args);
|
|
412
735
|
break;
|
|
736
|
+
case 'memory-feedback':
|
|
737
|
+
await cmdMemoryFeedback(aquifer, args);
|
|
738
|
+
break;
|
|
739
|
+
case 'feedback-stats':
|
|
740
|
+
await cmdFeedbackStats(aquifer, args);
|
|
741
|
+
break;
|
|
413
742
|
case 'backfill':
|
|
414
743
|
await cmdBackfill(aquifer, args);
|
|
415
744
|
break;
|
|
745
|
+
case 'operator':
|
|
746
|
+
await cmdOperator(aquifer, args);
|
|
747
|
+
break;
|
|
748
|
+
case 'compact':
|
|
749
|
+
await cmdCompact(aquifer, args);
|
|
750
|
+
break;
|
|
416
751
|
case 'stats':
|
|
417
752
|
await cmdStats(aquifer, args);
|
|
418
753
|
break;
|