@shadowforge0/aquifer-memory 0.6.0 → 0.8.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/README.md CHANGED
@@ -127,18 +127,28 @@ const results = await aquifer.recall('auth middleware decision', {
127
127
  ## Architecture
128
128
 
129
129
  ```
130
- ┌─────────────────────────────────────────────────────────────┐
131
- createAquifer (entry)
132
- Config · Migration · Ingest · Recall · Enrich
133
- └────────┬──────────┬──────────┬──────────┬───────────────────┘
130
+ ┌──────────────────────────────────────────────────────────────┐
131
+ Agent Hosts
132
+ Claude Code · OpenClaw · Codex · OpenCode · ...
133
+ └──────────────────────┬───────────────────────────────────────┘
134
+ │ MCP (stdio or HTTP)
135
+ ┌──────────────────────▼───────────────────────────────────────┐
136
+ │ Aquifer MCP Server (canonical API) │
137
+ │ session_recall · session_feedback · memory_stats · ... │
138
+ └──────────────────────┬───────────────────────────────────────┘
139
+
140
+ ┌──────────────────────▼───────────────────────────────────────┐
141
+ │ createAquifer (engine) │
142
+ │ Config · Migration · Ingest · Recall · Enrich │
143
+ └────────┬──────────┬──────────┬──────────┬────────────────────┘
134
144
  │ │ │ │
135
145
  ┌────▼───┐ ┌────▼────┐ ┌──▼───┐ ┌───▼──────────┐
136
146
  │storage │ │hybrid- │ │entity│ │ pipeline/ │
137
147
  │ .js │ │rank.js │ │ .js │ │summarize.js │
138
148
  └────────┘ └─────────┘ └──────┘ │embed.js │
139
149
  │ │ │extract-ent.js │
140
- ┌────▼───────────┐ ┌───▼──┐ └───────────────┘
141
- │ PostgreSQL │ │ LLM │
150
+ ┌────▼───────────┐ ┌───▼──┐ │rerank.js │
151
+ │ PostgreSQL │ │ LLM │ └───────────────┘
142
152
  │ + pgvector │ │ API │
143
153
  └────────────────┘ └──────┘
144
154
 
@@ -151,11 +161,13 @@ const results = await aquifer.recall('auth middleware decision', {
151
161
  └──────────────────────────────────┘
152
162
  ```
153
163
 
164
+ **Integration model:** MCP is the primary integration surface. Agent hosts connect to Aquifer through the MCP server (`consumers/mcp.js`), which exposes `session_recall`, `session_feedback`, `memory_stats`, and `memory_pending`. The CLI wraps the same engine for command-line use. The OpenClaw plugin (`consumers/openclaw-plugin.js`) is retained as a compatibility adapter for session capture but is not the primary tool delivery path.
165
+
154
166
  ### File Reference
155
167
 
156
168
  | File | Purpose |
157
169
  |------|---------|
158
- | `index.js` | Entry point — exports `createAquifer`, `createEmbedder` |
170
+ | `index.js` | Entry point — exports `createAquifer`, `createEmbedder`, `createReranker`, `normalizeSession` |
159
171
  | `core/aquifer.js` | Main facade: `migrate()`, `ingest()`, `recall()`, `enrich()` |
160
172
  | `core/storage.js` | Session/summary/turn CRUD, FTS search, embedding search |
161
173
  | `core/entity.js` | Entity upsert, mention tracking, relation graph, normalization |
@@ -163,6 +175,8 @@ const results = await aquifer.recall('auth middleware decision', {
163
175
  | `pipeline/summarize.js` | LLM-powered session summarization with structured output |
164
176
  | `pipeline/embed.js` | Embedding client (any OpenAI-compatible API) |
165
177
  | `pipeline/extract-entities.js` | LLM-powered entity extraction (12 types) |
178
+ | `pipeline/rerank.js` | Cross-encoder reranking (TEI, Jina, OpenRouter) |
179
+ | `pipeline/normalize/` | Session normalization for Claude Code / gateway noise |
166
180
  | `schema/001-base.sql` | DDL: sessions, summaries, turn_embeddings, FTS indexes |
167
181
  | `schema/002-entities.sql` | DDL: entities, mentions, relations, entity_sessions |
168
182
  | `schema/003-trust-feedback.sql` | DDL: trust_score column, session_feedback audit trail |
@@ -395,9 +409,48 @@ createAquifer({
395
409
 
396
410
  Fallback chain: `config.entities.scope` → `'default'`.
397
411
 
398
- ### Consumers (CLI, MCP, OpenClaw plugin)
412
+ ### MCP Server (primary integration)
413
+
414
+ Agent hosts should connect through the Aquifer MCP server. For OpenClaw, add to `openclaw.json`:
415
+
416
+ ```json
417
+ {
418
+ "mcp": {
419
+ "servers": {
420
+ "aquifer": {
421
+ "command": "node",
422
+ "args": ["/path/to/aquifer/consumers/mcp.js"],
423
+ "env": {
424
+ "DATABASE_URL": "postgresql://...",
425
+ "AQUIFER_SCHEMA": "aquifer",
426
+ "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
427
+ "AQUIFER_EMBED_MODEL": "bge-m3"
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ ```
434
+
435
+ Tools are exposed as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix is added by the host).
436
+
437
+ For Claude Code, add to `.claude.json`:
438
+
439
+ ```json
440
+ {
441
+ "mcpServers": {
442
+ "aquifer": {
443
+ "type": "stdio",
444
+ "command": "node",
445
+ "args": ["/path/to/aquifer/consumers/mcp.js"]
446
+ }
447
+ }
448
+ }
449
+ ```
450
+
451
+ ### CLI (secondary)
399
452
 
400
- For consumer-based setup using environment variables instead of code:
453
+ For command-line use with environment variables:
401
454
 
402
455
  ```bash
403
456
  export DATABASE_URL="postgresql://..."
package/consumers/cli.js CHANGED
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  const { createAquiferFromConfig } = require('./shared/factory');
17
- const { loadConfig } = require('./shared/config');
18
17
 
19
18
  // ---------------------------------------------------------------------------
20
19
  // Argument parser (minimal, no deps)
@@ -120,30 +119,12 @@ async function cmdBackfill(aquifer, args) {
120
119
  const skipTurnEmbed = !!args.flags['skip-turn-embed'];
121
120
  const skipEntities = !!args.flags['skip-entities'];
122
121
 
123
- const config = aquifer._config || {};
124
- const schema = config.schema || 'aquifer';
125
- const tenantId = config.tenantId || 'default';
126
- const pool = aquifer._pool;
122
+ const pending = await aquifer.getPendingSessions({ limit });
127
123
 
128
- if (!pool) {
129
- console.error('Backfill requires direct pool access.');
130
- process.exit(1);
131
- }
132
-
133
- const qi = (id) => `"${id}"`;
134
- const { rows } = await pool.query(`
135
- SELECT session_id, agent_id, processing_status
136
- FROM ${qi(schema)}.sessions
137
- WHERE tenant_id = $1
138
- AND processing_status IN ('pending', 'failed')
139
- ORDER BY started_at DESC
140
- LIMIT $2
141
- `, [tenantId, limit]);
142
-
143
- console.log(`Found ${rows.length} sessions to backfill${dryRun ? ' (dry-run)' : ''}`);
124
+ console.log(`Found ${pending.length} sessions to backfill${dryRun ? ' (dry-run)' : ''}`);
144
125
 
145
126
  let enriched = 0, failed = 0;
146
- for (const row of rows) {
127
+ for (const row of pending) {
147
128
  if (dryRun) {
148
129
  console.log(` [dry-run] ${row.session_id} (${row.agent_id}) status=${row.processing_status}`);
149
130
  continue;
@@ -164,40 +145,12 @@ async function cmdBackfill(aquifer, args) {
164
145
  }
165
146
  }
166
147
 
167
- console.log(`\nDone. enriched=${enriched} failed=${failed} total=${rows.length}`);
148
+ console.log(`\nDone. enriched=${enriched} failed=${failed} total=${pending.length}`);
168
149
  if (failed > 0) process.exitCode = 2;
169
150
  }
170
151
 
171
152
  async function cmdStats(aquifer, args) {
172
- const config = aquifer._config || {};
173
- const schema = config.schema || 'aquifer';
174
- const tenantId = config.tenantId || 'default';
175
- const pool = aquifer._pool;
176
-
177
- if (!pool) {
178
- console.error('Stats requires direct pool access.');
179
- process.exit(1);
180
- }
181
-
182
- const qi = (id) => `"${id}"`;
183
- const [sessions, summaries, turns, entities] = await Promise.all([
184
- pool.query(`SELECT processing_status, COUNT(*)::int as count FROM ${qi(schema)}.sessions WHERE tenant_id = $1 GROUP BY processing_status`, [tenantId]),
185
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.session_summaries WHERE tenant_id = $1`, [tenantId]),
186
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.turn_embeddings WHERE tenant_id = $1`, [tenantId]),
187
- pool.query(`SELECT COUNT(*)::int as count FROM ${qi(schema)}.entities WHERE tenant_id = $1`, [tenantId]).catch(() => ({ rows: [{ count: 0 }] })),
188
- ]);
189
-
190
- const timeRange = await pool.query(`SELECT MIN(started_at) as earliest, MAX(started_at) as latest FROM ${qi(schema)}.sessions WHERE tenant_id = $1`, [tenantId]);
191
-
192
- const stats = {
193
- sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
194
- sessionTotal: sessions.rows.reduce((s, r) => s + r.count, 0),
195
- summaries: summaries.rows[0]?.count || 0,
196
- turnEmbeddings: turns.rows[0]?.count || 0,
197
- entities: entities.rows[0]?.count || 0,
198
- earliest: timeRange.rows[0]?.earliest || null,
199
- latest: timeRange.rows[0]?.latest || null,
200
- };
153
+ const stats = await aquifer.getStats();
201
154
 
202
155
  if (args.flags.json) {
203
156
  console.log(JSON.stringify(stats, null, 2));
@@ -211,34 +164,14 @@ async function cmdStats(aquifer, args) {
211
164
  }
212
165
 
213
166
  async function cmdExport(aquifer, args) {
214
- const config = aquifer._config || {};
215
- const schema = config.schema || 'aquifer';
216
- const tenantId = config.tenantId || 'default';
217
- const pool = aquifer._pool;
218
167
  const output = args.flags.output || null;
219
168
  const limit = parseInt(args.flags.limit || '1000', 10);
220
169
 
221
- if (!pool) {
222
- console.error('Export requires direct pool access.');
223
- process.exit(1);
224
- }
225
-
226
- const qi = (id) => `"${id}"`;
227
- const where = [`s.tenant_id = $1`];
228
- const params = [tenantId];
229
-
230
- if (args.flags['agent-id']) { params.push(args.flags['agent-id']); where.push(`s.agent_id = $${params.length}`); }
231
- if (args.flags.source) { params.push(args.flags.source); where.push(`s.source = $${params.length}`); }
232
- params.push(limit);
233
-
234
- const { rows } = await pool.query(`
235
- SELECT s.*, ss.summary_text, ss.structured_summary
236
- FROM ${qi(schema)}.sessions s
237
- LEFT JOIN ${qi(schema)}.session_summaries ss ON ss.session_row_id = s.id
238
- WHERE ${where.join(' AND ')}
239
- ORDER BY s.started_at DESC
240
- LIMIT $${params.length}
241
- `, params);
170
+ const rows = await aquifer.exportSessions({
171
+ agentId: args.flags['agent-id'],
172
+ source: args.flags.source,
173
+ limit,
174
+ });
242
175
 
243
176
  const stream = output ? require('fs').createWriteStream(output) : process.stdout;
244
177
  for (const row of rows) {
@@ -340,7 +273,7 @@ Options:
340
273
  process.exit(1);
341
274
  }
342
275
  } finally {
343
- if (aquifer._pool) await aquifer._pool.end();
276
+ await aquifer.close();
344
277
  }
345
278
  }
346
279
 
package/consumers/mcp.js CHANGED
@@ -2,7 +2,12 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Aquifer MCP Server — session_recall tool via Model Context Protocol.
5
+ * Aquifer MCP Server — canonical external contract for agent host integration.
6
+ *
7
+ * This is the primary integration surface for Aquifer. Agent hosts (Claude Code,
8
+ * Codex, OpenCode, etc.) should integrate through this MCP server.
9
+ *
10
+ * Tools: session_recall, session_feedback, memory_stats, memory_pending
6
11
  *
7
12
  * Usage:
8
13
  * npx aquifer mcp
@@ -69,7 +74,7 @@ async function main() {
69
74
 
70
75
  const server = new McpServer({
71
76
  name: 'aquifer-memory',
72
- version: '0.6.0',
77
+ version: '0.8.0',
73
78
  });
74
79
 
75
80
  server.tool(
@@ -84,6 +89,7 @@ async function main() {
84
89
  dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
85
90
  entities: z.array(z.string()).optional().describe('Entity names to match'),
86
91
  entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
92
+ mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
87
93
  },
88
94
  async (params) => {
89
95
  try {
@@ -100,6 +106,7 @@ async function main() {
100
106
  recallOpts.entities = params.entities;
101
107
  recallOpts.entityMode = params.entityMode || 'any';
102
108
  }
109
+ if (params.mode) recallOpts.mode = params.mode;
103
110
 
104
111
  const results = await aquifer.recall(params.query, recallOpts);
105
112
  const text = formatResults(results, params.query);
@@ -120,6 +127,7 @@ async function main() {
120
127
  sessionId: z.string().min(1).describe('Session ID to give feedback on'),
121
128
  verdict: z.enum(['helpful', 'unhelpful']).describe('Was the recalled session useful?'),
122
129
  note: z.string().optional().describe('Optional reason'),
130
+ agentId: z.string().optional().describe('Agent ID the session was stored under (e.g. "main"). Defaults to "agent" if omitted.'),
123
131
  },
124
132
  async (params) => {
125
133
  try {
@@ -127,6 +135,7 @@ async function main() {
127
135
  const result = await aquifer.feedback(params.sessionId, {
128
136
  verdict: params.verdict,
129
137
  note: params.note || undefined,
138
+ agentId: params.agentId || undefined,
130
139
  });
131
140
  return {
132
141
  content: [{ type: 'text', text: `Feedback: ${result.verdict} (trust ${result.trustBefore.toFixed(2)} → ${result.trustAfter.toFixed(2)})` }],
@@ -140,9 +149,64 @@ async function main() {
140
149
  }
141
150
  );
142
151
 
152
+ server.tool(
153
+ 'memory_stats',
154
+ 'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
155
+ {},
156
+ async () => {
157
+ try {
158
+ const aquifer = getAquifer();
159
+ const stats = await aquifer.getStats();
160
+ const lines = [
161
+ `Sessions: ${stats.sessionTotal} total`,
162
+ ];
163
+ for (const [status, count] of Object.entries(stats.sessions)) {
164
+ lines.push(` ${status}: ${count}`);
165
+ }
166
+ lines.push(`Summaries: ${stats.summaries}`);
167
+ lines.push(`Turn embeddings: ${stats.turnEmbeddings}`);
168
+ lines.push(`Entities: ${stats.entities}`);
169
+ if (stats.earliest) lines.push(`Date range: ${new Date(stats.earliest).toISOString().slice(0, 10)} → ${new Date(stats.latest).toISOString().slice(0, 10)}`);
170
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
171
+ } catch (err) {
172
+ return {
173
+ content: [{ type: 'text', text: `memory_stats error: ${err.message}` }],
174
+ isError: true,
175
+ };
176
+ }
177
+ }
178
+ );
179
+
180
+ server.tool(
181
+ 'memory_pending',
182
+ 'List sessions with pending or failed processing status.',
183
+ {
184
+ limit: z.number().int().min(1).max(200).optional().describe('Max results (default 20)'),
185
+ },
186
+ async (params) => {
187
+ try {
188
+ const aquifer = getAquifer();
189
+ const rows = await aquifer.getPendingSessions({ limit: params.limit ?? 20 });
190
+ if (rows.length === 0) {
191
+ return { content: [{ type: 'text', text: 'No pending or failed sessions.' }] };
192
+ }
193
+ const lines = [`${rows.length} pending/failed session(s):\n`];
194
+ for (const row of rows) {
195
+ lines.push(`${row.session_id} [${row.processing_status}] agent=${row.agent_id}`);
196
+ }
197
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
198
+ } catch (err) {
199
+ return {
200
+ content: [{ type: 'text', text: `memory_pending error: ${err.message}` }],
201
+ isError: true,
202
+ };
203
+ }
204
+ }
205
+ );
206
+
143
207
  // Graceful shutdown
144
208
  const cleanup = async () => {
145
- if (_aquifer?._pool) await _aquifer._pool.end().catch(() => {});
209
+ if (_aquifer) await _aquifer.close().catch(() => {});
146
210
  process.exit(0);
147
211
  };
148
212
  process.on('SIGINT', cleanup);
@@ -153,7 +217,7 @@ async function main() {
153
217
 
154
218
  // Clean up pool when transport closes (stdin EOF)
155
219
  transport.onclose = async () => {
156
- if (_aquifer?._pool) await _aquifer._pool.end().catch(() => {});
220
+ if (_aquifer) await _aquifer.close().catch(() => {});
157
221
  };
158
222
  }
159
223
 
@@ -1,11 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Aquifer Memory — OpenClaw Plugin
4
+ * Aquifer Memory — OpenClaw Host Adapter
5
5
  *
6
- * Auto-captures sessions on before_reset and provides session_recall tool.
7
- * Install: add to openclaw.json plugins or extensions directory.
6
+ * Ingest adapter: auto-captures sessions on before_reset.
7
+ * Tool adapter: exposes session_recall/session_feedback via OpenClaw registerTool().
8
+ *
9
+ * Status: COMPATIBILITY ONLY. The official tool delivery path is mcp.servers.aquifer
10
+ * (see consumers/mcp.js). registerTool() exposure has OpenClaw upstream limitations
11
+ * that prevent reliable tool visibility. This plugin is retained for before_reset
12
+ * session capture; tool registration code is kept for future upstream fixes.
8
13
  *
14
+ * Install: add to openclaw.json plugins or extensions directory.
9
15
  * Config via plugin config, environment variables, or aquifer.config.json.
10
16
  */
11
17
 
@@ -169,6 +175,10 @@ module.exports = {
169
175
  } catch (enrichErr) {
170
176
  api.logger.warn(`[aquifer-memory] enrich failed for ${sessionId}: ${enrichErr.message}`);
171
177
  }
178
+ } else {
179
+ try {
180
+ await aquifer.skip(sessionId, { agentId, reason: `user_count=${norm.userCount} < min=${minUserMessages}` });
181
+ } catch (e) { api.logger.warn(`[aquifer-memory] skip failed for ${sessionId}: ${e.message}`); }
172
182
  }
173
183
 
174
184
  recentlyProcessed.set(dedupKey, Date.now());
@@ -189,8 +199,6 @@ module.exports = {
189
199
 
190
200
  // --- session_recall tool ---
191
201
 
192
- // --- session_recall tool ---
193
-
194
202
  api.registerTool((ctx) => {
195
203
  if ((ctx?.sessionKey || '').includes('subagent')) return null;
196
204
 
@@ -208,6 +216,7 @@ module.exports = {
208
216
  dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
209
217
  entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
210
218
  entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
219
+ mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall mode: "fts" (keyword only), "hybrid" (default), "vector" (vector only)' },
211
220
  },
212
221
  required: ['query'],
213
222
  },
@@ -225,6 +234,7 @@ module.exports = {
225
234
  recallOpts.entities = params.entities;
226
235
  recallOpts.entityMode = params.entityMode || 'any';
227
236
  }
237
+ if (params.mode) recallOpts.mode = params.mode;
228
238
 
229
239
  const results = await aquifer.recall(params.query, recallOpts);
230
240
  const text = formatRecallResults(results);
@@ -253,14 +263,17 @@ module.exports = {
253
263
  sessionId: { type: 'string', description: 'Session ID to give feedback on' },
254
264
  verdict: { type: 'string', enum: ['helpful', 'unhelpful'], description: 'Was the recalled session useful?' },
255
265
  note: { type: 'string', description: 'Optional reason' },
266
+ agentId: { type: 'string', description: 'Agent ID the session was stored under (e.g. "main"). Defaults to context agent or "agent" if omitted.' },
256
267
  },
257
268
  required: ['sessionId', 'verdict'],
258
269
  },
259
270
  async execute(_toolCallId, params) {
260
271
  try {
272
+ const resolvedAgentId = params.agentId || ctx?.agentId || undefined;
261
273
  const result = await aquifer.feedback(params.sessionId, {
262
274
  verdict: params.verdict,
263
275
  note: params.note || undefined,
276
+ agentId: resolvedAgentId,
264
277
  });
265
278
  return {
266
279
  content: [{ type: 'text', text: `Feedback: ${result.verdict} (trust ${result.trustBefore.toFixed(2)} → ${result.trustAfter.toFixed(2)})` }],
@@ -29,8 +29,19 @@ const DEFAULTS = {
29
29
  maxRetries: 3,
30
30
  temperature: 0,
31
31
  },
32
- entities: { enabled: false, mergeCall: true },
32
+ entities: { enabled: false, mergeCall: true, scope: 'default' },
33
33
  rank: { rrf: 0.65, timeDecay: 0.25, access: 0.10, entityBoost: 0.18 },
34
+ rerank: {
35
+ enabled: false,
36
+ provider: null, // 'tei' | 'jina' | 'openrouter' | 'custom'
37
+ baseUrl: null, // TEI base URL
38
+ apiKey: null, // Jina / OpenRouter API key
39
+ model: null, // model override (Jina / OpenRouter)
40
+ topK: 20,
41
+ maxChars: 1600,
42
+ timeoutMs: 2000,
43
+ maxRetries: 1,
44
+ },
34
45
  };
35
46
 
36
47
  // ---------------------------------------------------------------------------
@@ -57,6 +68,15 @@ const ENV_MAP = [
57
68
  ['AQUIFER_LLM_TIMEOUT_MS', 'llm.timeoutMs', Number],
58
69
  ['AQUIFER_LLM_TEMPERATURE', 'llm.temperature', Number],
59
70
  ['AQUIFER_ENTITIES_ENABLED', 'entities.enabled', Boolean],
71
+ ['AQUIFER_ENTITY_SCOPE', 'entities.scope'],
72
+ ['AQUIFER_RERANK_ENABLED', 'rerank.enabled', Boolean],
73
+ ['AQUIFER_RERANK_PROVIDER', 'rerank.provider'],
74
+ ['AQUIFER_RERANK_BASE_URL', 'rerank.baseUrl'],
75
+ ['AQUIFER_RERANK_API_KEY', 'rerank.apiKey'],
76
+ ['AQUIFER_RERANK_MODEL', 'rerank.model'],
77
+ ['AQUIFER_RERANK_TOP_K', 'rerank.topK', Number],
78
+ ['AQUIFER_RERANK_MAX_CHARS', 'rerank.maxChars', Number],
79
+ ['AQUIFER_RERANK_TIMEOUT_MS','rerank.timeoutMs', Number],
60
80
  ];
61
81
 
62
82
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { Pool } = require('pg');
4
- const { createAquifer, createEmbedder } = require('../../index');
4
+ const { createAquifer, createEmbedder, createReranker } = require('../../index');
5
5
  const { loadConfig } = require('./config');
6
6
  const { createLlmFn } = require('./llm');
7
7
 
@@ -57,20 +57,41 @@ function createAquiferFromConfig(overrides) {
57
57
  llmFn = createLlmFn(config.llm);
58
58
  }
59
59
 
60
+ // Rerank config (optional)
61
+ let rerankOpts = null;
62
+ if (config.rerank && config.rerank.enabled && config.rerank.provider) {
63
+ const rc = config.rerank;
64
+ const rerankConfig = { provider: rc.provider, topK: rc.topK, maxChars: rc.maxChars };
65
+ if (rc.provider === 'tei') {
66
+ rerankConfig.teiBaseUrl = rc.baseUrl || 'http://localhost:8080';
67
+ rerankConfig.timeout = rc.timeoutMs || 2000;
68
+ rerankConfig.maxRetries = rc.maxRetries ?? 1;
69
+ } else if (rc.provider === 'jina') {
70
+ rerankConfig.jinaApiKey = rc.apiKey;
71
+ if (rc.model) rerankConfig.jinaModel = rc.model;
72
+ rerankConfig.timeout = rc.timeoutMs || 2000;
73
+ rerankConfig.maxRetries = rc.maxRetries ?? 1;
74
+ } else if (rc.provider === 'openrouter') {
75
+ rerankConfig.openrouterApiKey = rc.apiKey;
76
+ if (rc.model) rerankConfig.model = rc.model;
77
+ rerankConfig.timeout = rc.timeoutMs || 5000;
78
+ rerankConfig.maxRetries = rc.maxRetries ?? 1;
79
+ }
80
+ rerankOpts = rerankConfig;
81
+ }
82
+
60
83
  const aquifer = createAquifer({
61
84
  db: pool,
85
+ ownsPool: true,
62
86
  schema: config.schema,
63
87
  tenantId: config.tenantId,
64
88
  embed: embedFn ? { fn: embedFn, dim: config.embed.dim || null } : null,
65
89
  llm: llmFn ? { fn: llmFn } : null,
66
90
  entities: config.entities,
67
91
  rank: config.rank,
92
+ rerank: rerankOpts,
68
93
  });
69
94
 
70
- // Attach pool for lifecycle management
71
- aquifer._pool = pool;
72
- aquifer._config = config;
73
-
74
95
  return aquifer;
75
96
  }
76
97