@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 CHANGED
@@ -187,21 +187,30 @@ function createAquifer(config) {
187
187
  // --- lifecycle ---
188
188
 
189
189
  async migrate() {
190
- // 1. Run base DDL
191
- const baseSql = loadSql('001-base.sql', schema);
192
- await pool.query(baseSql);
193
-
194
- // 2. If entities enabled, run entity DDL
195
- if (entitiesEnabled) {
196
- const entitySql = loadSql('002-entities.sql', schema);
197
- await pool.query(entitySql);
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
- // 3. Trust + feedback (always, not gated by entities)
201
- const trustSql = loadSql('003-trust-feedback.sql', schema);
202
- await pool.query(trustSql);
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
- migrated = true;
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
- const lo = Math.min(srcEntityId, dstEntityId);
231
- const hi = Math.max(srcEntityId, dstEntityId);
232
-
233
- await pool.query(
234
- `INSERT INTO ${qi(schema)}.entity_relations
235
- (src_entity_id, dst_entity_id, co_occurrence_count, first_seen_at, last_seen_at)
236
- VALUES ($1, $2, 1, $3, $3)
237
- ON CONFLICT (src_entity_id, dst_entity_id) DO UPDATE SET
238
- co_occurrence_count = ${qi(schema)}.entity_relations.co_occurrence_count + 1,
239
- last_seen_at = GREATEST(${qi(schema)}.entity_relations.last_seen_at, EXCLUDED.last_seen_at)`,
240
- [lo, hi, ts]
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
- return { upserted };
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
- await pool.query(
370
- `INSERT INTO ${qi(schema)}.turn_embeddings
371
- (session_row_id, tenant_id, session_id, agent_id, source,
372
- turn_index, message_index, role, content_text, content_hash, embedding)
373
- VALUES ($1,$2,$3,$4,$5,$6,$7,'user',$8,$9,$10::vector)
374
- ON CONFLICT (session_row_id, message_index) DO UPDATE SET
375
- content_text = EXCLUDED.content_text,
376
- content_hash = EXCLUDED.content_hash,
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.2",
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": [