@shadowforge0/aquifer-memory 1.8.1 → 1.9.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.
Files changed (57) hide show
  1. package/.env.example +1 -0
  2. package/README.md +82 -26
  3. package/README_CN.md +33 -23
  4. package/README_TW.md +25 -24
  5. package/aquifer.config.example.json +2 -1
  6. package/consumers/cli.js +587 -33
  7. package/consumers/codex-active-checkpoint.js +3 -1
  8. package/consumers/codex-current-memory.js +10 -6
  9. package/consumers/codex.js +6 -3
  10. package/consumers/default/daily-entries.js +2 -2
  11. package/consumers/default/index.js +40 -30
  12. package/consumers/default/prompts/summary.js +2 -2
  13. package/consumers/mcp.js +56 -46
  14. package/consumers/openclaw-ext/index.js +65 -7
  15. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  16. package/consumers/openclaw-ext/package.json +1 -1
  17. package/consumers/openclaw-install.js +326 -0
  18. package/consumers/openclaw-plugin.js +105 -24
  19. package/consumers/shared/compat-recall.js +101 -0
  20. package/consumers/shared/config.js +2 -0
  21. package/consumers/shared/openclaw-product-tools.js +130 -0
  22. package/consumers/shared/recall-format.js +2 -2
  23. package/core/aquifer.js +553 -41
  24. package/core/backends/local.js +169 -1
  25. package/core/doctor.js +924 -0
  26. package/core/finalization-inspector.js +164 -0
  27. package/core/finalization-review.js +88 -42
  28. package/core/interface.js +629 -0
  29. package/core/mcp-manifest.js +11 -3
  30. package/core/memory-bootstrap.js +25 -27
  31. package/core/memory-consolidation.js +564 -42
  32. package/core/memory-explain.js +593 -0
  33. package/core/memory-promotion.js +392 -55
  34. package/core/memory-recall.js +75 -71
  35. package/core/memory-records.js +107 -108
  36. package/core/memory-review.js +891 -0
  37. package/core/memory-serving.js +61 -4
  38. package/core/memory-type-policy.js +298 -0
  39. package/core/operator-observability.js +249 -0
  40. package/core/postgres-migrations.js +22 -0
  41. package/core/session-checkpoint-producer.js +3 -1
  42. package/core/session-checkpoints.js +1 -1
  43. package/core/session-finalization.js +78 -3
  44. package/core/storage.js +124 -8
  45. package/docs/getting-started.md +50 -4
  46. package/docs/setup.md +163 -24
  47. package/package.json +5 -4
  48. package/schema/004-completion.sql +4 -4
  49. package/schema/010-v1-finalization-review.sql +72 -0
  50. package/schema/019-v1-memory-review-resolutions.sql +53 -0
  51. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  52. package/scripts/backfill-canonical-key.js +1 -1
  53. package/scripts/codex-checkpoint-commands.js +28 -0
  54. package/scripts/codex-checkpoint-runtime.js +109 -0
  55. package/scripts/codex-recovery.js +16 -4
  56. package/scripts/diagnose-fts-zh.js +1 -1
  57. package/scripts/extract-insights-from-recent-sessions.js +4 -4
package/core/aquifer.js CHANGED
@@ -14,11 +14,243 @@ const { createMemoryServingRuntime } = require('./memory-serving');
14
14
  const { createLegacyBootstrap } = require('./legacy-bootstrap');
15
15
  const { buildRerankDocument, resolveEmbedFn, shouldAutoRerank } = require('./recall-runtime');
16
16
  const { filterPublicPlaceholderSessionRows } = require('./public-session-filter');
17
+ const { buildBacklogProductStatus, buildReadinessSurface } = require('./interface');
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // createAquifer
20
21
  // ---------------------------------------------------------------------------
21
22
 
