@shadowforge0/aquifer-memory 0.3.0 → 0.4.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/consumers/cli.js +39 -3
- package/consumers/mcp.js +38 -5
- package/consumers/openclaw-plugin.js +49 -4
- package/core/aquifer.js +18 -1
- package/package.json +1 -1
package/consumers/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ const { loadConfig } = require('./shared/config');
|
|
|
23
23
|
function parseArgs(argv) {
|
|
24
24
|
const args = { _: [], flags: {} };
|
|
25
25
|
// Flags that take a value (not boolean)
|
|
26
|
-
const VALUE_FLAGS = new Set(['limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status', 'concurrency']);
|
|
26
|
+
const VALUE_FLAGS = new Set(['limit', 'agent-id', 'source', 'date-from', 'date-to', 'output', 'format', 'config', 'status', 'concurrency', 'entities', 'entity-mode', 'session-id', 'verdict', 'note']);
|
|
27
27
|
for (let i = 0; i < argv.length; i++) {
|
|
28
28
|
if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
|
|
29
29
|
if (argv[i].startsWith('--')) {
|
|
@@ -56,13 +56,18 @@ async function cmdRecall(aquifer, args) {
|
|
|
56
56
|
process.exit(1);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const recallOpts = {
|
|
60
60
|
limit: parseInt(args.flags.limit || '5', 10),
|
|
61
61
|
agentId: args.flags['agent-id'] || undefined,
|
|
62
62
|
source: args.flags.source || undefined,
|
|
63
63
|
dateFrom: args.flags['date-from'] || undefined,
|
|
64
64
|
dateTo: args.flags['date-to'] || undefined,
|
|
65
|
-
}
|
|
65
|
+
};
|
|
66
|
+
if (args.flags.entities) {
|
|
67
|
+
recallOpts.entities = args.flags.entities.split(',').map(s => s.trim()).filter(Boolean);
|
|
68
|
+
recallOpts.entityMode = args.flags['entity-mode'] || 'any';
|
|
69
|
+
}
|
|
70
|
+
const results = await aquifer.recall(query, recallOpts);
|
|
66
71
|
|
|
67
72
|
if (args.flags.json) {
|
|
68
73
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -86,6 +91,28 @@ async function cmdRecall(aquifer, args) {
|
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
async function cmdFeedback(aquifer, args) {
|
|
95
|
+
const sessionId = args.flags['session-id'] || args._[1];
|
|
96
|
+
const verdict = args.flags.verdict;
|
|
97
|
+
|
|
98
|
+
if (!sessionId || !verdict) {
|
|
99
|
+
console.error('Usage: aquifer feedback --session-id ID --verdict helpful|unhelpful [--note TEXT] [--agent-id ID]');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await aquifer.feedback(sessionId, {
|
|
104
|
+
verdict,
|
|
105
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
106
|
+
note: args.flags.note || undefined,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (args.flags.json) {
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`Feedback: ${result.verdict} (trust ${result.trustBefore.toFixed(2)} → ${result.trustAfter.toFixed(2)})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
async function cmdBackfill(aquifer, args) {
|
|
90
117
|
const limit = parseInt(args.flags.limit || '100', 10);
|
|
91
118
|
const dryRun = !!args.flags['dry-run'];
|
|
@@ -243,6 +270,7 @@ async function main() {
|
|
|
243
270
|
Commands:
|
|
244
271
|
migrate Run database migrations
|
|
245
272
|
recall <query> Search sessions (requires embed config)
|
|
273
|
+
feedback Record trust feedback on a session
|
|
246
274
|
backfill Enrich pending sessions
|
|
247
275
|
stats Show database statistics
|
|
248
276
|
export Export sessions as JSONL
|
|
@@ -254,6 +282,11 @@ Options:
|
|
|
254
282
|
--source NAME Filter by source
|
|
255
283
|
--date-from YYYY-MM-DD Start date
|
|
256
284
|
--date-to YYYY-MM-DD End date
|
|
285
|
+
--entities A,B,C Entity names (comma-separated, recall)
|
|
286
|
+
--entity-mode any|all Entity match mode (recall, default: any)
|
|
287
|
+
--session-id ID Session ID (feedback)
|
|
288
|
+
--verdict helpful|unhelpful Feedback verdict (feedback)
|
|
289
|
+
--note TEXT Feedback note (feedback)
|
|
257
290
|
--json JSON output
|
|
258
291
|
--dry-run Preview only (backfill)
|
|
259
292
|
--output PATH Output file (export)
|
|
@@ -290,6 +323,9 @@ Options:
|
|
|
290
323
|
case 'recall':
|
|
291
324
|
await cmdRecall(aquifer, args);
|
|
292
325
|
break;
|
|
326
|
+
case 'feedback':
|
|
327
|
+
await cmdFeedback(aquifer, args);
|
|
328
|
+
break;
|
|
293
329
|
case 'backfill':
|
|
294
330
|
await cmdBackfill(aquifer, args);
|
|
295
331
|
break;
|
package/consumers/mcp.js
CHANGED
|
@@ -69,12 +69,12 @@ async function main() {
|
|
|
69
69
|
|
|
70
70
|
const server = new McpServer({
|
|
71
71
|
name: 'aquifer-memory',
|
|
72
|
-
version: '0.
|
|
72
|
+
version: '0.3.1',
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
server.tool(
|
|
76
76
|
'session_recall',
|
|
77
|
-
'Search stored sessions by keyword
|
|
77
|
+
'Search stored sessions by keyword. Supports entity intersection for precise multi-entity queries.',
|
|
78
78
|
{
|
|
79
79
|
query: z.string().min(1).describe('Search query (keyword or natural language)'),
|
|
80
80
|
limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
|
|
@@ -82,20 +82,26 @@ async function main() {
|
|
|
82
82
|
source: z.string().optional().describe('Filter by source (e.g., gateway, cc)'),
|
|
83
83
|
dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
84
84
|
dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
85
|
+
entities: z.array(z.string()).optional().describe('Entity names to match'),
|
|
86
|
+
entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
|
|
85
87
|
},
|
|
86
88
|
async (params) => {
|
|
87
89
|
try {
|
|
88
90
|
const aquifer = getAquifer();
|
|
89
91
|
const limit = params.limit || 5;
|
|
90
|
-
|
|
91
|
-
const results = await aquifer.recall(params.query, {
|
|
92
|
+
const recallOpts = {
|
|
92
93
|
limit,
|
|
93
94
|
agentId: params.agentId || undefined,
|
|
94
95
|
source: params.source || undefined,
|
|
95
96
|
dateFrom: params.dateFrom || undefined,
|
|
96
97
|
dateTo: params.dateTo || undefined,
|
|
97
|
-
}
|
|
98
|
+
};
|
|
99
|
+
if (params.entities && params.entities.length > 0) {
|
|
100
|
+
recallOpts.entities = params.entities;
|
|
101
|
+
recallOpts.entityMode = params.entityMode || 'any';
|
|
102
|
+
}
|
|
98
103
|
|
|
104
|
+
const results = await aquifer.recall(params.query, recallOpts);
|
|
99
105
|
const text = formatResults(results, params.query);
|
|
100
106
|
return { content: [{ type: 'text', text }] };
|
|
101
107
|
} catch (err) {
|
|
@@ -107,6 +113,33 @@ async function main() {
|
|
|
107
113
|
}
|
|
108
114
|
);
|
|
109
115
|
|
|
116
|
+
server.tool(
|
|
117
|
+
'session_feedback',
|
|
118
|
+
'Record trust feedback on a recalled session. Helpful sessions rank higher in future recalls.',
|
|
119
|
+
{
|
|
120
|
+
sessionId: z.string().min(1).describe('Session ID to give feedback on'),
|
|
121
|
+
verdict: z.enum(['helpful', 'unhelpful']).describe('Was the recalled session useful?'),
|
|
122
|
+
note: z.string().optional().describe('Optional reason'),
|
|
123
|
+
},
|
|
124
|
+
async (params) => {
|
|
125
|
+
try {
|
|
126
|
+
const aquifer = getAquifer();
|
|
127
|
+
const result = await aquifer.feedback(params.sessionId, {
|
|
128
|
+
verdict: params.verdict,
|
|
129
|
+
note: params.note || undefined,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: `Feedback: ${result.verdict} (trust ${result.trustBefore.toFixed(2)} → ${result.trustAfter.toFixed(2)})` }],
|
|
133
|
+
};
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: 'text', text: `session_feedback error: ${err.message}` }],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
110
143
|
// Graceful shutdown
|
|
111
144
|
const cleanup = async () => {
|
|
112
145
|
if (_aquifer?._pool) await _aquifer._pool.end().catch(() => {});
|
|
@@ -189,12 +189,14 @@ module.exports = {
|
|
|
189
189
|
|
|
190
190
|
// --- session_recall tool ---
|
|
191
191
|
|
|
192
|
+
// --- session_recall tool ---
|
|
193
|
+
|
|
192
194
|
api.registerTool((ctx) => {
|
|
193
195
|
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
194
196
|
|
|
195
197
|
return {
|
|
196
198
|
name: 'session_recall',
|
|
197
|
-
description: 'Search stored sessions by keyword
|
|
199
|
+
description: 'Search stored sessions by keyword. Supports entity intersection for precise multi-entity queries.',
|
|
198
200
|
parameters: {
|
|
199
201
|
type: 'object',
|
|
200
202
|
properties: {
|
|
@@ -204,20 +206,27 @@ module.exports = {
|
|
|
204
206
|
source: { type: 'string', description: 'Filter by source' },
|
|
205
207
|
dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
|
|
206
208
|
dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
|
|
209
|
+
entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
|
|
210
|
+
entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
|
|
207
211
|
},
|
|
208
212
|
required: ['query'],
|
|
209
213
|
},
|
|
210
214
|
async execute(_toolCallId, params) {
|
|
211
215
|
try {
|
|
212
216
|
const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
|
|
213
|
-
const
|
|
217
|
+
const recallOpts = {
|
|
214
218
|
limit,
|
|
215
219
|
agentId: params.agentId || undefined,
|
|
216
220
|
source: params.source || undefined,
|
|
217
221
|
dateFrom: params.dateFrom || undefined,
|
|
218
222
|
dateTo: params.dateTo || undefined,
|
|
219
|
-
}
|
|
223
|
+
};
|
|
224
|
+
if (Array.isArray(params.entities) && params.entities.length > 0) {
|
|
225
|
+
recallOpts.entities = params.entities;
|
|
226
|
+
recallOpts.entityMode = params.entityMode || 'any';
|
|
227
|
+
}
|
|
220
228
|
|
|
229
|
+
const results = await aquifer.recall(params.query, recallOpts);
|
|
221
230
|
const text = formatRecallResults(results);
|
|
222
231
|
return { content: [{ type: 'text', text }] };
|
|
223
232
|
} catch (err) {
|
|
@@ -230,6 +239,42 @@ module.exports = {
|
|
|
230
239
|
};
|
|
231
240
|
}, { name: 'session_recall' });
|
|
232
241
|
|
|
233
|
-
|
|
242
|
+
// --- session_feedback tool ---
|
|
243
|
+
|
|
244
|
+
api.registerTool((ctx) => {
|
|
245
|
+
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
name: 'session_feedback',
|
|
249
|
+
description: 'Record trust feedback on a recalled session. Helpful sessions rank higher in future recalls.',
|
|
250
|
+
parameters: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
sessionId: { type: 'string', description: 'Session ID to give feedback on' },
|
|
254
|
+
verdict: { type: 'string', enum: ['helpful', 'unhelpful'], description: 'Was the recalled session useful?' },
|
|
255
|
+
note: { type: 'string', description: 'Optional reason' },
|
|
256
|
+
},
|
|
257
|
+
required: ['sessionId', 'verdict'],
|
|
258
|
+
},
|
|
259
|
+
async execute(_toolCallId, params) {
|
|
260
|
+
try {
|
|
261
|
+
const result = await aquifer.feedback(params.sessionId, {
|
|
262
|
+
verdict: params.verdict,
|
|
263
|
+
note: params.note || undefined,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: 'text', text: `Feedback: ${result.verdict} (trust ${result.trustBefore.toFixed(2)} → ${result.trustAfter.toFixed(2)})` }],
|
|
267
|
+
};
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: `session_feedback error: ${err.message}` }],
|
|
271
|
+
isError: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}, { name: 'session_feedback' });
|
|
277
|
+
|
|
278
|
+
api.logger.info('[aquifer-memory] registered (before_reset + session_recall + session_feedback)');
|
|
234
279
|
},
|
|
235
280
|
};
|
package/core/aquifer.js
CHANGED
|
@@ -94,6 +94,14 @@ function createAquifer(config) {
|
|
|
94
94
|
|
|
95
95
|
// Track if migrate was called
|
|
96
96
|
let migrated = false;
|
|
97
|
+
let migratePromise = null;
|
|
98
|
+
|
|
99
|
+
async function ensureMigrated() {
|
|
100
|
+
if (migrated) return;
|
|
101
|
+
if (migratePromise) return migratePromise;
|
|
102
|
+
migratePromise = aquifer.migrate().finally(() => { migratePromise = null; });
|
|
103
|
+
return migratePromise;
|
|
104
|
+
}
|
|
97
105
|
|
|
98
106
|
// --- Helper: embed search on summaries ---
|
|
99
107
|
async function embeddingSearchSummaries(queryVec, opts) {
|
|
@@ -196,6 +204,7 @@ function createAquifer(config) {
|
|
|
196
204
|
async commit(sessionId, messages, opts = {}) {
|
|
197
205
|
if (!sessionId) throw new Error('sessionId is required');
|
|
198
206
|
if (!messages || !Array.isArray(messages)) throw new Error('messages must be an array');
|
|
207
|
+
await ensureMigrated();
|
|
199
208
|
|
|
200
209
|
const agentId = opts.agentId || 'agent';
|
|
201
210
|
const source = opts.source || 'api';
|
|
@@ -240,6 +249,7 @@ function createAquifer(config) {
|
|
|
240
249
|
// --- enrichment ---
|
|
241
250
|
|
|
242
251
|
async enrich(sessionId, opts = {}) {
|
|
252
|
+
await ensureMigrated();
|
|
243
253
|
const agentId = opts.agentId || 'agent';
|
|
244
254
|
const skipSummary = opts.skipSummary || false;
|
|
245
255
|
const skipTurnEmbed = opts.skipTurnEmbed || false;
|
|
@@ -470,6 +480,13 @@ function createAquifer(config) {
|
|
|
470
480
|
entityMode = 'any',
|
|
471
481
|
} = opts;
|
|
472
482
|
|
|
483
|
+
// Validate before touching DB
|
|
484
|
+
if (explicitEntities && explicitEntities.length > 0 && !entitiesEnabled) {
|
|
485
|
+
throw new Error('Entities are not enabled');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await ensureMigrated();
|
|
489
|
+
|
|
473
490
|
const fetchLimit = limit * 4;
|
|
474
491
|
|
|
475
492
|
// 1. Embed query
|
|
@@ -482,7 +499,6 @@ function createAquifer(config) {
|
|
|
482
499
|
let entityScoreBySession = new Map();
|
|
483
500
|
|
|
484
501
|
if (explicitEntities && explicitEntities.length > 0) {
|
|
485
|
-
if (!entitiesEnabled) throw new Error('Entities are not enabled');
|
|
486
502
|
|
|
487
503
|
const resolved = await entity.resolveEntities(pool, {
|
|
488
504
|
schema, tenantId, names: explicitEntities, agentId,
|
|
@@ -671,6 +687,7 @@ function createAquifer(config) {
|
|
|
671
687
|
const agentId = opts.agentId || 'agent';
|
|
672
688
|
const verdict = opts.verdict;
|
|
673
689
|
if (!verdict) throw new Error('opts.verdict is required ("helpful" or "unhelpful")');
|
|
690
|
+
await ensureMigrated();
|
|
674
691
|
|
|
675
692
|
const session = await storage.getSession(pool, sessionId, agentId, {}, { schema, tenantId });
|
|
676
693
|
if (!session) throw new Error(`Session not found: ${sessionId} (agentId=${agentId})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. Includes CLI, MCP server, and OpenClaw plugin.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|