@shadowforge0/aquifer-memory 1.0.2 → 1.0.3
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/core/aquifer.js +22 -13
- package/core/entity.js +29 -15
- package/core/storage.js +31 -18
- package/package.json +1 -1
package/core/aquifer.js
CHANGED
|
@@ -187,21 +187,30 @@ function createAquifer(config) {
|
|
|
187
187
|
// --- lifecycle ---
|
|
188
188
|
|
|
189
189
|
async migrate() {
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
// Advisory lock prevents concurrent migrations across processes.
|
|
191
|
+
// Lock key is derived from schema name to allow parallel migration
|
|
192
|
+
// of different schemas in the same database.
|
|
193
|
+
const lockKey = Buffer.from(`aquifer:${schema}`).reduce((h, b) => (h * 31 + b) & 0x7fffffff, 0);
|
|
194
|
+
await pool.query('SELECT pg_advisory_lock($1)', [lockKey]);
|
|
195
|
+
try {
|
|
196
|
+
// 1. Run base DDL
|
|
197
|
+
const baseSql = loadSql('001-base.sql', schema);
|
|
198
|
+
await pool.query(baseSql);
|
|
199
|
+
|
|
200
|
+
// 2. If entities enabled, run entity DDL
|
|
201
|
+
if (entitiesEnabled) {
|
|
202
|
+
const entitySql = loadSql('002-entities.sql', schema);
|
|
203
|
+
await pool.query(entitySql);
|
|
204
|
+
}
|
|
199
205
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
// 3. Trust + feedback (always, not gated by entities)
|
|
207
|
+
const trustSql = loadSql('003-trust-feedback.sql', schema);
|
|
208
|
+
await pool.query(trustSql);
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
migrated = true;
|
|
211
|
+
} finally {
|
|
212
|
+
await pool.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch(() => {});
|
|
213
|
+
}
|
|
205
214
|
},
|
|
206
215
|
|
|
207
216
|
async close() {
|
package/core/entity.js
CHANGED
|
@@ -222,27 +222,41 @@ async function upsertEntityRelations(pool, {
|
|
|
222
222
|
}) {
|
|
223
223
|
if (!pairs || pairs.length === 0) return { upserted: 0 };
|
|
224
224
|
const ts = occurredAt || new Date().toISOString();
|
|
225
|
-
let upserted = 0;
|
|
226
225
|
|
|
226
|
+
// Filter and normalize pairs
|
|
227
|
+
const validPairs = [];
|
|
227
228
|
for (const { srcEntityId, dstEntityId } of pairs) {
|
|
228
229
|
if (!srcEntityId || !dstEntityId || srcEntityId === dstEntityId) continue;
|
|
230
|
+
validPairs.push({
|
|
231
|
+
lo: Math.min(srcEntityId, dstEntityId),
|
|
232
|
+
hi: Math.max(srcEntityId, dstEntityId),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
229
235
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
242
|
-
upserted++;
|
|
236
|
+
if (validPairs.length === 0) return { upserted: 0 };
|
|
237
|
+
|
|
238
|
+
// Batch insert: multi-row VALUES
|
|
239
|
+
const COLS_PER_ROW = 3;
|
|
240
|
+
const valueClauses = [];
|
|
241
|
+
const params = [];
|
|
242
|
+
|
|
243
|
+
for (const { lo, hi } of validPairs) {
|
|
244
|
+
const off = params.length;
|
|
245
|
+
params.push(lo, hi, ts);
|
|
246
|
+
valueClauses.push(`($${off+1}, $${off+2}, 1, $${off+3}, $${off+3})`);
|
|
243
247
|
}
|
|
244
248
|
|
|
245
|
-
|
|
249
|
+
await pool.query(
|
|
250
|
+
`INSERT INTO ${qi(schema)}.entity_relations
|
|
251
|
+
(src_entity_id, dst_entity_id, co_occurrence_count, first_seen_at, last_seen_at)
|
|
252
|
+
VALUES ${valueClauses.join(',\n')}
|
|
253
|
+
ON CONFLICT (src_entity_id, dst_entity_id) DO UPDATE SET
|
|
254
|
+
co_occurrence_count = ${qi(schema)}.entity_relations.co_occurrence_count + 1,
|
|
255
|
+
last_seen_at = GREATEST(${qi(schema)}.entity_relations.last_seen_at, EXCLUDED.last_seen_at)`,
|
|
256
|
+
params
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return { upserted: validPairs.length };
|
|
246
260
|
}
|
|
247
261
|
|
|
248
262
|
// ---------------------------------------------------------------------------
|
package/core/storage.js
CHANGED
|
@@ -360,32 +360,45 @@ async function upsertTurnEmbeddings(pool, sessionRowId, {
|
|
|
360
360
|
throw new Error(`turns.length (${turns.length}) !== vectors.length (${vectors.length})`);
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
// Batch insert: build multi-row VALUES clause
|
|
364
|
+
const COLS_PER_ROW = 10;
|
|
365
|
+
const valueClauses = [];
|
|
366
|
+
const params = [];
|
|
367
|
+
|
|
363
368
|
for (let i = 0; i < turns.length; i++) {
|
|
364
369
|
const t = turns[i];
|
|
365
370
|
const vec = vectors[i];
|
|
366
371
|
if (!vec) continue;
|
|
367
372
|
|
|
368
373
|
const contentHash = crypto.createHash('sha256').update(t.text).digest('hex').slice(0, 16);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
embedding = CASE
|
|
378
|
-
WHEN ${qi(schema)}.turn_embeddings.content_hash = EXCLUDED.content_hash
|
|
379
|
-
THEN ${qi(schema)}.turn_embeddings.embedding
|
|
380
|
-
ELSE EXCLUDED.embedding
|
|
381
|
-
END`,
|
|
382
|
-
[
|
|
383
|
-
sessionRowId, tenantId, sessionId, agentId, source || null,
|
|
384
|
-
t.turnIndex, t.messageIndex,
|
|
385
|
-
t.text, contentHash, vecToStr(vec),
|
|
386
|
-
]
|
|
374
|
+
const off = params.length;
|
|
375
|
+
params.push(
|
|
376
|
+
sessionRowId, tenantId, sessionId, agentId, source || null,
|
|
377
|
+
t.turnIndex, t.messageIndex,
|
|
378
|
+
t.text, contentHash, vecToStr(vec),
|
|
379
|
+
);
|
|
380
|
+
valueClauses.push(
|
|
381
|
+
`($${off+1},$${off+2},$${off+3},$${off+4},$${off+5},$${off+6},$${off+7},'user',$${off+8},$${off+9},$${off+10}::vector)`
|
|
387
382
|
);
|
|
388
383
|
}
|
|
384
|
+
|
|
385
|
+
if (valueClauses.length === 0) return;
|
|
386
|
+
|
|
387
|
+
await pool.query(
|
|
388
|
+
`INSERT INTO ${qi(schema)}.turn_embeddings
|
|
389
|
+
(session_row_id, tenant_id, session_id, agent_id, source,
|
|
390
|
+
turn_index, message_index, role, content_text, content_hash, embedding)
|
|
391
|
+
VALUES ${valueClauses.join(',\n')}
|
|
392
|
+
ON CONFLICT (session_row_id, message_index) DO UPDATE SET
|
|
393
|
+
content_text = EXCLUDED.content_text,
|
|
394
|
+
content_hash = EXCLUDED.content_hash,
|
|
395
|
+
embedding = CASE
|
|
396
|
+
WHEN ${qi(schema)}.turn_embeddings.content_hash = EXCLUDED.content_hash
|
|
397
|
+
THEN ${qi(schema)}.turn_embeddings.embedding
|
|
398
|
+
ELSE EXCLUDED.embedding
|
|
399
|
+
END`,
|
|
400
|
+
params
|
|
401
|
+
);
|
|
389
402
|
}
|
|
390
403
|
|
|
391
404
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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": [
|