23
+ const ACTIONABLE_PENDING_STATUSES = new Set(['pending', 'failed']);
24
+ const TERMINAL_FINALIZATION_STATUSES = ['finalized', 'skipped', 'declined', 'deferred'];
25
+
26
+ function normalizePendingWorkLimit(value, fallback = 100, max = 500) {
27
+ const parsed = parseInt(value, 10);
28
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
29
+ return Math.min(parsed, max);
30
+ }
31
+
32
+ function normalizePendingWorkStatuses(value) {
33
+ const input = Array.isArray(value)
34
+ ? value
35
+ : String(value || '').split(',');
36
+ const statuses = input.map(item => String(item || '').trim()).filter(Boolean);
37
+ const normalized = statuses.length > 0 ? statuses : Array.from(ACTIONABLE_PENDING_STATUSES);
38
+ for (const status of normalized) {
39
+ if (!ACTIONABLE_PENDING_STATUSES.has(status)) {
40
+ throw new Error(`pending work status must be one of: ${Array.from(ACTIONABLE_PENDING_STATUSES).join(', ')}`);
41
+ }
42
+ }
43
+ return [...new Set(normalized)];
44
+ }
45
+
46
+ function normalizePendingWorkAction(value) {
47
+ const action = String(value || 'inspect').trim();
48
+ if (!['inspect', 'backfill', 'skip'].includes(action)) {
49
+ throw new Error('pending work action must be inspect, backfill, or skip');
50
+ }
51
+ return action;
52
+ }
53
+
54
+ function pluralize(count, singular, plural = `${singular}s`) {
55
+ return count === 1 ? singular : plural;
56
+ }
57
+
58
+ function normalizePendingWorkRow(row = {}) {
59
+ return {
60
+ sessionId: row.session_id,
61
+ sessionKey: row.session_key || null,
62
+ source: row.source || 'api',
63
+ agentId: row.agent_id,
64
+ status: row.processing_status,
65
+ processingError: row.processing_error || null,
66
+ startedAt: row.started_at || null,
67
+ lastMessageAt: row.last_message_at || null,
68
+ msgCount: row.msg_count || 0,
69
+ userCount: row.user_count || 0,
70
+ model: row.model || null,
71
+ };
72
+ }
73
+
74
+ function normalizePendingWorkGroup(row = {}) {
75
+ return {
76
+ source: row.source || 'api',
77
+ agentId: row.agent_id,
78
+ status: row.processing_status,
79
+ count: row.count || 0,
80
+ earliest: row.earliest || null,
81
+ latest: row.latest || null,
82
+ msgCount: row.msg_count || 0,
83
+ userCount: row.user_count || 0,
84
+ };
85
+ }
86
+
87
+ function buildPendingWorkGuidance(groups = []) {
88
+ const statuses = new Set(groups.map(group => group.status));
89
+ const guidance = [];
90
+ if (statuses.has('pending')) {
91
+ guidance.push({
92
+ status: 'pending',
93
+ recommendedAction: 'backfill',
94
+ alternateAction: 'skip',
95
+ reason: 'Pending sessions can be enriched; skip is reserved for deterministic policy suppression.',
96
+ });
97
+ }
98
+ if (statuses.has('failed')) {
99
+ guidance.push({
100
+ status: 'failed',
101
+ recommendedAction: 'backfill',
102
+ alternateAction: 'inspect_error',
103
+ reason: 'Failed sessions are retryable work; session skip only mutates rows still in pending status.',
104
+ });
105
+ }
106
+ return guidance;
107
+ }
108
+
109
+ function buildPendingWorkPlan(samples = [], action = 'inspect', groups = []) {
110
+ const totalByStatus = {};
111
+ for (const group of groups || []) {
112
+ totalByStatus[group.status] = (totalByStatus[group.status] || 0) + (group.count || 0);
113
+ }
114
+ const plan = {
115
+ action,
116
+ dryRunOnly: true,
117
+ allowed: 0,
118
+ blocked: 0,
119
+ changes: [],
120
+ notes: [],
121
+ };
122
+ if (action === 'inspect') {
123
+ plan.notes.push('No lifecycle changes planned.');
124
+ return plan;
125
+ }
126
+ if (action === 'backfill') {
127
+ plan.allowed = Object.values(totalByStatus).reduce((sum, count) => sum + count, 0);
128
+ } else if (action === 'skip') {
129
+ plan.allowed = totalByStatus.pending || 0;
130
+ plan.blocked = totalByStatus.failed || 0;
131
+ }
132
+ for (const row of samples) {
133
+ if (action === 'backfill') {
134
+ plan.changes.push({
135
+ sessionId: row.sessionId,
136
+ source: row.source,
137
+ agentId: row.agentId,
138
+ status: row.status,
139
+ operation: 'enrich',
140
+ allowed: true,
141
+ });
142
+ continue;
143
+ }
144
+ if (action === 'skip' && row.status === 'pending') {
145
+ plan.changes.push({
146
+ sessionId: row.sessionId,
147
+ source: row.source,
148
+ agentId: row.agentId,
149
+ status: row.status,
150
+ operation: 'mark_session_skipped',
151
+ allowed: true,
152
+ });
153
+ continue;
154
+ }
155
+ plan.changes.push({
156
+ sessionId: row.sessionId,
157
+ source: row.source,
158
+ agentId: row.agentId,
159
+ status: row.status,
160
+ operation: action,
161
+ allowed: false,
162
+ reason: action === 'skip'
163
+ ? 'skip only applies to pending sessions; failed sessions should be retried or suppressed through an explicit terminal policy'
164
+ : 'unsupported action',
165
+ });
166
+ }
167
+ if (action === 'skip' && plan.blocked > 0) {
168
+ plan.notes.push(`${plan.blocked} failed row(s) are intentionally blocked from skip. Filter to --status pending before applying skip.`);
169
+ }
170
+ return plan;
171
+ }
172
+
173
+ function buildPendingWorkDecision(groups = []) {
174
+ const statusCounts = {};
175
+ for (const group of groups || []) {
176
+ statusCounts[group.status] = (statusCounts[group.status] || 0) + (group.count || 0);
177
+ }
178
+ const publicStatus = buildBacklogProductStatus({ statusCounts });
179
+ return {
180
+ status: publicStatus.code === 'clear'
181
+ ? 'clear'
182
+ : publicStatus.code === 'recoverable'
183
+ ? 'recoverable'
184
+ : 'attention',
185
+ summary: publicStatus.headline,
186
+ nextStep: publicStatus.action,
187
+ statusCounts,
188
+ publicStatus,
189
+ };
190
+ }
191
+
192
+ function buildReadiness(stats = {}) {
193
+ const servingMode = stats.serving?.mode || 'legacy';
194
+ const activeScopePath = Array.isArray(stats.serving?.activeScopePath)
195
+ ? stats.serving.activeScopePath
196
+ : [];
197
+ const memoryRecords = stats.memoryRecords || {};
198
+ const pendingTotal = stats.pendingSessions?.total || 0;
199
+ const hasHistoricalMemory = (stats.summaries || 0) > 0 || (stats.turnEmbeddings || 0) > 0;
200
+ const currentEnabled = servingMode === 'curated';
201
+ const activeCurrentRows = memoryRecords.available ? (memoryRecords.active || 0) : 0;
202
+ const currentReady = currentEnabled && activeCurrentRows > 0;
203
+ const currentEmpty = currentEnabled && activeCurrentRows === 0;
204
+ const activeScopeReady = currentEnabled && activeScopePath.length > 0 && !(activeScopePath.length === 1 && activeScopePath[0] === 'global');
205
+
206
+ const checks = [
207
+ {
208
+ key: 'backend',
209
+ status: 'ready',
210
+ label: 'Service',
211
+ message: 'Aquifer is online.',
212
+ },
213
+ {
214
+ key: 'historical_memory',
215
+ status: hasHistoricalMemory ? 'ready' : 'empty',
216
+ label: 'Saved content',
217
+ message: hasHistoricalMemory
218
+ ? 'Saved content is available.'
219
+ : 'No saved content is available yet.',
220
+ },
221
+ {
222
+ key: 'current_memory',
223
+ status: currentReady ? 'ready' : currentEmpty ? 'empty' : 'not_enabled',
224
+ label: 'Memory',
225
+ message: currentReady
226
+ ? `Memory has ${activeCurrentRows} active row(s).`
227
+ : currentEmpty
228
+ ? 'Memory is enabled, but no active rows are available yet.'
229
+ : 'Memory is still getting ready.',
230
+ },
231
+ {
232
+ key: 'active_scope',
233
+ status: activeScopeReady ? 'ready' : currentEnabled ? 'attention' : 'not_enabled',
234
+ label: 'App link',
235
+ message: activeScopeReady
236
+ ? `Memory is linked through ${activeScopePath.join(' > ')}.`
237
+ : currentEnabled
238
+ ? 'Memory is not linked to this app yet.'
239
+ : 'Memory link is not active yet.',
240
+ },
241
+ {
242
+ key: 'backlog',
243
+ status: pendingTotal > 0 ? 'attention' : 'ready',
244
+ label: 'Saved content',
245
+ message: pendingTotal > 0
246
+ ? `${pendingTotal} saved-content ${pluralize(pendingTotal, 'item')} still need attention.`
247
+ : 'Saved content is ready.',
248
+ },
249
+ ];
250
+
251
+ return buildReadinessSurface(stats, { checks });
252
+ }
253
+
22
254
  function createAquifer(config = {}) {
23
255
  const backendKind = normalizeBackendKind(config.backend?.kind || config.storage?.backend || 'postgres');
24
256
  if (backendKind !== 'postgres') {
@@ -657,36 +889,34 @@ function createAquifer(config = {}) {
657
889
  // --- read path ---
658
890
 
659
891
  async memoryRecall(query, opts = {}) {
892
+ assertMemoryRecallQuery(query);
660
893
  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
894
  const validModes = new Set(['fts', 'hybrid', 'vector']);
666
895
  const mode = opts.mode || 'hybrid';
667
896
  if (!validModes.has(mode)) {
668
897
  throw new Error(`Invalid curated recall mode: "${mode}". Must be one of: fts, hybrid, vector`);
669
898
  }
899
+ if (mode === 'vector' && !embedFn) {
900
+ throw new Error('curated memory_recall mode=vector requires config.embed.fn or EMBED_PROVIDER env');
901
+ }
902
+ const scopedOpts = memoryServing.withDefaultScope(opts);
903
+ await ensureMigrated();
670
904
  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 {
905
+ if ((mode === 'hybrid' || mode === 'vector') && embedFn) {
677
906
  const embedded = await embedFn([query]);
678
907
  queryVec = Array.isArray(embedded) && Array.isArray(embedded[0]) ? embedded[0] : null;
679
908
  if (!queryVec && mode === 'vector') throw new Error('embedFn returned empty vector for curated memory_recall');
680
- }
681
909
  }
682
- const scopedOpts = memoryServing.withDefaultScope(opts);
683
910
  const limit = Math.max(1, Math.min(50, scopedOpts.limit || 10));
684
911
  const runLexical = mode === 'fts' || mode === 'hybrid';
685
912
  const runVector = (mode === 'vector' || mode === 'hybrid') && queryVec;
686
913
  const [lexicalRows, embeddingRows] = await Promise.all([
687
914
  runLexical ? aquifer.memory.recall(query, {
688
915
  ...scopedOpts,
689
- ftsConfig: migrationRuntime.getFtsConfig(),
916
+ // memory_records.search_tsv is generated with the stable simple config
917
+ // in schema/007, so curated memory_recall must query with the same
918
+ // config even when legacy session search selected a zh tokenizer.
919
+ ftsConfig: 'simple',
690
920
  }) : Promise.resolve([]),
691
921
  runVector ? aquifer.memory.recallViaMemoryEmbeddings(queryVec, scopedOpts) : Promise.resolve([]),
692
922
  ]);
@@ -1168,22 +1398,24 @@ function createAquifer(config = {}) {
1168
1398
  async skip(sessionId, opts = {}) {
1169
1399
  const agentId = opts.agentId || 'agent';
1170
1400
  const reason = opts.reason || null;
1401
+ const source = opts.source || null;
1171
1402
  // Atomic CAS: only skip if still pending (avoids race with concurrent enrich)
1172
1403
  const result = await pool.query(
1173
1404
  `UPDATE ${qi(schema)}.sessions
1174
1405
  SET processing_status = 'skipped', processing_error = $1
1175
1406
  WHERE session_id = $2 AND agent_id = $3 AND tenant_id = $4
1407
+ AND ($5::text IS NULL OR source = $5)
1176
1408
  AND processing_status = 'pending'
1177
1409
  RETURNING id`,
1178
- [reason, sessionId, agentId, tenantId]
1410
+ [reason, sessionId, agentId, tenantId, source]
1179
1411
  );
1180
1412
  if (result.rows.length === 0) {
1181
1413
  // Check if session exists at all
1182
- const existing = await storage.getSession(pool, sessionId, agentId, {}, { schema, tenantId });
1414
+ const existing = await storage.getSession(pool, sessionId, agentId, { source: source || undefined }, { schema, tenantId });
1183
1415
  if (!existing) throw new Error(`Session not found: ${sessionId} (agentId=${agentId})`);
1184
1416
  return null; // exists but not pending — no-op
1185
1417
  }
1186
- return { id: result.rows[0].id, sessionId, agentId, status: 'skipped' };
1418
+ return { id: result.rows[0].id, sessionId, agentId, source, status: 'skipped' };
1187
1419
  },
1188
1420
 
1189
1421
  // --- public config accessor ---
@@ -1195,6 +1427,7 @@ function createAquifer(config = {}) {
1195
1427
  memoryServingMode: memoryServing.servingMode,
1196
1428
  memoryActiveScopeKey: memoryServing.defaultActiveScopeKey,
1197
1429
  memoryActiveScopePath: memoryServing.defaultActiveScopePath,
1430
+ memoryAllowedScopeKeys: memoryServing.defaultAllowedScopeKeys,
1198
1431
  backendKind,
1199
1432
  backendProfile: backendInfo.profile,
1200
1433
  capabilities: backendInfo.capabilities,
@@ -1299,6 +1532,11 @@ function createAquifer(config = {}) {
1299
1532
  latestFinalizedAt: null,
1300
1533
  latestUpdatedAt: null,
1301
1534
  };
1535
+ let pendingSessions = {
1536
+ available: false,
1537
+ total: null,
1538
+ statuses: {},
1539
+ };
1302
1540
  try {
1303
1541
  const finalizationResult = await pool.query(
1304
1542
  `SELECT
@@ -1327,15 +1565,47 @@ function createAquifer(config = {}) {
1327
1565
  .sort()
1328
1566
  .pop() || null,
1329
1567
  };
1330
- } catch { /* session_finalizations table may not exist on older installs */ }
1568
+ } catch (err) {
1569
+ if (err?.code !== '42P01') throw err;
1570
+ /* session_finalizations table may not exist on older installs */
1571
+ }
1331
1572
 
1332
- return {
1573
+ try {
1574
+ const actionablePending = await pool.query(
1575
+ `SELECT s.processing_status, COUNT(*)::int AS count
1576
+ FROM ${qi(schema)}.sessions s
1577
+ WHERE s.tenant_id = $1
1578
+ AND s.processing_status IN ('pending', 'failed')
1579
+ AND NOT EXISTS (
1580
+ SELECT 1
1581
+ FROM ${qi(schema)}.session_finalizations f
1582
+ WHERE f.tenant_id = s.tenant_id
1583
+ AND f.session_id = s.session_id
1584
+ AND f.agent_id = s.agent_id
1585
+ AND f.source = s.source
1586
+ AND f.status IN ('finalized', 'skipped', 'declined', 'deferred')
1587
+ )
1588
+ GROUP BY s.processing_status`,
1589
+ [tenantId]
1590
+ );
1591
+ pendingSessions = {
1592
+ available: true,
1593
+ total: actionablePending.rows.reduce((sum, row) => sum + row.count, 0),
1594
+ statuses: Object.fromEntries(actionablePending.rows.map(row => [row.processing_status, row.count])),
1595
+ };
1596
+ } catch (err) {
1597
+ if (err?.code !== '42P01') throw err;
1598
+ /* session_finalizations table may not exist on older installs */
1599
+ }
1600
+
1601
+ const stats = {
1333
1602
  backendKind,
1334
1603
  backendProfile: backendInfo.profile,
1335
1604
  serving: {
1336
1605
  mode: memoryServing.servingMode,
1337
1606
  activeScopeKey: memoryServing.defaultActiveScopeKey,
1338
1607
  activeScopePath: memoryServing.defaultActiveScopePath,
1608
+ allowedScopeKeys: memoryServing.defaultAllowedScopeKeys,
1339
1609
  },
1340
1610
  sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
1341
1611
  sessionTotal: sessions.rows.reduce((s, r) => s + r.count, 0),
@@ -1344,22 +1614,175 @@ function createAquifer(config = {}) {
1344
1614
  entities: entityCount,
1345
1615
  memoryRecords,
1346
1616
  sessionFinalizations,
1617
+ pendingSessions,
1347
1618
  earliest: timeRange.rows[0]?.earliest || null,
1348
1619
  latest: timeRange.rows[0]?.latest || null,
1349
1620
  };
1621
+ return {
1622
+ ...stats,
1623
+ readiness: buildReadiness(stats),
1624
+ };
1625
+ },
1626
+
1627
+ async getPendingWork(opts = {}) {
1628
+ const limit = normalizePendingWorkLimit(opts.limit, 100, 500);
1629
+ const statuses = normalizePendingWorkStatuses(opts.statuses || opts.status);
1630
+ const action = normalizePendingWorkAction(opts.action || opts.plan);
1631
+ const filters = {
1632
+ source: opts.source || null,
1633
+ agentId: opts.agentId || null,
1634
+ statuses,
1635
+ limit,
1636
+ };
1637
+
1638
+ const params = [tenantId, statuses];
1639
+ const where = [
1640
+ 's.tenant_id = $1',
1641
+ 's.processing_status = ANY($2::text[])',
1642
+ ];
1643
+ if (filters.source) {
1644
+ params.push(filters.source);
1645
+ where.push(`s.source = $${params.length}`);
1646
+ }
1647
+ if (filters.agentId) {
1648
+ params.push(filters.agentId);
1649
+ where.push(`s.agent_id = $${params.length}`);
1650
+ }
1651
+
1652
+ const terminalExclusion = `NOT EXISTS (
1653
+ SELECT 1
1654
+ FROM ${qi(schema)}.session_finalizations f
1655
+ WHERE f.tenant_id = s.tenant_id
1656
+ AND f.session_id = s.session_id
1657
+ AND f.agent_id = s.agent_id
1658
+ AND f.source = s.source
1659
+ AND f.status = ANY($${params.length + 1}::text[])
1660
+ )`;
1661
+ const runQueries = async (excludeTerminal = true) => {
1662
+ const queryParams = excludeTerminal ? [...params, TERMINAL_FINALIZATION_STATUSES] : params;
1663
+ const whereSql = [...where, ...(excludeTerminal ? [terminalExclusion] : [])].join(' AND ');
1664
+ const limitParam = queryParams.length + 1;
1665
+ const [groupsResult, samplesResult] = await Promise.all([
1666
+ pool.query(
1667
+ `SELECT
1668
+ COALESCE(s.source, 'api') AS source,
1669
+ s.agent_id,
1670
+ s.processing_status,
1671
+ COUNT(*)::int AS count,
1672
+ MIN(s.started_at) AS earliest,
1673
+ MAX(s.started_at) AS latest,
1674
+ COALESCE(SUM(s.msg_count), 0)::int AS msg_count,
1675
+ COALESCE(SUM(s.user_count), 0)::int AS user_count
1676
+ FROM ${qi(schema)}.sessions s
1677
+ WHERE ${whereSql}
1678
+ GROUP BY COALESCE(s.source, 'api'), s.agent_id, s.processing_status
1679
+ ORDER BY COUNT(*) DESC, latest DESC`,
1680
+ queryParams,
1681
+ ),
1682
+ pool.query(
1683
+ `SELECT
1684
+ s.session_id,
1685
+ s.session_key,
1686
+ COALESCE(s.source, 'api') AS source,
1687
+ s.agent_id,
1688
+ s.processing_status,
1689
+ s.processing_error,
1690
+ s.started_at,
1691
+ s.last_message_at,
1692
+ s.msg_count,
1693
+ s.user_count,
1694
+ s.model
1695
+ FROM ${qi(schema)}.sessions s
1696
+ WHERE ${whereSql}
1697
+ ORDER BY s.started_at DESC
1698
+ LIMIT $${limitParam}`,
1699
+ [...queryParams, limit],
1700
+ ),
1701
+ ]);
1702
+ return { groupsResult, samplesResult, terminalFiltering: excludeTerminal };
1703
+ };
1704
+
1705
+ let result;
1706
+ try {
1707
+ result = await runQueries(true);
1708
+ } catch (err) {
1709
+ if (err?.code !== '42P01') throw err;
1710
+ result = await runQueries(false);
1711
+ }
1712
+
1713
+ const groups = result.groupsResult.rows.map(normalizePendingWorkGroup);
1714
+ const samples = result.samplesResult.rows.map(normalizePendingWorkRow);
1715
+ const total = groups.reduce((sum, group) => sum + group.count, 0);
1716
+ const decision = buildPendingWorkDecision(groups);
1717
+ return {
1718
+ available: true,
1719
+ generatedAt: new Date().toISOString(),
1720
+ terminalFiltering: result.terminalFiltering,
1721
+ filters,
1722
+ total,
1723
+ status: decision.status,
1724
+ statusCounts: decision.statusCounts,
1725
+ summary: decision.summary,
1726
+ nextStep: decision.nextStep,
1727
+ publicStatus: decision.publicStatus,
1728
+ groups,
1729
+ samples,
1730
+ guidance: buildPendingWorkGuidance(groups),
1731
+ plan: buildPendingWorkPlan(samples, action, groups),
1732
+ };
1350
1733
  },
1351
1734
 
1352
1735
  async getPendingSessions(opts = {}) {
1353
- 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
- );
1736
+ const limit = normalizePendingWorkLimit(opts.limit, 100, 500);
1737
+ const statuses = normalizePendingWorkStatuses(opts.statuses || opts.status);
1738
+ const params = [tenantId, statuses];
1739
+ const where = [
1740
+ 's.tenant_id = $1',
1741
+ 's.processing_status = ANY($2::text[])',
1742
+ ];
1743
+ if (opts.source) {
1744
+ params.push(opts.source);
1745
+ where.push(`s.source = $${params.length}`);
1746
+ }
1747
+ if (opts.agentId) {
1748
+ params.push(opts.agentId);
1749
+ where.push(`s.agent_id = $${params.length}`);
1750
+ }
1751
+ let result;
1752
+ try {
1753
+ const terminalParam = params.length + 1;
1754
+ const limitParam = params.length + 2;
1755
+ result = await pool.query(
1756
+ `SELECT s.session_id, s.session_key, s.source, s.agent_id, s.processing_status,
1757
+ s.processing_error, s.started_at, s.last_message_at, s.msg_count, s.user_count, s.model
1758
+ FROM ${qi(schema)}.sessions s
1759
+ WHERE ${where.join(' AND ')}
1760
+ AND NOT EXISTS (
1761
+ SELECT 1
1762
+ FROM ${qi(schema)}.session_finalizations f
1763
+ WHERE f.tenant_id = s.tenant_id
1764
+ AND f.session_id = s.session_id
1765
+ AND f.agent_id = s.agent_id
1766
+ AND f.source = s.source
1767
+ AND f.status = ANY($${terminalParam}::text[])
1768
+ )
1769
+ ORDER BY s.started_at DESC
1770
+ LIMIT $${limitParam}`,
1771
+ [...params, TERMINAL_FINALIZATION_STATUSES, limit]
1772
+ );
1773
+ } catch (err) {
1774
+ if (err?.code !== '42P01') throw err;
1775
+ const limitParam = params.length + 1;
1776
+ result = await pool.query(
1777
+ `SELECT session_id, session_key, source, agent_id, processing_status,
1778
+ processing_error, started_at, last_message_at, msg_count, user_count, model
1779
+ FROM ${qi(schema)}.sessions s
1780
+ WHERE ${where.join(' AND ')}
1781
+ ORDER BY started_at DESC
1782
+ LIMIT $${limitParam}`,
1783
+ [...params, limit]
1784
+ );
1785
+ }
1363
1786
  return result.rows;
1364
1787
  },
1365
1788
 
@@ -1386,9 +1809,10 @@ function createAquifer(config = {}) {
1386
1809
  },
1387
1810
 
1388
1811
  async memoryBootstrap(opts = {}) {
1389
- await ensureMigrated();
1390
1812
  memoryServing.assertCuratedBootstrapOpts(opts);
1391
- return aquifer.memory.bootstrap(memoryServing.withDefaultScope(opts));
1813
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1814
+ await ensureMigrated();
1815
+ return memoryBootstrap.bootstrap(scopedOpts);
1392
1816
  },
1393
1817
 
1394
1818
  async historicalBootstrap(opts = {}) {
@@ -1426,6 +1850,10 @@ function createAquifer(config = {}) {
1426
1850
  const { createMemoryConsolidation } = require('./memory-consolidation');
1427
1851
  const { createSessionFinalization } = require('./session-finalization');
1428
1852
  const { createSessionCheckpoints } = require('./session-checkpoints');
1853
+ const { createOperatorObservability } = require('./operator-observability');
1854
+ const { createDoctor } = require('./doctor');
1855
+ const { createMemoryExplain } = require('./memory-explain');
1856
+ const { createMemoryReview } = require('./memory-review');
1429
1857
  const qSchema = qi(schema);
1430
1858
  aquifer.narratives = createNarratives({ pool, schema: qSchema, defaultTenantId: tenantId });
1431
1859
  aquifer.timeline = createTimeline({ pool, schema: qSchema, defaultTenantId: tenantId });
@@ -1437,8 +1865,7 @@ function createAquifer(config = {}) {
1437
1865
  aquifer.consolidation = createConsolidation({ pool, schema: qSchema, defaultTenantId: tenantId });
1438
1866
  aquifer.bundles = createBundles({ pool, schema: qSchema, defaultTenantId: tenantId });
1439
1867
  // entityState materialises in schema/005-entity-state-history.sql, gated on
1440
- // entitiesEnabled (it FK-references entities). Drop-clean — see
1441
- // scripts/drop-entity-state-history.sql.
1868
+ // entitiesEnabled because it FK-references entities.
1442
1869
  aquifer.entityState = createEntityState({ pool, schema: qSchema, defaultTenantId: tenantId });
1443
1870
  // insights materialises in schema/006-insights.sql. No FK from elsewhere
1444
1871
  // into this table; DROP CASCADE is clean. See scripts/drop-insights.sql.
@@ -1476,6 +1903,33 @@ function createAquifer(config = {}) {
1476
1903
  schema,
1477
1904
  defaultTenantId: tenantId,
1478
1905
  });
1906
+ const operatorObservability = createOperatorObservability({
1907
+ pool,
1908
+ schema: qSchema,
1909
+ defaultTenantId: tenantId,
1910
+ });
1911
+ const doctor = createDoctor({
1912
+ pool,
1913
+ schema,
1914
+ recordsSchema: qSchema,
1915
+ defaultTenantId: tenantId,
1916
+ dbConfigured: Boolean(dbInput),
1917
+ backendKind,
1918
+ backendProfile: backendInfo.profile,
1919
+ memoryServing,
1920
+ listPendingMigrations: () => migrationRuntime.listPendingMigrations(),
1921
+ operatorObservability,
1922
+ });
1923
+ const memoryExplain = createMemoryExplain({
1924
+ pool,
1925
+ schema: qSchema,
1926
+ defaultTenantId: tenantId,
1927
+ });
1928
+ const memoryReview = createMemoryReview({
1929
+ pool,
1930
+ schema: qSchema,
1931
+ defaultTenantId: tenantId,
1932
+ });
1479
1933
 
1480
1934
  function currentMemoryScopeKeys(opts = {}) {
1481
1935
  if (Array.isArray(opts.activeScopePath) && opts.activeScopePath.length > 0) {
@@ -1485,6 +1939,12 @@ function createAquifer(config = {}) {
1485
1939
  return null;
1486
1940
  }
1487
1941
 
1942
+ function assertMemoryRecallQuery(query) {
1943
+ if (typeof query !== 'string' || query.trim().length === 0) {
1944
+ throw new Error('memory.recall(query): query must be a non-empty string');
1945
+ }
1946
+ }
1947
+
1488
1948
  // v1 curated-memory sidecar. Top-level recall/bootstrap can opt into this
1489
1949
  // plane through memory.servingMode while legacy/evidence mode remains
1490
1950
  // available for compatibility and debugging.
@@ -1520,21 +1980,25 @@ function createAquifer(config = {}) {
1520
1980
  return memoryPromotion.promote(candidates, opts);
1521
1981
  },
1522
1982
  bootstrap: async (opts = {}) => {
1983
+ memoryServing.assertCuratedBootstrapOpts(opts);
1984
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1523
1985
  await ensureMigrated();
1524
- return memoryBootstrap.bootstrap(opts);
1986
+ return memoryBootstrap.bootstrap(scopedOpts);
1525
1987
  },
1526
1988
  current: async (opts = {}) => {
1989
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1527
1990
  await ensureMigrated();
1528
- return memoryRecords.currentProjection(memoryServing.withDefaultScope(opts));
1991
+ return memoryRecords.currentProjection(scopedOpts);
1529
1992
  },
1530
1993
  listCurrentMemory: async (opts = {}) => {
1994
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1531
1995
  await ensureMigrated();
1532
- return memoryRecords.currentProjection(memoryServing.withDefaultScope(opts));
1996
+ return memoryRecords.currentProjection(scopedOpts);
1533
1997
  },
1534
1998
  backfillEmbeddings: async (opts = {}) => {
1535
- await ensureMigrated();
1536
1999
  requireEmbed('memory.backfillEmbeddings');
1537
2000
  const scopedOpts = memoryServing.withDefaultScope(opts);
2001
+ await ensureMigrated();
1538
2002
  const listInput = {
1539
2003
  tenantId: scopedOpts.tenantId || tenantId,
1540
2004
  asOf: scopedOpts.asOf,
@@ -1611,20 +2075,39 @@ function createAquifer(config = {}) {
1611
2075
  };
1612
2076
  },
1613
2077
  recall: async (query, opts = {}) => {
2078
+ assertMemoryRecallQuery(query);
2079
+ memoryServing.assertCuratedRecallOpts(opts);
2080
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1614
2081
  await ensureMigrated();
1615
- return memoryRecall.recall(query, opts);
2082
+ return memoryRecall.recall(query, scopedOpts);
1616
2083
  },
1617
2084
  recallViaEvidenceItems: async (query, opts = {}) => {
2085
+ assertMemoryRecallQuery(query);
2086
+ memoryServing.assertCuratedRecallOpts(opts);
2087
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1618
2088
  await ensureMigrated();
1619
- return memoryRecall.recallViaEvidenceItems(query, opts);
2089
+ return memoryRecall.recallViaEvidenceItems(query, scopedOpts);
1620
2090
  },
1621
2091
  recallViaMemoryEmbeddings: async (queryVec, opts = {}) => {
2092
+ memoryServing.assertCuratedRecallOpts(opts);
2093
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1622
2094
  await ensureMigrated();
1623
- return memoryRecall.recallViaMemoryEmbeddings(queryVec, opts);
2095
+ return memoryRecall.recallViaMemoryEmbeddings(queryVec, scopedOpts);
1624
2096
  },
1625
2097
  recallViaLinkedSummaryEmbeddings: async (queryVec, opts = {}) => {
2098
+ memoryServing.assertCuratedRecallOpts(opts);
2099
+ const scopedOpts = memoryServing.withDefaultScope(opts);
1626
2100
  await ensureMigrated();
1627
- return memoryRecall.recallViaLinkedSummaryEmbeddings(queryVec, opts);
2101
+ return memoryRecall.recallViaLinkedSummaryEmbeddings(queryVec, scopedOpts);
2102
+ },
2103
+ explainBootstrap: async (opts = {}) => {
2104
+ return memoryExplain.explainBootstrap(memoryServing.withDefaultScope(opts));
2105
+ },
2106
+ explainCurrent: async (query, opts = {}) => {
2107
+ return memoryExplain.explainCurrent(query, memoryServing.withDefaultScope(opts));
2108
+ },
2109
+ explainMemory: async (query, opts = {}) => {
2110
+ return memoryExplain.explainMemory(query, memoryServing.withDefaultScope(opts));
1628
2111
  },
1629
2112
  rankHybridMemoryRows: (lexicalRows, embeddingRows, opts = {}) => {
1630
2113
  return memoryRecall.rankHybridMemoryRows(lexicalRows, embeddingRows, opts);
@@ -1665,9 +2148,11 @@ function createAquifer(config = {}) {
1665
2148
  return sessionFinalization.get(input);
1666
2149
  },
1667
2150
  list: async (input = {}) => {
1668
- await ensureMigrated();
1669
2151
  return sessionFinalization.list(input);
1670
2152
  },
2153
+ inspect: async (input = {}) => {
2154
+ return sessionFinalization.inspect(input);
2155
+ },
1671
2156
  updateStatus: async (input = {}) => {
1672
2157
  await ensureMigrated();
1673
2158
  return sessionFinalization.updateStatus(input);
@@ -1722,6 +2207,33 @@ function createAquifer(config = {}) {
1722
2207
  },
1723
2208
  };
1724
2209
 
2210
+ aquifer.operator = {
2211
+ status: async (input = {}) => operatorObservability.status(input),
2212
+ inspect: async (input = {}) => operatorObservability.inspect(input),
2213
+ };
2214
+
2215
+ aquifer.doctor = {
2216
+ run: async (input = {}) => doctor.run(input),
2217
+ };
2218
+
2219
+ aquifer.review = {
2220
+ queue: async (input = {}) => {
2221
+ const scopedInput = memoryServing.withDefaultScope(input);
2222
+ await ensureMigrated();
2223
+ return memoryReview.queue(scopedInput);
2224
+ },
2225
+ inspect: async (input = {}) => {
2226
+ const scopedInput = memoryServing.withDefaultScope(input);
2227
+ await ensureMigrated();
2228
+ return memoryReview.inspect(scopedInput);
2229
+ },
2230
+ resolve: async (input = {}) => {
2231
+ const scopedInput = memoryServing.withDefaultScope(input);
2232
+ await ensureMigrated();
2233
+ return memoryReview.resolve(scopedInput);
2234
+ },
2235
+ };
2236
+
1725
2237
  aquifer.finalizeSession = aquifer.finalization.finalizeSession;
1726
2238
 
1727
2239
  return aquifer;