@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 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 results = await aquifer.recall(query, {
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.2.0',
72
+ version: '0.3.1',
73
73
  });
74
74
 
75
75
  server.tool(
76
76
  'session_recall',
77
- 'Search stored sessions by keyword, returning ranked summaries and matched conversation turns.',
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, returning ranked summaries and matched conversation turns.',
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 results = await aquifer.recall(params.query, {
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
- api.logger.info('[aquifer-memory] registered (before_reset + session_recall)');
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.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": [