@shadowforge0/aquifer-memory 0.9.1 → 1.0.1
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 +24 -4
- package/consumers/cli.js +41 -2
- package/consumers/mcp.js +29 -0
- package/consumers/opencode.js +345 -0
- package/core/aquifer.js +158 -17
- package/core/storage.js +2 -118
- package/index.js +1 -2
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -130,13 +130,14 @@ Full env-to-config mapping is in [consumers/shared/config.js](consumers/shared/c
|
|
|
130
130
|
|
|
131
131
|
## Host Integration
|
|
132
132
|
|
|
133
|
-
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes
|
|
133
|
+
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes five tools: `session_recall`, `session_feedback`, `session_bootstrap`, `memory_stats`, `memory_pending`.
|
|
134
134
|
|
|
135
135
|
| Integration | Route | Status | When to use |
|
|
136
136
|
|-------------|-------|--------|-------------|
|
|
137
137
|
| MCP server | `consumers/mcp.js` | Primary | Claude Code, OpenClaw, Codex, any MCP-capable host |
|
|
138
138
|
| Library API | `createAquifer()` | Primary | Backend apps, custom pipelines, direct Node.js usage |
|
|
139
|
-
| CLI | `consumers/cli.js` | Secondary | Operations, debugging, manual recall/backfill |
|
|
139
|
+
| CLI | `consumers/cli.js` | Secondary | Operations, debugging, manual recall/backfill (`aquifer bootstrap`, `aquifer ingest-opencode`, etc.) |
|
|
140
|
+
| OpenCode ingest | `consumers/opencode.js` | Secondary | Import sessions from OpenCode's SQLite DB |
|
|
140
141
|
| OpenClaw plugin | `consumers/openclaw-plugin.js` | Compatibility only | Session capture via `before_reset` — not for tool delivery |
|
|
141
142
|
|
|
142
143
|
### Claude Code
|
|
@@ -160,7 +161,7 @@ Add to your project's `.claude.json` or user-level MCP config:
|
|
|
160
161
|
}
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__session_feedback`, etc.
|
|
164
|
+
Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__session_feedback`, `mcp__aquifer__session_bootstrap`, etc.
|
|
164
165
|
|
|
165
166
|
### OpenClaw
|
|
166
167
|
|
|
@@ -184,7 +185,7 @@ Add to `openclaw.json` under `mcp.servers`:
|
|
|
184
185
|
}
|
|
185
186
|
```
|
|
186
187
|
|
|
187
|
-
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
188
|
+
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__session_bootstrap`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
188
189
|
|
|
189
190
|
The OpenClaw plugin (`consumers/openclaw-plugin.js`) is retained for session capture via `before_reset` but is **not** the recommended tool delivery path. Use MCP.
|
|
190
191
|
|
|
@@ -245,6 +246,7 @@ Any host that supports MCP stdio can connect the same way — point it at `node
|
|
|
245
246
|
| `pipeline/extract-entities.js` | LLM-powered entity extraction (12 types) |
|
|
246
247
|
| `pipeline/rerank.js` | Cross-encoder reranking (TEI, Jina, OpenRouter) |
|
|
247
248
|
| `pipeline/normalize/` | Session normalization for Claude Code / gateway noise |
|
|
249
|
+
| `consumers/opencode.js` | OpenCode SQLite ingest — reads sessions from OpenCode's local DB |
|
|
248
250
|
| `schema/001-base.sql` | DDL: sessions, summaries, turn_embeddings, FTS indexes |
|
|
249
251
|
| `schema/002-entities.sql` | DDL: entities, mentions, relations, entity_sessions |
|
|
250
252
|
| `schema/003-trust-feedback.sql` | DDL: trust_score column, session_feedback audit trail |
|
|
@@ -435,6 +437,24 @@ await aquifer.feedback('session-id', {
|
|
|
435
437
|
});
|
|
436
438
|
```
|
|
437
439
|
|
|
440
|
+
#### `aquifer.bootstrap(opts)`
|
|
441
|
+
|
|
442
|
+
Loads recent session context for a new conversation — summaries, open loops, and decisions. Time-based (no embedding search), designed for session-start injection.
|
|
443
|
+
|
|
444
|
+
```javascript
|
|
445
|
+
const result = await aquifer.bootstrap({
|
|
446
|
+
agentId: 'main',
|
|
447
|
+
limit: 5, // max sessions (default: 5)
|
|
448
|
+
lookbackDays: 14, // how far back (default: 14)
|
|
449
|
+
maxChars: 4000, // max output chars (default: 4000)
|
|
450
|
+
format: 'text', // 'text', 'structured', or 'both'
|
|
451
|
+
});
|
|
452
|
+
// format='text': result.text contains XML block ready for injection
|
|
453
|
+
// format='structured': result.sessions, result.openLoops, result.recentDecisions
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Cross-session dedup on open loops and decisions, sentinel filtering (removes 無/none/n/a), and maxChars truncation.
|
|
457
|
+
|
|
438
458
|
#### `aquifer.close()`
|
|
439
459
|
|
|
440
460
|
Closes the PostgreSQL connection pool (only if Aquifer created it).
|
package/consumers/cli.js
CHANGED
|
@@ -43,7 +43,7 @@ function parsePositiveInt(value, fallback) {
|
|
|
43
43
|
function parseArgs(argv) {
|
|
44
44
|
const args = { _: [], flags: {} };
|
|
45
45
|
// Flags that take a value (not boolean)
|
|
46
|
-
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']);
|
|
46
|
+
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', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars']);
|
|
47
47
|
for (let i = 0; i < argv.length; i++) {
|
|
48
48
|
if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
|
|
49
49
|
if (argv[i].startsWith('--')) {
|
|
@@ -252,6 +252,30 @@ async function cmdQuickstart(aquifer) {
|
|
|
252
252
|
console.log(' npx aquifer mcp');
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
async function cmdBootstrap(aquifer, args) {
|
|
256
|
+
const result = await aquifer.bootstrap({
|
|
257
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
258
|
+
source: args.flags.source || undefined,
|
|
259
|
+
limit: parsePositiveInt(args.flags.limit, 5),
|
|
260
|
+
lookbackDays: parsePositiveInt(args.flags['lookback-days'], 14),
|
|
261
|
+
maxChars: parsePositiveInt(args.flags['max-chars'], 4000),
|
|
262
|
+
format: args.flags.json ? 'structured' : 'text',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (args.flags.json) {
|
|
266
|
+
console.log(JSON.stringify(result, null, 2));
|
|
267
|
+
} else {
|
|
268
|
+
if (result.text) {
|
|
269
|
+
console.log(result.text);
|
|
270
|
+
} else {
|
|
271
|
+
// structured without text — format it
|
|
272
|
+
const { formatBootstrapText } = require('../core/aquifer');
|
|
273
|
+
const { text } = formatBootstrapText(result, result.meta?.maxChars || 4000);
|
|
274
|
+
console.log(text);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
255
279
|
async function cmdExport(aquifer, args) {
|
|
256
280
|
const output = args.flags.output || null;
|
|
257
281
|
const limit = parsePositiveInt(args.flags.limit, 1000);
|
|
@@ -297,6 +321,8 @@ Commands:
|
|
|
297
321
|
backfill Enrich pending sessions
|
|
298
322
|
stats Show database statistics
|
|
299
323
|
export Export sessions as JSONL
|
|
324
|
+
bootstrap Show recent session context (for new session start)
|
|
325
|
+
ingest-opencode Import sessions from OpenCode's local SQLite DB
|
|
300
326
|
mcp Start MCP server
|
|
301
327
|
|
|
302
328
|
Options:
|
|
@@ -313,7 +339,12 @@ Options:
|
|
|
313
339
|
--json JSON output
|
|
314
340
|
--dry-run Preview only (backfill)
|
|
315
341
|
--output PATH Output file (export)
|
|
316
|
-
--config PATH Config file path
|
|
342
|
+
--config PATH Config file path
|
|
343
|
+
--lookback-days N How far back in days (bootstrap, default: 14)
|
|
344
|
+
--max-chars N Max output characters (bootstrap, default: 4000)
|
|
345
|
+
--db PATH OpenCode SQLite path (ingest-opencode)
|
|
346
|
+
--since YYYY-MM-DD Only ingest sessions after date (ingest-opencode)
|
|
347
|
+
--min-messages N Min user messages to ingest (ingest-opencode, default: 3)`);
|
|
317
348
|
process.exit(0);
|
|
318
349
|
}
|
|
319
350
|
|
|
@@ -361,6 +392,14 @@ Options:
|
|
|
361
392
|
case 'export':
|
|
362
393
|
await cmdExport(aquifer, args);
|
|
363
394
|
break;
|
|
395
|
+
case 'bootstrap':
|
|
396
|
+
await cmdBootstrap(aquifer, args);
|
|
397
|
+
break;
|
|
398
|
+
case 'ingest-opencode': {
|
|
399
|
+
const { ingestOpenCode } = require('./opencode');
|
|
400
|
+
await ingestOpenCode(aquifer, args);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
364
403
|
default:
|
|
365
404
|
console.error(`Unknown command: ${command}. Run 'aquifer --help' for usage.`);
|
|
366
405
|
process.exit(1);
|
package/consumers/mcp.js
CHANGED
|
@@ -208,6 +208,35 @@ async function main() {
|
|
|
208
208
|
}
|
|
209
209
|
);
|
|
210
210
|
|
|
211
|
+
server.tool(
|
|
212
|
+
'session_bootstrap',
|
|
213
|
+
'Load recent session context for a new conversation. Returns summaries, open items, and decisions from recent sessions. Call this at the start of a conversation for continuity; use session_recall for keyword search.',
|
|
214
|
+
{
|
|
215
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
216
|
+
limit: z.number().int().min(1).max(20).optional().describe('Max sessions (default 5)'),
|
|
217
|
+
lookbackDays: z.number().int().min(1).max(90).optional().describe('How far back in days (default 14)'),
|
|
218
|
+
maxChars: z.number().int().min(500).max(12000).optional().describe('Max output characters (default 4000)'),
|
|
219
|
+
},
|
|
220
|
+
async (params) => {
|
|
221
|
+
try {
|
|
222
|
+
const aquifer = getAquifer();
|
|
223
|
+
const result = await aquifer.bootstrap({
|
|
224
|
+
agentId: params.agentId,
|
|
225
|
+
limit: params.limit,
|
|
226
|
+
lookbackDays: params.lookbackDays,
|
|
227
|
+
maxChars: params.maxChars,
|
|
228
|
+
format: 'text',
|
|
229
|
+
});
|
|
230
|
+
return { content: [{ type: 'text', text: result.text }] };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: 'text', text: `session_bootstrap error: ${err.message}` }],
|
|
234
|
+
isError: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
211
240
|
// Graceful shutdown
|
|
212
241
|
const cleanup = async () => {
|
|
213
242
|
if (_aquifer) await _aquifer.close().catch(() => {});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Aquifer — OpenCode Ingest Consumer
|
|
5
|
+
*
|
|
6
|
+
* Reads session history from OpenCode's local SQLite database and commits
|
|
7
|
+
* conversations into Aquifer for long-term memory (embedding + recall).
|
|
8
|
+
*
|
|
9
|
+
* OpenCode stores sessions in ~/.local/share/opencode/opencode.db (SQLite)
|
|
10
|
+
* with a Drizzle-managed schema: session → message → part.
|
|
11
|
+
*
|
|
12
|
+
* Usage (via CLI):
|
|
13
|
+
* aquifer ingest-opencode [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --db PATH OpenCode SQLite path (default: ~/.local/share/opencode/opencode.db)
|
|
17
|
+
* --agent-id ID Aquifer agent ID to store under (default: "opencode")
|
|
18
|
+
* --limit N Max sessions to ingest per run (default: 50)
|
|
19
|
+
* --since YYYY-MM-DD Only ingest sessions updated after this date
|
|
20
|
+
* --min-messages N Min user messages to ingest (default: 3)
|
|
21
|
+
* --dry-run Show what would be ingested without committing
|
|
22
|
+
* --enrich Run enrich (summary + embedding) after commit
|
|
23
|
+
* --json JSON output
|
|
24
|
+
* --session-id ID Ingest a single OpenCode session by ID
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { createAquiferFromConfig } = require('./shared/factory');
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// SQLite access — use Node 22+ built-in or fall back to better-sqlite3
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function openSqlite(dbPath) {
|
|
36
|
+
// Try node:sqlite (Node 22+)
|
|
37
|
+
try {
|
|
38
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
39
|
+
return new DatabaseSync(dbPath, { open: true, readOnly: true });
|
|
40
|
+
} catch (_) {
|
|
41
|
+
// not available
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Try better-sqlite3
|
|
45
|
+
try {
|
|
46
|
+
const Database = require('better-sqlite3');
|
|
47
|
+
return new Database(dbPath, { readonly: true });
|
|
48
|
+
} catch (_) {
|
|
49
|
+
// not available
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(
|
|
53
|
+
'No SQLite driver found. Upgrade to Node 22+ or install better-sqlite3:\n' +
|
|
54
|
+
' npm install better-sqlite3'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Read sessions from OpenCode SQLite
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function getOpenCodeSessions(db, { limit = 50, since = null, sessionId = null } = {}) {
|
|
63
|
+
if (sessionId) {
|
|
64
|
+
const row = db.prepare(
|
|
65
|
+
'SELECT id, title, directory, time_created, time_updated FROM session WHERE id = ?'
|
|
66
|
+
).get(sessionId);
|
|
67
|
+
return row ? [row] : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let sql = `
|
|
71
|
+
SELECT id, title, directory, time_created, time_updated
|
|
72
|
+
FROM session
|
|
73
|
+
WHERE 1=1
|
|
74
|
+
`;
|
|
75
|
+
const params = [];
|
|
76
|
+
|
|
77
|
+
if (since) {
|
|
78
|
+
sql += ' AND time_updated >= ?';
|
|
79
|
+
params.push(new Date(since).getTime());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
sql += ' ORDER BY time_updated DESC LIMIT ?';
|
|
83
|
+
params.push(limit);
|
|
84
|
+
|
|
85
|
+
return db.prepare(sql).all(...params);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getSessionConversation(db, sessionId) {
|
|
89
|
+
// Get messages ordered by creation time
|
|
90
|
+
const messages = db.prepare(`
|
|
91
|
+
SELECT id, data, time_created
|
|
92
|
+
FROM message
|
|
93
|
+
WHERE session_id = ?
|
|
94
|
+
ORDER BY time_created ASC
|
|
95
|
+
`).all(sessionId);
|
|
96
|
+
|
|
97
|
+
// Get all parts for this session, grouped by message
|
|
98
|
+
const parts = db.prepare(`
|
|
99
|
+
SELECT id, message_id, data, time_created
|
|
100
|
+
FROM part
|
|
101
|
+
WHERE session_id = ?
|
|
102
|
+
ORDER BY time_created ASC
|
|
103
|
+
`).all(sessionId);
|
|
104
|
+
|
|
105
|
+
const partsByMsg = new Map();
|
|
106
|
+
for (const p of parts) {
|
|
107
|
+
const msgId = p.message_id;
|
|
108
|
+
if (!partsByMsg.has(msgId)) partsByMsg.set(msgId, []);
|
|
109
|
+
partsByMsg.get(msgId).push(p);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { messages, partsByMsg };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Normalize OpenCode conversation → Aquifer messages format
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function normalizeConversation(messages, partsByMsg) {
|
|
120
|
+
const normalized = [];
|
|
121
|
+
let model = null;
|
|
122
|
+
let tokensIn = 0, tokensOut = 0;
|
|
123
|
+
let startedAt = null, lastMessageAt = null;
|
|
124
|
+
|
|
125
|
+
for (const msg of messages) {
|
|
126
|
+
const msgData = JSON.parse(msg.data);
|
|
127
|
+
const role = msgData.role;
|
|
128
|
+
if (!role || !['user', 'assistant'].includes(role)) continue;
|
|
129
|
+
|
|
130
|
+
// Extract model info
|
|
131
|
+
if (!model) {
|
|
132
|
+
if (msgData.model?.modelID) model = msgData.model.modelID;
|
|
133
|
+
else if (msgData.modelID) model = msgData.modelID;
|
|
134
|
+
else if (msgData.providerID && msgData.modelID) model = `${msgData.providerID}/${msgData.modelID}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Accumulate tokens
|
|
138
|
+
if (msgData.tokens) {
|
|
139
|
+
tokensIn += msgData.tokens.input || 0;
|
|
140
|
+
tokensOut += msgData.tokens.output || 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Timestamp (ms → ISO)
|
|
144
|
+
const ts = msg.time_created ? new Date(msg.time_created).toISOString() : null;
|
|
145
|
+
if (ts && !startedAt) startedAt = ts;
|
|
146
|
+
if (ts) lastMessageAt = ts;
|
|
147
|
+
|
|
148
|
+
// Build content from parts
|
|
149
|
+
const msgParts = partsByMsg.get(msg.id) || [];
|
|
150
|
+
const textParts = [];
|
|
151
|
+
|
|
152
|
+
for (const part of msgParts) {
|
|
153
|
+
let partData;
|
|
154
|
+
try { partData = JSON.parse(part.data); } catch { continue; }
|
|
155
|
+
|
|
156
|
+
if (partData.type === 'text' && partData.text) {
|
|
157
|
+
textParts.push(partData.text);
|
|
158
|
+
} else if (partData.type === 'tool' && partData.state?.output) {
|
|
159
|
+
// Include tool results as context (truncated)
|
|
160
|
+
const toolName = partData.tool || 'tool';
|
|
161
|
+
const output = partData.state.output;
|
|
162
|
+
const truncated = output.length > 500 ? output.slice(0, 500) + '...' : output;
|
|
163
|
+
textParts.push(`[${toolName}]: ${truncated}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const content = textParts.join('\n').trim();
|
|
168
|
+
if (!content) continue;
|
|
169
|
+
|
|
170
|
+
// Merge consecutive same-role messages (OpenCode splits assistant into steps)
|
|
171
|
+
const last = normalized[normalized.length - 1];
|
|
172
|
+
if (last && last.role === role) {
|
|
173
|
+
last.content += '\n\n' + content;
|
|
174
|
+
last.timestamp = ts || last.timestamp;
|
|
175
|
+
} else {
|
|
176
|
+
normalized.push({ role, content, timestamp: ts });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
messages: normalized,
|
|
182
|
+
userCount: normalized.filter(m => m.role === 'user').length,
|
|
183
|
+
assistantCount: normalized.filter(m => m.role === 'assistant').length,
|
|
184
|
+
model,
|
|
185
|
+
tokensIn,
|
|
186
|
+
tokensOut,
|
|
187
|
+
startedAt,
|
|
188
|
+
lastMessageAt,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Ingest command
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
async function ingestOpenCode(aquifer, args) {
|
|
197
|
+
const defaultDb = path.join(os.homedir(), '.local/share/opencode/opencode.db');
|
|
198
|
+
const dbPath = args.flags.db || defaultDb;
|
|
199
|
+
const agentId = args.flags['agent-id'] || 'opencode';
|
|
200
|
+
const limit = Math.max(1, parseInt(args.flags.limit || '50', 10) || 50);
|
|
201
|
+
const since = args.flags.since || null;
|
|
202
|
+
const dryRun = !!args.flags['dry-run'];
|
|
203
|
+
const doEnrich = !!args.flags.enrich;
|
|
204
|
+
const jsonOutput = !!args.flags.json;
|
|
205
|
+
const sessionId = args.flags['session-id'] || null;
|
|
206
|
+
const minUserMessages = parseInt(args.flags['min-messages'] || '3', 10);
|
|
207
|
+
|
|
208
|
+
// Open OpenCode DB
|
|
209
|
+
let db;
|
|
210
|
+
try {
|
|
211
|
+
db = openSqlite(dbPath);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error(`Cannot open OpenCode database: ${err.message}`);
|
|
214
|
+
console.error(`Expected at: ${dbPath}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check which sessions are already in Aquifer
|
|
219
|
+
const existingSet = new Set();
|
|
220
|
+
try {
|
|
221
|
+
const existing = await aquifer.exportSessions({ source: 'opencode', limit: 10000 });
|
|
222
|
+
for (const row of existing) existingSet.add(row.session_id);
|
|
223
|
+
} catch (_) {
|
|
224
|
+
// exportSessions may not exist in all versions
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get OpenCode sessions
|
|
228
|
+
const sessions = getOpenCodeSessions(db, { limit, since, sessionId });
|
|
229
|
+
|
|
230
|
+
const results = [];
|
|
231
|
+
let committed = 0, skipped = 0, failed = 0;
|
|
232
|
+
|
|
233
|
+
for (const session of sessions) {
|
|
234
|
+
const sid = session.id;
|
|
235
|
+
|
|
236
|
+
// Skip already ingested (unless explicitly requested by session-id)
|
|
237
|
+
if (!sessionId && existingSet.has(sid)) {
|
|
238
|
+
skipped++;
|
|
239
|
+
if (jsonOutput) results.push({ sessionId: sid, status: 'exists' });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Read conversation
|
|
244
|
+
const { messages, partsByMsg } = getSessionConversation(db, sid);
|
|
245
|
+
const norm = normalizeConversation(messages, partsByMsg);
|
|
246
|
+
|
|
247
|
+
if (norm.userCount < minUserMessages) {
|
|
248
|
+
skipped++;
|
|
249
|
+
if (jsonOutput) results.push({ sessionId: sid, status: 'too_short', userMessages: norm.userCount });
|
|
250
|
+
else if (!jsonOutput && !dryRun) {
|
|
251
|
+
console.log(` [skip] ${sid} — ${norm.userCount} user msg(s)`);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const info = {
|
|
257
|
+
sessionId: sid,
|
|
258
|
+
title: session.title,
|
|
259
|
+
messages: norm.messages.length,
|
|
260
|
+
userMessages: norm.userCount,
|
|
261
|
+
model: norm.model,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (dryRun) {
|
|
265
|
+
info.status = 'dry-run';
|
|
266
|
+
if (jsonOutput) {
|
|
267
|
+
results.push(info);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` [dry-run] ${sid} "${session.title}" — ${norm.messages.length} msgs (${norm.userCount} user)`);
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Commit to Aquifer
|
|
275
|
+
try {
|
|
276
|
+
await aquifer.commit(sid, norm.messages, {
|
|
277
|
+
agentId,
|
|
278
|
+
source: 'opencode',
|
|
279
|
+
model: norm.model,
|
|
280
|
+
tokensIn: norm.tokensIn,
|
|
281
|
+
tokensOut: norm.tokensOut,
|
|
282
|
+
startedAt: norm.startedAt,
|
|
283
|
+
lastMessageAt: norm.lastMessageAt,
|
|
284
|
+
});
|
|
285
|
+
committed++;
|
|
286
|
+
info.status = 'committed';
|
|
287
|
+
|
|
288
|
+
// Enrich if requested
|
|
289
|
+
if (doEnrich) {
|
|
290
|
+
try {
|
|
291
|
+
const enrichResult = await aquifer.enrich(sid, { agentId });
|
|
292
|
+
info.status = 'enriched';
|
|
293
|
+
info.turnsEmbedded = enrichResult.turnsEmbedded;
|
|
294
|
+
info.entitiesFound = enrichResult.entitiesFound;
|
|
295
|
+
} catch (enrichErr) {
|
|
296
|
+
info.enrichError = enrichErr.message;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (jsonOutput) {
|
|
301
|
+
results.push(info);
|
|
302
|
+
} else {
|
|
303
|
+
const enrichNote = info.turnsEmbedded != null ? ` (${info.turnsEmbedded} turns, ${info.entitiesFound} entities)` : '';
|
|
304
|
+
console.log(` [${committed}] ${sid} "${session.title}"${enrichNote}`);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
failed++;
|
|
308
|
+
info.status = 'error';
|
|
309
|
+
info.error = err.message;
|
|
310
|
+
if (jsonOutput) {
|
|
311
|
+
results.push(info);
|
|
312
|
+
} else {
|
|
313
|
+
console.error(` [error] ${sid}: ${err.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Close SQLite
|
|
319
|
+
db.close();
|
|
320
|
+
|
|
321
|
+
// Summary
|
|
322
|
+
if (jsonOutput) {
|
|
323
|
+
console.log(JSON.stringify({ committed, skipped, failed, total: sessions.length, sessions: results }, null, 2));
|
|
324
|
+
} else {
|
|
325
|
+
console.log(`\nDone. committed=${committed} skipped=${skipped} failed=${failed} total=${sessions.length}`);
|
|
326
|
+
if (committed > 0 && !doEnrich) {
|
|
327
|
+
console.log('Tip: run "aquifer backfill" to enrich committed sessions.');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (failed > 0) process.exitCode = 2;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Exports
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
ingestOpenCode,
|
|
340
|
+
// Exposed for testing
|
|
341
|
+
openSqlite,
|
|
342
|
+
getOpenCodeSessions,
|
|
343
|
+
getSessionConversation,
|
|
344
|
+
normalizeConversation,
|
|
345
|
+
};
|
package/core/aquifer.js
CHANGED
|
@@ -918,7 +918,6 @@ function createAquifer(config) {
|
|
|
918
918
|
},
|
|
919
919
|
|
|
920
920
|
async getSessionFull(sessionId) {
|
|
921
|
-
// Try to find the session across agents by querying directly
|
|
922
921
|
const result = await pool.query(
|
|
923
922
|
`SELECT * FROM ${qi(schema)}.sessions
|
|
924
923
|
WHERE session_id = $1 AND tenant_id = $2
|
|
@@ -928,24 +927,15 @@ function createAquifer(config) {
|
|
|
928
927
|
const session = result.rows[0];
|
|
929
928
|
if (!session) return null;
|
|
930
929
|
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
),
|
|
938
|
-
pool.query(
|
|
939
|
-
`SELECT * FROM ${qi(schema)}.session_summaries
|
|
940
|
-
WHERE session_row_id = $1
|
|
941
|
-
LIMIT 1`,
|
|
942
|
-
[session.id]
|
|
943
|
-
),
|
|
944
|
-
]);
|
|
930
|
+
const sumResult = await pool.query(
|
|
931
|
+
`SELECT * FROM ${qi(schema)}.session_summaries
|
|
932
|
+
WHERE session_row_id = $1
|
|
933
|
+
LIMIT 1`,
|
|
934
|
+
[session.id]
|
|
935
|
+
);
|
|
945
936
|
|
|
946
937
|
return {
|
|
947
938
|
session,
|
|
948
|
-
segments: segResult.rows,
|
|
949
939
|
summary: sumResult.rows[0] || null,
|
|
950
940
|
};
|
|
951
941
|
},
|
|
@@ -1036,13 +1026,164 @@ function createAquifer(config) {
|
|
|
1036
1026
|
);
|
|
1037
1027
|
return result.rows;
|
|
1038
1028
|
},
|
|
1029
|
+
|
|
1030
|
+
async bootstrap(opts = {}) {
|
|
1031
|
+
await ensureMigrated();
|
|
1032
|
+
|
|
1033
|
+
const agentId = opts.agentId || null;
|
|
1034
|
+
const source = opts.source || null;
|
|
1035
|
+
const limit = Math.max(1, Math.min(20, opts.limit || 5));
|
|
1036
|
+
const lookbackDays = opts.lookbackDays || 14;
|
|
1037
|
+
const maxChars = opts.maxChars || 4000;
|
|
1038
|
+
const format = opts.format || 'structured';
|
|
1039
|
+
|
|
1040
|
+
const where = [`s.tenant_id = $1`, `s.processing_status = 'succeeded'`];
|
|
1041
|
+
const params = [tenantId];
|
|
1042
|
+
|
|
1043
|
+
if (agentId) {
|
|
1044
|
+
params.push(agentId);
|
|
1045
|
+
where.push(`s.agent_id = $${params.length}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (source) {
|
|
1048
|
+
params.push(source);
|
|
1049
|
+
where.push(`s.source = $${params.length}`);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
params.push(lookbackDays);
|
|
1053
|
+
where.push(`s.started_at > now() - ($${params.length} || ' days')::interval`);
|
|
1054
|
+
|
|
1055
|
+
params.push(limit);
|
|
1056
|
+
|
|
1057
|
+
const result = await pool.query(
|
|
1058
|
+
`SELECT s.session_id, s.agent_id, s.source, s.started_at, s.msg_count,
|
|
1059
|
+
ss.summary_text, ss.structured_summary
|
|
1060
|
+
FROM ${qi(schema)}.sessions s
|
|
1061
|
+
JOIN ${qi(schema)}.session_summaries ss ON ss.session_row_id = s.id
|
|
1062
|
+
WHERE ${where.join(' AND ')}
|
|
1063
|
+
ORDER BY s.started_at DESC
|
|
1064
|
+
LIMIT $${params.length}`,
|
|
1065
|
+
params
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
const sessions = result.rows.map(r => {
|
|
1069
|
+
const ss = r.structured_summary || {};
|
|
1070
|
+
const hasSS = ss.title || ss.overview;
|
|
1071
|
+
return {
|
|
1072
|
+
sessionId: r.session_id,
|
|
1073
|
+
agentId: r.agent_id,
|
|
1074
|
+
source: r.source,
|
|
1075
|
+
startedAt: r.started_at,
|
|
1076
|
+
title: ss.title || (hasSS ? null : (r.summary_text || '').slice(0, 60).trim() || null),
|
|
1077
|
+
overview: ss.overview || (hasSS ? null : (r.summary_text || '').slice(0, 200).trim() || null),
|
|
1078
|
+
topics: Array.isArray(ss.topics) ? ss.topics : [],
|
|
1079
|
+
decisions: Array.isArray(ss.decisions) ? ss.decisions : [],
|
|
1080
|
+
openLoops: Array.isArray(ss.open_loops) ? ss.open_loops : [],
|
|
1081
|
+
importantFacts: Array.isArray(ss.important_facts) ? ss.important_facts : [],
|
|
1082
|
+
};
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// Cross-session open loops merge + dedup + sentinel filter
|
|
1086
|
+
const SENTINELS = new Set(['無', 'none', 'n/a', 'na', 'done', '']);
|
|
1087
|
+
const seenLoops = new Set();
|
|
1088
|
+
const openLoops = [];
|
|
1089
|
+
for (const s of sessions) {
|
|
1090
|
+
for (const loop of s.openLoops) {
|
|
1091
|
+
const raw = typeof loop === 'string' ? loop : (loop.item || '');
|
|
1092
|
+
const normalized = raw.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
1093
|
+
if (SENTINELS.has(normalized) || !normalized || seenLoops.has(normalized)) continue;
|
|
1094
|
+
seenLoops.add(normalized);
|
|
1095
|
+
openLoops.push({ item: raw.trim(), fromSession: s.sessionId, latestStartedAt: s.startedAt });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Cross-session recent decisions dedup
|
|
1100
|
+
const seenDecisions = new Set();
|
|
1101
|
+
const recentDecisions = [];
|
|
1102
|
+
for (const s of sessions) {
|
|
1103
|
+
for (const d of s.decisions) {
|
|
1104
|
+
const key = typeof d === 'string' ? d : (d.decision || '');
|
|
1105
|
+
const normalized = key.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
1106
|
+
if (!normalized || seenDecisions.has(normalized)) continue;
|
|
1107
|
+
seenDecisions.add(normalized);
|
|
1108
|
+
recentDecisions.push({ decision: key.trim(), reason: d.reason || null, fromSession: s.sessionId });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const structured = {
|
|
1113
|
+
sessions,
|
|
1114
|
+
openLoops,
|
|
1115
|
+
recentDecisions,
|
|
1116
|
+
meta: { lookbackDays, count: sessions.length, maxChars, truncated: false },
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
if (format === 'text' || format === 'both') {
|
|
1120
|
+
const textResult = formatBootstrapText(structured, maxChars);
|
|
1121
|
+
structured.text = textResult.text;
|
|
1122
|
+
structured.meta.truncated = textResult.truncated;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return structured;
|
|
1126
|
+
},
|
|
1039
1127
|
};
|
|
1040
1128
|
|
|
1041
1129
|
return aquifer;
|
|
1042
1130
|
}
|
|
1043
1131
|
|
|
1132
|
+
// ---------------------------------------------------------------------------
|
|
1133
|
+
// formatBootstrapText — pure function, builds <session-bootstrap> XML block
|
|
1134
|
+
// ---------------------------------------------------------------------------
|
|
1135
|
+
|
|
1136
|
+
function formatBootstrapText(data, maxChars) {
|
|
1137
|
+
if (!data.sessions || data.sessions.length === 0) {
|
|
1138
|
+
return { text: 'No recent sessions found.', truncated: false };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
let truncated = false;
|
|
1142
|
+
const parts = [];
|
|
1143
|
+
|
|
1144
|
+
// Build session lines (newest first, truncate from oldest if over budget)
|
|
1145
|
+
const sessionLines = [];
|
|
1146
|
+
for (const s of data.sessions) {
|
|
1147
|
+
const date = s.startedAt ? new Date(s.startedAt).toISOString().slice(0, 10) : '?';
|
|
1148
|
+
const title = s.title || '(untitled)';
|
|
1149
|
+
const overview = s.overview ? s.overview.slice(0, 200) : '';
|
|
1150
|
+
let line = `- ${date} | ${title}`;
|
|
1151
|
+
if (overview) line += ` — ${overview}`;
|
|
1152
|
+
const decisions = s.decisions
|
|
1153
|
+
.map(d => typeof d === 'string' ? d : d.decision)
|
|
1154
|
+
.filter(Boolean);
|
|
1155
|
+
if (decisions.length > 0) line += `\n Decisions: ${decisions.join('; ')}`;
|
|
1156
|
+
sessionLines.push(line);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Fit within maxChars by removing oldest sessions
|
|
1160
|
+
let bodyLines = [...sessionLines];
|
|
1161
|
+
const footer = [];
|
|
1162
|
+
if (data.openLoops.length > 0) {
|
|
1163
|
+
footer.push(`Open items: ${data.openLoops.map(l => l.item).join(', ')}`);
|
|
1164
|
+
}
|
|
1165
|
+
if (data.recentDecisions.length > 0) {
|
|
1166
|
+
footer.push(`Recent decisions: ${data.recentDecisions.map(d => d.decision).join(', ')}`);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const buildText = (lines) => {
|
|
1170
|
+
const body = ['Recent sessions:', ...lines].join('\n');
|
|
1171
|
+
const full = footer.length > 0 ? body + '\n' + footer.join('\n') : body;
|
|
1172
|
+
return `<session-bootstrap sessions="${lines.length}" open_loops="${data.openLoops.length}">\n${full}\n</session-bootstrap>`;
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
let text = buildText(bodyLines);
|
|
1176
|
+
while (text.length > maxChars && bodyLines.length > 1) {
|
|
1177
|
+
bodyLines.pop(); // remove oldest
|
|
1178
|
+
truncated = true;
|
|
1179
|
+
text = buildText(bodyLines);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return { text, truncated };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1044
1185
|
// ---------------------------------------------------------------------------
|
|
1045
1186
|
// Exports
|
|
1046
1187
|
// ---------------------------------------------------------------------------
|
|
1047
1188
|
|
|
1048
|
-
module.exports = { createAquifer };
|
|
1189
|
+
module.exports = { createAquifer, formatBootstrapText };
|
package/core/storage.js
CHANGED
|
@@ -96,44 +96,6 @@ async function upsertSession(pool, {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// upsertSegments
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
async function upsertSegments(pool, sessionRowId, segments, { schema } = {}) {
|
|
104
|
-
if (!segments || segments.length === 0) return;
|
|
105
|
-
for (const seg of segments) {
|
|
106
|
-
await pool.query(
|
|
107
|
-
`INSERT INTO ${qi(schema)}.session_segments
|
|
108
|
-
(session_row_id, segment_no, start_msg_idx, end_msg_idx,
|
|
109
|
-
started_at, ended_at, raw_msg_count, effective_msg_count,
|
|
110
|
-
boundary_type, boundary_meta)
|
|
111
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
|
112
|
-
ON CONFLICT (session_row_id, segment_no) DO UPDATE SET
|
|
113
|
-
start_msg_idx = EXCLUDED.start_msg_idx,
|
|
114
|
-
end_msg_idx = EXCLUDED.end_msg_idx,
|
|
115
|
-
started_at = EXCLUDED.started_at,
|
|
116
|
-
ended_at = EXCLUDED.ended_at,
|
|
117
|
-
raw_msg_count = EXCLUDED.raw_msg_count,
|
|
118
|
-
effective_msg_count = EXCLUDED.effective_msg_count,
|
|
119
|
-
boundary_type = EXCLUDED.boundary_type,
|
|
120
|
-
boundary_meta = EXCLUDED.boundary_meta`,
|
|
121
|
-
[
|
|
122
|
-
sessionRowId,
|
|
123
|
-
seg.segmentNo,
|
|
124
|
-
seg.startMsgIdx !== null && seg.startMsgIdx !== undefined ? seg.startMsgIdx : null,
|
|
125
|
-
seg.endMsgIdx !== null && seg.endMsgIdx !== undefined ? seg.endMsgIdx : null,
|
|
126
|
-
seg.startedAt || null,
|
|
127
|
-
seg.endedAt || null,
|
|
128
|
-
seg.rawMsgCount || 0,
|
|
129
|
-
seg.effectiveMsgCount || 0,
|
|
130
|
-
seg.boundaryType || null,
|
|
131
|
-
seg.boundaryMeta ? JSON.stringify(seg.boundaryMeta) : '{}',
|
|
132
|
-
]
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
99
|
// ---------------------------------------------------------------------------
|
|
138
100
|
// upsertSummary
|
|
139
101
|
// ---------------------------------------------------------------------------
|
|
@@ -159,9 +121,8 @@ async function upsertSummary(pool, sessionRowId, {
|
|
|
159
121
|
`INSERT INTO ${qi(schema)}.session_summaries
|
|
160
122
|
(session_row_id, tenant_id, agent_id, session_id, summary_version, model, source_hash,
|
|
161
123
|
message_count, user_message_count, assistant_message_count,
|
|
162
|
-
boundary_count, fresh_tail_count,
|
|
163
124
|
started_at, ended_at, structured_summary, summary_text, embedding, updated_at)
|
|
164
|
-
VALUES ($1,$2,$3,$4,1,$5,$6,$7,$8,$9
|
|
125
|
+
VALUES ($1,$2,$3,$4,1,$5,$6,$7,$8,$9,$10,$11,COALESCE($12::jsonb,'{}'::jsonb),COALESCE($13,''),$14::vector,now())
|
|
165
126
|
ON CONFLICT (session_row_id) DO UPDATE SET
|
|
166
127
|
tenant_id = EXCLUDED.tenant_id,
|
|
167
128
|
agent_id = EXCLUDED.agent_id,
|
|
@@ -211,50 +172,6 @@ async function markStatus(pool, sessionRowId, status, error, { schema } = {}) {
|
|
|
211
172
|
return result.rows[0] || null;
|
|
212
173
|
}
|
|
213
174
|
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// persistProcessingResults (@internal — prefer aquifer.enrich() for full pipeline)
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
async function persistProcessingResults(pool, sessionRowId, {
|
|
219
|
-
schema,
|
|
220
|
-
segments,
|
|
221
|
-
summaryText,
|
|
222
|
-
structuredSummary,
|
|
223
|
-
agentId,
|
|
224
|
-
sessionId,
|
|
225
|
-
tenantId,
|
|
226
|
-
model,
|
|
227
|
-
sourceHash,
|
|
228
|
-
msgCount,
|
|
229
|
-
userCount,
|
|
230
|
-
assistantCount,
|
|
231
|
-
startedAt,
|
|
232
|
-
endedAt,
|
|
233
|
-
embedding,
|
|
234
|
-
}) {
|
|
235
|
-
const client = await pool.connect();
|
|
236
|
-
try {
|
|
237
|
-
await client.query('BEGIN');
|
|
238
|
-
if (segments) await upsertSegments(client, sessionRowId, segments, { schema });
|
|
239
|
-
await upsertSummary(client, sessionRowId, {
|
|
240
|
-
schema, tenantId, agentId, sessionId, summaryText,
|
|
241
|
-
structuredSummary, model, sourceHash,
|
|
242
|
-
msgCount, userCount, assistantCount,
|
|
243
|
-
startedAt, endedAt, embedding,
|
|
244
|
-
});
|
|
245
|
-
await markStatus(client, sessionRowId, 'succeeded', null, { schema });
|
|
246
|
-
await client.query('COMMIT');
|
|
247
|
-
} catch (err) {
|
|
248
|
-
await client.query('ROLLBACK').catch(() => {});
|
|
249
|
-
try {
|
|
250
|
-
await markStatus(pool, sessionRowId, 'failed', err.message, { schema });
|
|
251
|
-
} catch (_) { /* swallow */ }
|
|
252
|
-
throw err;
|
|
253
|
-
} finally {
|
|
254
|
-
client.release();
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
175
|
// ---------------------------------------------------------------------------
|
|
259
176
|
// getSession
|
|
260
177
|
// ---------------------------------------------------------------------------
|
|
@@ -282,36 +199,6 @@ async function getSession(pool, sessionId, agentId, options = {}, { schema, tena
|
|
|
282
199
|
return result.rows[0] || null;
|
|
283
200
|
}
|
|
284
201
|
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
// getSessionFull
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
async function getSessionFull(pool, sessionId, agentId, { schema, tenantId } = {}) {
|
|
290
|
-
const session = await getSession(pool, sessionId, agentId, { tenantId }, { schema, tenantId });
|
|
291
|
-
if (!session) return null;
|
|
292
|
-
|
|
293
|
-
const [segResult, sumResult] = await Promise.all([
|
|
294
|
-
pool.query(
|
|
295
|
-
`SELECT * FROM ${qi(schema)}.session_segments
|
|
296
|
-
WHERE session_row_id = $1
|
|
297
|
-
ORDER BY segment_no ASC`,
|
|
298
|
-
[session.id]
|
|
299
|
-
),
|
|
300
|
-
pool.query(
|
|
301
|
-
`SELECT * FROM ${qi(schema)}.session_summaries
|
|
302
|
-
WHERE session_row_id = $1
|
|
303
|
-
LIMIT 1`,
|
|
304
|
-
[session.id]
|
|
305
|
-
),
|
|
306
|
-
]);
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
session,
|
|
310
|
-
segments: segResult.rows,
|
|
311
|
-
summary: sumResult.rows[0] || null,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
202
|
// ---------------------------------------------------------------------------
|
|
316
203
|
// getMessages
|
|
317
204
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +301,7 @@ async function recordAccess(pool, sessionRowIds, { schema } = {}) {
|
|
|
414
301
|
if (!sessionRowIds || sessionRowIds.length === 0) return;
|
|
415
302
|
await pool.query(
|
|
416
303
|
`UPDATE ${qi(schema)}.session_summaries
|
|
417
|
-
SET access_count = access_count + 1, last_accessed_at = now()
|
|
304
|
+
SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = now()
|
|
418
305
|
WHERE session_row_id = ANY($1)`,
|
|
419
306
|
[sessionRowIds]
|
|
420
307
|
);
|
|
@@ -643,12 +530,9 @@ async function recordFeedback(pool, {
|
|
|
643
530
|
|
|
644
531
|
module.exports = {
|
|
645
532
|
upsertSession,
|
|
646
|
-
upsertSegments,
|
|
647
533
|
upsertSummary,
|
|
648
534
|
markStatus,
|
|
649
|
-
persistProcessingResults,
|
|
650
535
|
getSession,
|
|
651
|
-
getSessionFull,
|
|
652
536
|
getMessages,
|
|
653
537
|
searchSessions,
|
|
654
538
|
recordAccess,
|
package/index.js
CHANGED
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
const { createAquifer } = require('./core/aquifer');
|
|
4
4
|
const { createEmbedder } = require('./pipeline/embed');
|
|
5
5
|
const { createReranker } = require('./pipeline/rerank');
|
|
6
|
-
const { normalizeSession, detectClient } = require('./pipeline/normalize');
|
|
7
6
|
|
|
8
|
-
module.exports = { createAquifer, createEmbedder, createReranker
|
|
7
|
+
module.exports = { createAquifer, createEmbedder, createReranker };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. MCP server, CLI, and library API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -17,10 +17,9 @@
|
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./index.js",
|
|
20
|
-
"./core/*": "./core/*.js",
|
|
21
|
-
"./pipeline/*": "./pipeline/*.js",
|
|
22
20
|
"./consumers/mcp": "./consumers/mcp.js",
|
|
23
21
|
"./consumers/openclaw-plugin": "./consumers/openclaw-plugin.js",
|
|
22
|
+
"./consumers/opencode": "./consumers/opencode.js",
|
|
24
23
|
"./consumers/shared/config": "./consumers/shared/config.js",
|
|
25
24
|
"./consumers/shared/factory": "./consumers/shared/factory.js"
|
|
26
25
|
},
|