@shadowforge0/aquifer-memory 1.8.0 → 1.9.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/core/aquifer.js CHANGED
@@ -657,29 +657,24 @@ function createAquifer(config = {}) {
657
657
  // --- read path ---
658
658
 
659
659
  async memoryRecall(query, opts = {}) {
660
+ assertMemoryRecallQuery(query);
660
661
  memoryServing.assertCuratedRecallOpts(opts);
661
- await ensureMigrated();
662
- if (typeof query !== 'string' || query.trim().length === 0) {
663
- throw new Error('memory.recall(query): query must be a non-empty string');
664
- }
665
662
  const validModes = new Set(['fts', 'hybrid', 'vector']);
666
663
  const mode = opts.mode || 'hybrid';
667
664
  if (!validModes.has(mode)) {
668
665
  throw new Error(`Invalid curated recall mode: "${mode}". Must be one of: fts, hybrid, vector`);
669
666
  }
667
+ if (mode === 'vector' && !embedFn) {
668
+ throw new Error('curated memory_recall mode=vector requires config.embed.fn or EMBED_PROVIDER env');
669
+ }
670
+ const scopedOpts = memoryServing.withDefaultScope(opts);
671
+ await ensureMigrated();
670
672
  let queryVec = null;
671
- if (mode === 'hybrid' || mode === 'vector') {
672
- if (!embedFn) {
673
- if (mode === 'vector') {
674
- throw new Error('curated memory_recall mode=vector requires config.embed.fn or EMBED_PROVIDER env');
675
- }
676
- } else {
673
+ if ((mode === 'hybrid' || mode === 'vector') && embedFn) {
677
674
  const embedded = await embedFn([query]);
678
675
  queryVec = Array.isArray(embedded) && Array.isArray(embedded[0]) ? embedded[0] : null;
679
676
  if (!queryVec && mode === 'vector') throw new Error('embedFn returned empty vector for curated memory_recall');
680
- }
681
677
  }
682
- const scopedOpts = memoryServing.withDefaultScope(opts);
683
678
  const limit = Math.max(1, Math.min(50, scopedOpts.limit || 10));
684
679
  const runLexical = mode === 'fts' || mode === 'hybrid';
685
680
  const runVector = (mode === 'vector' || mode === 'hybrid') && queryVec;
@@ -1195,6 +1190,7 @@ function createAquifer(config = {}) {
1195
1190
  memoryServingMode: memoryServing.servingMode,
1196
1191
  memoryActiveScopeKey: memoryServing.defaultActiveScopeKey,
1197
1192
  memoryActiveScopePath: memoryServing.defaultActiveScopePath,
1193
+ memoryAllowedScopeKeys: memoryServing.defaultAllowedScopeKeys,
1198
1194
  backendKind,
1199
1195
  backendProfile: backendInfo.profile,
1200
1196
  capabilities: backendInfo.capabilities,
@@ -1299,6 +1295,11 @@ function createAquifer(config = {}) {
1299
1295
  latestFinalizedAt: null,
1300
1296
  latestUpdatedAt: null,
1301
1297
  };
1298
+ let pendingSessions = {
1299
+ available: false,
1300
+ total: null,
1301
+ statuses: {},
1302
+ };
1302
1303
  try {
1303
1304
  const finalizationResult = await pool.query(
1304
1305
  `SELECT
@@ -1327,7 +1328,38 @@ function createAquifer(config = {}) {
1327
1328
  .sort()
1328
1329
  .pop() || null,
1329
1330
  };
1330
- } catch { /* session_finalizations table may not exist on older installs */ }
1331
+ } catch (err) {
1332
+ if (err?.code !== '42P01') throw err;
1333
+ /* session_finalizations table may not exist on older installs */
1334
+ }
1335
+
1336
+ try {
1337
+ const actionablePending = await pool.query(
1338
+ `SELECT s.processing_status, COUNT(*)::int AS count
1339
+ FROM ${qi(schema)}.sessions s
1340
+ WHERE s.tenant_id = $1
1341
+ AND s.processing_status IN ('pending', 'failed')
1342
+ AND NOT EXISTS (
1343
+ SELECT 1
1344
+ FROM ${qi(schema)}.session_finalizations f
1345
+ WHERE f.tenant_id = s.tenant_id
1346
+ AND f.session_id = s.session_id
1347
+ AND f.agent_id = s.agent_id
1348
+ AND f.source = s.source
1349
+ AND f.status IN ('finalized', 'skipped', 'declined', 'deferred')
1350
+ )
1351
+ GROUP BY s.processing_status`,
1352
+ [tenantId]
1353
+ );
1354
+ pendingSessions = {
1355
+ available: true,
1356
+ total: actionablePending.rows.reduce((sum, row) => sum + row.count, 0),
1357
+ statuses: Object.fromEntries(actionablePending.rows.map(row => [row.processing_status, row.count])),
1358
+ };
1359
+ } catch (err) {
1360
+ if (err?.code !== '42P01') throw err;
1361
+ /* session_finalizations table may not exist on older installs */
1362
+ }
1331
1363
 
1332
1364
  return {
1333
1365
  backendKind,
@@ -1336,6 +1368,7 @@ function createAquifer(config = {}) {
1336
1368
  mode: memoryServing.servingMode,
1337
1369
  activeScopeKey: memoryServing.defaultActiveScopeKey,
1338
1370
  activeScopePath: memoryServing.defaultActiveScopePath,
1371
+ allowedScopeKeys: memoryServing.defaultAllowedScopeKeys,
1339
1372
  },
1340
1373
  sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
1341
1374
  sessionTotal: sessions.rows.reduce((s, r) => s + r.count, 0),
@@ -1344,6 +1377,7 @@ function createAquifer(config = {}) {
1344
1377
  entities: entityCount,
1345
1378
  memoryRecords,
1346
1379
  sessionFinalizations,
1380
+ pendingSessions,
1347
1381
  earliest: timeRange.rows[0]?.earliest || null,
1348
1382
  latest: timeRange.rows[0]?.latest || null,
1349
1383
  };
@@ -1351,15 +1385,38 @@ function createAquifer(config = {}) {
1351
1385
 
1352
1386
  async getPendingSessions(opts = {}) {
1353
1387
  const limit = opts.limit !== undefined ? opts.limit : 100;
1354
- const result = await pool.query(
1355
- `SELECT session_id, agent_id, processing_status
1356
- FROM ${qi(schema)}.sessions
1357
- WHERE tenant_id = $1
1358
- AND processing_status IN ('pending', 'failed')
1359
- ORDER BY started_at DESC
1360
- LIMIT $2`,
1361
- [tenantId, limit]
1362
- );
1388
+ let result;
1389
+ try {
1390
+ result = await pool.query(
1391
+ `SELECT s.session_id, s.agent_id, s.processing_status
1392
+ FROM ${qi(schema)}.sessions s
1393
+ WHERE s.tenant_id = $1
1394
+ AND s.processing_status IN ('pending', 'failed')
1395
+ AND NOT EXISTS (
1396
+ SELECT 1
1397
+ FROM ${qi(schema)}.session_finalizations f
1398
+ WHERE f.tenant_id = s.tenant_id
1399
+ AND f.session_id = s.session_id
1400
+ AND f.agent_id = s.agent_id
1401
+ AND f.source = s.source
1402
+ AND f.status IN ('finalized', 'skipped', 'declined', 'deferred')
1403
+ )
1404
+ ORDER BY s.started_at DESC
1405
+ LIMIT $2`,
1406
+ [tenantId, limit]
1407
+ );
1408
+ } catch (err) {
1409
+ if (err?.code !== '42P01') throw err;
1410
+ result = await pool.query(
1411
+ `SELECT session_id, agent_id, processing_status
1412
+ FROM ${qi(schema)}.sessions
1413
+ WHERE tenant_id = $1
1414
+ AND processing_status IN ('pending', 'failed')
1415
+ ORDER BY started_at DESC
1416
+ LIMIT $2`,
1417
+ [tenantId, limit]
1418
+ );
1419
+ }
1363
1420
  return result.rows;
1364
1421
  },
1365
1422
 
@@ -1386,9 +1443,10 @@ function createAquifer(config = {}) {
1386
1443
  },
1387
1444
 
1388
1445
  async memoryBootstrap(opts = {}) {
1389
- await ensureMigrated();
1390
1446
  memoryServing.assertCuratedBootstrapOpts(opts);
1391
- return aquifer.memory.bootstrap(memoryServing.withDefaultScope(opts));
1447
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1448
+ await ensureMigrated();
1449
+ return memoryBootstrap.bootstrap(scopedOpts);
1392
1450
  },
1393
1451
 
1394
1452
  async historicalBootstrap(opts = {}) {
@@ -1426,6 +1484,10 @@ function createAquifer(config = {}) {
1426
1484
  const { createMemoryConsolidation } = require('./memory-consolidation');
1427
1485
  const { createSessionFinalization } = require('./session-finalization');
1428
1486
  const { createSessionCheckpoints } = require('./session-checkpoints');
1487
+ const { createOperatorObservability } = require('./operator-observability');
1488
+ const { createDoctor } = require('./doctor');
1489
+ const { createMemoryExplain } = require('./memory-explain');
1490
+ const { createMemoryReview } = require('./memory-review');
1429
1491
  const qSchema = qi(schema);
1430
1492
  aquifer.narratives = createNarratives({ pool, schema: qSchema, defaultTenantId: tenantId });
1431
1493
  aquifer.timeline = createTimeline({ pool, schema: qSchema, defaultTenantId: tenantId });
@@ -1476,6 +1538,33 @@ function createAquifer(config = {}) {
1476
1538
  schema,
1477
1539
  defaultTenantId: tenantId,
1478
1540
  });
1541
+ const operatorObservability = createOperatorObservability({
1542
+ pool,
1543
+ schema: qSchema,
1544
+ defaultTenantId: tenantId,
1545
+ });
1546
+ const doctor = createDoctor({
1547
+ pool,
1548
+ schema,
1549
+ recordsSchema: qSchema,
1550
+ defaultTenantId: tenantId,
1551
+ dbConfigured: Boolean(dbInput),
1552
+ backendKind,
1553
+ backendProfile: backendInfo.profile,
1554
+ memoryServing,
1555
+ listPendingMigrations: () => migrationRuntime.listPendingMigrations(),
1556
+ operatorObservability,
1557
+ });
1558
+ const memoryExplain = createMemoryExplain({
1559
+ pool,
1560
+ schema: qSchema,
1561
+ defaultTenantId: tenantId,
1562
+ });
1563
+ const memoryReview = createMemoryReview({
1564
+ pool,
1565
+ schema: qSchema,
1566
+ defaultTenantId: tenantId,
1567
+ });
1479
1568
 
1480
1569
  function currentMemoryScopeKeys(opts = {}) {
1481
1570
  if (Array.isArray(opts.activeScopePath) && opts.activeScopePath.length > 0) {
@@ -1485,6 +1574,12 @@ function createAquifer(config = {}) {
1485
1574
  return null;
1486
1575
  }
1487
1576
 
1577
+ function assertMemoryRecallQuery(query) {
1578
+ if (typeof query !== 'string' || query.trim().length === 0) {
1579
+ throw new Error('memory.recall(query): query must be a non-empty string');
1580
+ }
1581
+ }
1582
+
1488
1583
  // v1 curated-memory sidecar. Top-level recall/bootstrap can opt into this
1489
1584
  // plane through memory.servingMode while legacy/evidence mode remains
1490
1585
  // available for compatibility and debugging.
@@ -1520,21 +1615,25 @@ function createAquifer(config = {}) {
1520
1615
  return memoryPromotion.promote(candidates, opts);
1521
1616
  },
1522
1617
  bootstrap: async (opts = {}) => {
1618
+ memoryServing.assertCuratedBootstrapOpts(opts);
1619
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1523
1620
  await ensureMigrated();
1524
- return memoryBootstrap.bootstrap(opts);
1621
+ return memoryBootstrap.bootstrap(scopedOpts);
1525
1622
  },
1526
1623
  current: async (opts = {}) => {
1624
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1527
1625
  await ensureMigrated();
1528
- return memoryRecords.currentProjection(memoryServing.withDefaultScope(opts));
1626
+ return memoryRecords.currentProjection(scopedOpts);
1529
1627
  },
1530
1628
  listCurrentMemory: async (opts = {}) => {
1629
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1531
1630
  await ensureMigrated();
1532
- return memoryRecords.currentProjection(memoryServing.withDefaultScope(opts));
1631
+ return memoryRecords.currentProjection(scopedOpts);
1533
1632
  },
1534
1633
  backfillEmbeddings: async (opts = {}) => {
1535
- await ensureMigrated();
1536
1634
  requireEmbed('memory.backfillEmbeddings');
1537
1635
  const scopedOpts = memoryServing.withDefaultScope(opts);
1636
+ await ensureMigrated();
1538
1637
  const listInput = {
1539
1638
  tenantId: scopedOpts.tenantId || tenantId,
1540
1639
  asOf: scopedOpts.asOf,
@@ -1611,20 +1710,39 @@ function createAquifer(config = {}) {
1611
1710
  };
1612
1711
  },
1613
1712
  recall: async (query, opts = {}) => {
1713
+ assertMemoryRecallQuery(query);
1714
+ memoryServing.assertCuratedRecallOpts(opts);
1715
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1614
1716
  await ensureMigrated();
1615
- return memoryRecall.recall(query, opts);
1717
+ return memoryRecall.recall(query, scopedOpts);
1616
1718
  },
1617
1719
  recallViaEvidenceItems: async (query, opts = {}) => {
1720
+ assertMemoryRecallQuery(query);
1721
+ memoryServing.assertCuratedRecallOpts(opts);
1722
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1618
1723
  await ensureMigrated();
1619
- return memoryRecall.recallViaEvidenceItems(query, opts);
1724
+ return memoryRecall.recallViaEvidenceItems(query, scopedOpts);
1620
1725
  },
1621
1726
  recallViaMemoryEmbeddings: async (queryVec, opts = {}) => {
1727
+ memoryServing.assertCuratedRecallOpts(opts);
1728
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1622
1729
  await ensureMigrated();
1623
- return memoryRecall.recallViaMemoryEmbeddings(queryVec, opts);
1730
+ return memoryRecall.recallViaMemoryEmbeddings(queryVec, scopedOpts);
1624
1731
  },
1625
1732
  recallViaLinkedSummaryEmbeddings: async (queryVec, opts = {}) => {
1733
+ memoryServing.assertCuratedRecallOpts(opts);
1734
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1626
1735
  await ensureMigrated();
1627
- return memoryRecall.recallViaLinkedSummaryEmbeddings(queryVec, opts);
1736
+ return memoryRecall.recallViaLinkedSummaryEmbeddings(queryVec, scopedOpts);
1737
+ },
1738
+ explainBootstrap: async (opts = {}) => {
1739
+ return memoryExplain.explainBootstrap(memoryServing.withDefaultScope(opts));
1740
+ },
1741
+ explainCurrent: async (query, opts = {}) => {
1742
+ return memoryExplain.explainCurrent(query, memoryServing.withDefaultScope(opts));
1743
+ },
1744
+ explainMemory: async (query, opts = {}) => {
1745
+ return memoryExplain.explainMemory(query, memoryServing.withDefaultScope(opts));
1628
1746
  },
1629
1747
  rankHybridMemoryRows: (lexicalRows, embeddingRows, opts = {}) => {
1630
1748
  return memoryRecall.rankHybridMemoryRows(lexicalRows, embeddingRows, opts);
@@ -1665,9 +1783,11 @@ function createAquifer(config = {}) {
1665
1783
  return sessionFinalization.get(input);
1666
1784
  },
1667
1785
  list: async (input = {}) => {
1668
- await ensureMigrated();
1669
1786
  return sessionFinalization.list(input);
1670
1787
  },
1788
+ inspect: async (input = {}) => {
1789
+ return sessionFinalization.inspect(input);
1790
+ },
1671
1791
  updateStatus: async (input = {}) => {
1672
1792
  await ensureMigrated();
1673
1793
  return sessionFinalization.updateStatus(input);
@@ -1722,6 +1842,33 @@ function createAquifer(config = {}) {
1722
1842
  },
1723
1843
  };
1724
1844
 
1845
+ aquifer.operator = {
1846
+ status: async (input = {}) => operatorObservability.status(input),
1847
+ inspect: async (input = {}) => operatorObservability.inspect(input),
1848
+ };
1849
+
1850
+ aquifer.doctor = {
1851
+ run: async (input = {}) => doctor.run(input),
1852
+ };
1853
+
1854
+ aquifer.review = {
1855
+ queue: async (input = {}) => {
1856
+ const scopedInput = memoryServing.withDefaultScope(input);
1857
+ await ensureMigrated();
1858
+ return memoryReview.queue(scopedInput);
1859
+ },
1860
+ inspect: async (input = {}) => {
1861
+ const scopedInput = memoryServing.withDefaultScope(input);
1862
+ await ensureMigrated();
1863
+ return memoryReview.inspect(scopedInput);
1864
+ },
1865
+ resolve: async (input = {}) => {
1866
+ const scopedInput = memoryServing.withDefaultScope(input);
1867
+ await ensureMigrated();
1868
+ return memoryReview.resolve(scopedInput);
1869
+ },
1870
+ };
1871
+
1725
1872
  aquifer.finalizeSession = aquifer.finalization.finalizeSession;
1726
1873
 
1727
1874
  return aquifer;
@@ -299,6 +299,115 @@ function createLocalAquifer(config = {}) {
299
299
  async memoryBootstrap() {
300
300
  unsupported('memoryBootstrap', 'curatedBootstrap');
301
301
  },
302
+ doctor: {
303
+ async run() {
304
+ return {
305
+ ok: false,
306
+ status: 'warn',
307
+ checks: [
308
+ {
309
+ id: 'backend',
310
+ status: 'warn',
311
+ summary: 'Local starter backend is running with degraded governance coverage.',
312
+ details: {
313
+ backendKind: 'local',
314
+ backendProfile: capabilities.profile,
315
+ backendPath,
316
+ capabilities: capabilities.capabilities,
317
+ },
318
+ nextAction: capabilities.upgradeHint,
319
+ },
320
+ {
321
+ id: 'db',
322
+ status: 'warn',
323
+ summary: 'PostgreSQL governance ledgers are unavailable on the local starter backend.',
324
+ details: {
325
+ configured: false,
326
+ tenantId,
327
+ },
328
+ nextAction: 'Use the PostgreSQL backend for migrations, finalization ledgers, and operator workflows.',
329
+ },
330
+ ],
331
+ };
332
+ },
333
+ },
334
+ finalization: {
335
+ async list() {
336
+ unsupported('finalization list', 'finalizationLedger');
337
+ },
338
+ async inspect() {
339
+ unsupported('finalization inspect', 'finalizationLedger');
340
+ },
341
+ },
342
+ operator: {
343
+ async status() {
344
+ return {
345
+ readOnly: true,
346
+ compaction: {
347
+ available: false,
348
+ latest: [],
349
+ staleClaims: [],
350
+ statusCounts: {},
351
+ error: 'operator compaction ledger is unavailable on the local starter backend',
352
+ },
353
+ checkpoint: {
354
+ available: false,
355
+ latest: [],
356
+ statusCounts: {},
357
+ error: 'operator checkpoint ledger is unavailable on the local starter backend',
358
+ },
359
+ };
360
+ },
361
+ async inspect() {
362
+ unsupported('operator inspect', 'operatorCompaction');
363
+ },
364
+ },
365
+ memory: {
366
+ async explainBootstrap() {
367
+ unsupported('explain bootstrap', 'curatedBootstrap');
368
+ },
369
+ async explainCurrent() {
370
+ unsupported('explain memory', 'curatedRecall');
371
+ },
372
+ async explainMemory() {
373
+ unsupported('explain memory', 'curatedRecall');
374
+ },
375
+ },
376
+ review: {
377
+ async queue() {
378
+ return {
379
+ readOnly: true,
380
+ derived: true,
381
+ available: false,
382
+ items: [],
383
+ reviewQueue: [],
384
+ scope: {
385
+ activeScopeKey: null,
386
+ activeScopePath: [],
387
+ },
388
+ filters: {},
389
+ summary: {
390
+ scanned: 0,
391
+ queued: 0,
392
+ truncated: false,
393
+ severityCounts: {},
394
+ feedbackTypeCounts: {},
395
+ },
396
+ totals: {
397
+ items: 0,
398
+ issueFeedback: 0,
399
+ allFeedback: 0,
400
+ },
401
+ error: 'Memory review queue is unavailable on the local starter backend',
402
+ };
403
+ },
404
+ async inspect() {
405
+ unsupported('review inspect', 'curatedRecall');
406
+ },
407
+ async resolve() {
408
+ unsupported('review resolve', 'curatedRecall');
409
+ },
410
+ },
302
411
  async historicalBootstrap(opts = {}) {
303
412
  return this.bootstrap(opts);
304
413
  },