@shadowforge0/aquifer-memory 1.7.0 → 1.8.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 (39) hide show
  1. package/.env.example +8 -0
  2. package/README.md +66 -0
  3. package/aquifer.config.example.json +19 -0
  4. package/consumers/cli.js +217 -14
  5. package/consumers/codex-active-checkpoint.js +186 -0
  6. package/consumers/codex-current-memory.js +106 -0
  7. package/consumers/codex-handoff.js +442 -3
  8. package/consumers/codex.js +164 -107
  9. package/consumers/mcp.js +144 -6
  10. package/consumers/shared/config.js +60 -1
  11. package/consumers/shared/factory.js +10 -3
  12. package/core/aquifer.js +351 -840
  13. package/core/backends/capabilities.js +89 -0
  14. package/core/backends/local.js +430 -0
  15. package/core/legacy-bootstrap.js +140 -0
  16. package/core/mcp-manifest.js +66 -2
  17. package/core/memory-promotion.js +157 -26
  18. package/core/memory-recall.js +341 -22
  19. package/core/memory-records.js +128 -8
  20. package/core/memory-serving.js +132 -0
  21. package/core/postgres-migrations.js +533 -0
  22. package/core/public-session-filter.js +40 -0
  23. package/core/recall-runtime.js +115 -0
  24. package/core/scope-attribution.js +279 -0
  25. package/core/session-checkpoint-producer.js +412 -0
  26. package/core/session-checkpoints.js +432 -0
  27. package/core/session-finalization.js +82 -1
  28. package/core/storage-checkpoints.js +546 -0
  29. package/core/storage.js +121 -8
  30. package/docs/setup.md +22 -0
  31. package/package.json +8 -4
  32. package/schema/014-v1-checkpoint-runs.sql +349 -0
  33. package/schema/015-v1-evidence-items.sql +92 -0
  34. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  35. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  36. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  37. package/scripts/codex-checkpoint-commands.js +464 -0
  38. package/scripts/codex-checkpoint-runtime.js +520 -0
  39. package/scripts/codex-recovery.js +105 -0
@@ -22,6 +22,16 @@ const crypto = require('crypto');
22
22
  const DEFAULT_CODEX_HOME = path.join(os.homedir(), '.codex');
23
23
  const { normalizeMessages } = require('./shared/normalize');
24
24
  const { applyEnrichSafetyGate } = require('../core/memory-safety-gate');
25
+ const {
26
+ buildActiveSessionCheckpointInput,
27
+ buildActiveSessionCheckpointPrompt,
28
+ prepareActiveSessionCheckpoint: prepareActiveSessionCheckpointImpl,
29
+ } = require('./codex-active-checkpoint');
30
+ const {
31
+ formatCurrentMemoryPromptBlock,
32
+ compactCurrentMemorySnapshot,
33
+ resolveCurrentMemoryForFinalization,
34
+ } = require('./codex-current-memory');
25
35
  const DEFAULT_IDLE_MS = 5 * 60 * 1000;
26
36
  const DEFAULT_CLAIM_TTL_MS = 5 * 60 * 1000;
27
37
  const DEFAULT_MIN_BYTES = 1000;
@@ -1102,6 +1112,92 @@ function approxPromptTokens(text) {
1102
1112
  return Math.ceil(String(text || '').length / 3);
1103
1113
  }
1104
1114
 
1115
+ function recoveryPromptBudgetExceeded(text, opts = {}) {
1116
+ const promptTokens = approxPromptTokens(text);
1117
+ const maxChars = opts.maxChars;
1118
+ const maxPromptTokens = opts.maxPromptTokens;
1119
+ return {
1120
+ promptTokens,
1121
+ exceeded: Boolean(
1122
+ (Number.isFinite(maxChars) && maxChars > 0 && text.length > maxChars)
1123
+ || (Number.isFinite(maxPromptTokens) && maxPromptTokens > 0 && promptTokens > maxPromptTokens)
1124
+ ),
1125
+ };
1126
+ }
1127
+
1128
+ function buildBoundedRecoveryTranscript(safeMessages = [], opts = {}) {
1129
+ const allowTail = opts.allowTail === true;
1130
+ const maxMessages = opts.maxMessages;
1131
+ const maxChars = opts.maxChars;
1132
+ const maxPromptTokens = opts.maxPromptTokens;
1133
+ const fullText = formatRecoveryTranscript(safeMessages);
1134
+ const fullPromptTokens = approxPromptTokens(fullText);
1135
+ let messages = safeMessages;
1136
+ let omittedMessageCount = 0;
1137
+ let truncatedByMessages = false;
1138
+
1139
+ if (Number.isFinite(maxMessages) && maxMessages > 0 && messages.length > maxMessages) {
1140
+ if (!allowTail) {
1141
+ return {
1142
+ status: 'deferred',
1143
+ reason: 'max_messages',
1144
+ messageCount: messages.length,
1145
+ };
1146
+ }
1147
+ omittedMessageCount = messages.length - maxMessages;
1148
+ messages = messages.slice(-maxMessages);
1149
+ truncatedByMessages = true;
1150
+ }
1151
+
1152
+ let text = formatRecoveryTranscript(messages);
1153
+ let budget = recoveryPromptBudgetExceeded(text, { maxChars, maxPromptTokens });
1154
+ let truncatedByBudget = false;
1155
+ while (allowTail && budget.exceeded && messages.length > 1) {
1156
+ messages = messages.slice(1);
1157
+ omittedMessageCount += 1;
1158
+ truncatedByBudget = true;
1159
+ text = formatRecoveryTranscript(messages);
1160
+ budget = recoveryPromptBudgetExceeded(text, { maxChars, maxPromptTokens });
1161
+ }
1162
+
1163
+ if (budget.exceeded) {
1164
+ return {
1165
+ status: 'deferred',
1166
+ reason: 'prompt_budget',
1167
+ charCount: text.length,
1168
+ approxPromptTokens: budget.promptTokens,
1169
+ };
1170
+ }
1171
+
1172
+ return {
1173
+ status: 'ok',
1174
+ messages,
1175
+ text,
1176
+ charCount: text.length,
1177
+ approxPromptTokens: budget.promptTokens,
1178
+ fullCharCount: fullText.length,
1179
+ fullApproxPromptTokens: fullPromptTokens,
1180
+ truncated: truncatedByMessages || truncatedByBudget,
1181
+ transcriptWindow: {
1182
+ mode: allowTail ? 'tail' : 'full',
1183
+ truncated: truncatedByMessages || truncatedByBudget,
1184
+ omittedMessageCount,
1185
+ includedMessageCount: messages.length,
1186
+ totalMessageCount: safeMessages.length,
1187
+ truncatedByMessages,
1188
+ truncatedByBudget,
1189
+ },
1190
+ coverage: {
1191
+ coordinateSystem: 'codex_sanitized_view_v1',
1192
+ messageIndexBase: 0,
1193
+ charIndexBase: 0,
1194
+ semantics: 'coveredUntilChar is the first uncovered zero-based char offset; messages up to coveredUntilMessageIndex are covered.',
1195
+ coveredUntilMessageIndex: safeMessages.length > 0 ? safeMessages.length - 1 : 0,
1196
+ coveredUntilChar: fullText.length,
1197
+ },
1198
+ };
1199
+ }
1200
+
1105
1201
  function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1106
1202
  const filePath = candidate.filePath || candidate.metadata?.filePath;
1107
1203
  if (!filePath) {
@@ -1115,6 +1211,7 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1115
1211
  const maxMessages = opts.maxRecoveryMessages ?? DEFAULT_RECOVERY_MAX_MESSAGES;
1116
1212
  const maxChars = opts.maxRecoveryChars ?? DEFAULT_RECOVERY_MAX_CHARS;
1117
1213
  const maxPromptTokens = opts.maxRecoveryPromptTokens ?? DEFAULT_RECOVERY_MAX_PROMPT_TOKENS;
1214
+ const allowTail = opts.tailOnMaxBudget === true || opts.tailOnBudget === true || opts.tail === true;
1118
1215
 
1119
1216
  let stat;
1120
1217
  try {
@@ -1122,7 +1219,7 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1122
1219
  } catch {
1123
1220
  return { status: 'not_found', sessionId: candidate.sessionId || null, filePath };
1124
1221
  }
1125
- if (Number.isFinite(maxBytes) && maxBytes > 0 && stat.size > maxBytes) {
1222
+ if (!allowTail && Number.isFinite(maxBytes) && maxBytes > 0 && stat.size > maxBytes) {
1126
1223
  return { status: 'deferred', reason: 'max_bytes', sessionId: candidate.sessionId || null, filePath, size: stat.size };
1127
1224
  }
1128
1225
 
@@ -1154,7 +1251,13 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1154
1251
  safetyGate: safety.meta,
1155
1252
  };
1156
1253
  }
1157
- if (Number.isFinite(maxMessages) && maxMessages > 0 && safeMessages.length > maxMessages) {
1254
+ const bounded = buildBoundedRecoveryTranscript(safeMessages, {
1255
+ allowTail,
1256
+ maxMessages,
1257
+ maxChars,
1258
+ maxPromptTokens,
1259
+ });
1260
+ if (bounded.status === 'deferred' && bounded.reason === 'max_messages') {
1158
1261
  return {
1159
1262
  status: 'deferred',
1160
1263
  reason: 'max_messages',
@@ -1166,20 +1269,16 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1166
1269
  safetyGate: safety.meta,
1167
1270
  };
1168
1271
  }
1169
-
1170
- const text = formatRecoveryTranscript(safeMessages);
1171
- const promptTokens = approxPromptTokens(text);
1172
- if ((Number.isFinite(maxChars) && maxChars > 0 && text.length > maxChars)
1173
- || (Number.isFinite(maxPromptTokens) && maxPromptTokens > 0 && promptTokens > maxPromptTokens)) {
1272
+ if (bounded.status === 'deferred') {
1174
1273
  return {
1175
1274
  status: 'deferred',
1176
- reason: 'prompt_budget',
1275
+ reason: bounded.reason,
1177
1276
  sessionId: parsed.sessionId,
1178
1277
  fileSessionId: parsed.fileSessionId,
1179
1278
  filePath,
1180
1279
  transcriptHash,
1181
- charCount: text.length,
1182
- approxPromptTokens: promptTokens,
1280
+ charCount: bounded.charCount,
1281
+ approxPromptTokens: bounded.approxPromptTokens,
1183
1282
  safetyGate: safety.meta,
1184
1283
  };
1185
1284
  }
@@ -1190,16 +1289,23 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1190
1289
  fileSessionId: parsed.fileSessionId,
1191
1290
  filePath,
1192
1291
  transcriptHash,
1193
- messages: safeMessages,
1194
- text,
1195
- charCount: text.length,
1196
- approxPromptTokens: promptTokens,
1292
+ messages: bounded.messages,
1293
+ text: bounded.text,
1294
+ charCount: bounded.charCount,
1295
+ approxPromptTokens: bounded.approxPromptTokens,
1296
+ fullCharCount: bounded.fullCharCount,
1297
+ fullApproxPromptTokens: bounded.fullApproxPromptTokens,
1298
+ truncated: bounded.truncated,
1299
+ transcriptWindow: bounded.transcriptWindow,
1300
+ coverage: bounded.coverage,
1197
1301
  safetyGate: safety.meta,
1198
1302
  counts: {
1199
1303
  messageCount: parsed.normalized.messages.length,
1200
1304
  safeMessageCount: safeMessages.length,
1201
1305
  userCount: parsed.normalized.userCount,
1202
1306
  assistantCount: parsed.normalized.assistantCount,
1307
+ fullCharCount: bounded.fullCharCount,
1308
+ fullApproxPromptTokens: bounded.fullApproxPromptTokens,
1203
1309
  },
1204
1310
  metadata: {
1205
1311
  model: parsed.normalized.model,
@@ -1212,96 +1318,11 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1212
1318
  };
1213
1319
  }
1214
1320
 
1215
- function compactCurrentMemoryRow(row = {}) {
1216
- const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
1217
- const confidence = payload.confidence || payload.currentMemoryConfidence || null;
1218
- return {
1219
- memoryType: row.memoryType || row.memory_type || 'memory',
1220
- canonicalKey: row.canonicalKey || row.canonical_key || null,
1221
- scopeKey: row.scopeKey || row.scope_key || null,
1222
- summary: String(row.summary || row.title || '').replace(/\s+/g, ' ').trim(),
1223
- authority: row.authority || null,
1224
- confidence,
1225
- };
1226
- }
1227
-
1228
- function formatCurrentMemoryPromptBlock(currentMemory = null, opts = {}) {
1229
- const maxItems = Math.max(0, Math.min(20, opts.maxCurrentMemoryItems || opts.currentMemoryLimit || 12));
1230
- const meta = currentMemory && currentMemory.meta ? currentMemory.meta : {};
1231
- const rows = Array.isArray(currentMemory?.memories)
1232
- ? currentMemory.memories
1233
- : (Array.isArray(currentMemory?.items) ? currentMemory.items : []);
1234
- const compactRows = rows.map(compactCurrentMemoryRow).filter(row => row.summary).slice(0, maxItems);
1235
- const attrs = [
1236
- `source="${meta.source || 'memory_records'}"`,
1237
- `serving_contract="${meta.servingContract || meta.serving_contract || 'current_memory_v1'}"`,
1238
- `count="${compactRows.length}"`,
1239
- `truncated="${Boolean(meta.truncated || rows.length > compactRows.length)}"`,
1240
- `degraded="${Boolean(meta.degraded || currentMemory?.error)}"`,
1241
- ];
1242
- const lines = compactRows.map(row => {
1243
- const scope = row.scopeKey ? ` scope=${row.scopeKey}` : '';
1244
- const authority = row.authority ? ` authority=${row.authority}` : '';
1245
- const confidence = row.confidence ? ` confidence=${row.confidence}` : '';
1246
- return `- ${row.memoryType}${scope}${authority}${confidence}: ${row.summary}`;
1321
+ async function prepareActiveSessionCheckpoint(aquifer, opts = {}) {
1322
+ return prepareActiveSessionCheckpointImpl(aquifer, opts, {
1323
+ materializeRecoveryTranscriptView,
1324
+ resolveCurrentMemoryForFinalization,
1247
1325
  });
1248
- if (currentMemory && currentMemory.error && lines.length === 0) {
1249
- lines.push(`- degraded: ${String(currentMemory.error).replace(/\s+/g, ' ').trim()}`);
1250
- }
1251
- if (lines.length === 0) lines.push('- none');
1252
- return [
1253
- `<current_memory ${attrs.join(' ')}>`,
1254
- ...lines,
1255
- '</current_memory>',
1256
- ].join('\n');
1257
- }
1258
-
1259
- function compactCurrentMemorySnapshot(currentMemory = null, opts = {}) {
1260
- const maxItems = Math.max(0, Math.min(20, opts.maxCurrentMemoryItems || opts.currentMemoryLimit || 12));
1261
- const meta = currentMemory && currentMemory.meta ? currentMemory.meta : {};
1262
- const rows = Array.isArray(currentMemory?.memories)
1263
- ? currentMemory.memories
1264
- : (Array.isArray(currentMemory?.items) ? currentMemory.items : []);
1265
- return {
1266
- memories: rows.map(compactCurrentMemoryRow).filter(row => row.summary).slice(0, maxItems),
1267
- meta: {
1268
- source: meta.source || 'memory_records',
1269
- servingContract: meta.servingContract || meta.serving_contract || 'current_memory_v1',
1270
- count: Math.min(rows.length, maxItems),
1271
- truncated: Boolean(meta.truncated || rows.length > maxItems),
1272
- degraded: Boolean(meta.degraded || currentMemory?.error),
1273
- },
1274
- };
1275
- }
1276
-
1277
- async function resolveCurrentMemoryForFinalization(aquifer, opts = {}) {
1278
- if (opts.includeCurrentMemory === false) return null;
1279
- if (opts.currentMemory !== undefined) return opts.currentMemory;
1280
- const currentFn = aquifer?.memory?.current || aquifer?.memory?.listCurrentMemory;
1281
- if (typeof currentFn !== 'function') return null;
1282
- const limit = Math.max(1, Math.min(20, opts.currentMemoryLimit || opts.maxCurrentMemoryItems || 12));
1283
- try {
1284
- return await currentFn.call(aquifer.memory, {
1285
- tenantId: opts.tenantId,
1286
- activeScopeKey: opts.activeScopeKey || opts.scopeKey,
1287
- activeScopePath: opts.activeScopePath,
1288
- scopeId: opts.scopeId,
1289
- asOf: opts.asOf,
1290
- limit,
1291
- });
1292
- } catch (err) {
1293
- return {
1294
- memories: [],
1295
- meta: {
1296
- source: 'memory_records',
1297
- servingContract: 'current_memory_v1',
1298
- count: 0,
1299
- truncated: false,
1300
- degraded: true,
1301
- },
1302
- error: err.message,
1303
- };
1304
- }
1305
1326
  }
1306
1327
 
1307
1328
  function buildFinalizationPrompt(view = {}, opts = {}) {
@@ -1346,6 +1367,30 @@ function normalizeFinalizationSummary(summary = {}) {
1346
1367
  return { summaryText, structuredSummary };
1347
1368
  }
1348
1369
 
1370
+ function inferScopeKindFromKey(scopeKey) {
1371
+ const key = String(scopeKey || '').trim();
1372
+ if (!key) return null;
1373
+ if (key === 'global') return 'global';
1374
+ const prefix = key.split(':')[0];
1375
+ return ['user', 'workspace', 'project', 'repo', 'task', 'session', 'host_runtime'].includes(prefix)
1376
+ ? prefix
1377
+ : null;
1378
+ }
1379
+
1380
+ function resolveFinalizationScope(opts = {}) {
1381
+ const meta = opts.currentMemory && opts.currentMemory.meta ? opts.currentMemory.meta : {};
1382
+ const metaPath = Array.isArray(meta.activeScopePath) ? meta.activeScopePath : null;
1383
+ const scopeKey = opts.scopeKey
1384
+ || opts.activeScopeKey
1385
+ || meta.activeScopeKey
1386
+ || (metaPath && metaPath.length > 0 ? metaPath[metaPath.length - 1] : null)
1387
+ || null;
1388
+ return {
1389
+ scopeKey,
1390
+ scopeKind: opts.scopeKind || inferScopeKindFromKey(scopeKey),
1391
+ };
1392
+ }
1393
+
1349
1394
  async function ensureCommittedForFinalization(aquifer, view = {}, opts = {}) {
1350
1395
  const {
1351
1396
  agentId = 'main',
@@ -1415,10 +1460,15 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
1415
1460
  trigger: mode,
1416
1461
  ...(opts.metadata || {}),
1417
1462
  };
1463
+ let resolvedCurrentMemory = opts.currentMemory;
1418
1464
  if (!metadata.currentMemory) {
1419
- const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
1420
- if (currentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(currentMemory, opts);
1465
+ resolvedCurrentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
1466
+ if (resolvedCurrentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(resolvedCurrentMemory, opts);
1421
1467
  }
1468
+ const finalizationScope = resolveFinalizationScope({
1469
+ ...opts,
1470
+ currentMemory: resolvedCurrentMemory,
1471
+ });
1422
1472
  const result = await finalizeSession({
1423
1473
  sessionId: view.sessionId,
1424
1474
  agentId,
@@ -1435,13 +1485,15 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
1435
1485
  startedAt: view.metadata?.startedAt || null,
1436
1486
  endedAt: view.metadata?.lastMessageAt || null,
1437
1487
  embedding: opts.embedding || null,
1438
- scopeKind: opts.scopeKind || null,
1439
- scopeKey: opts.scopeKey || null,
1488
+ scopeKind: finalizationScope.scopeKind,
1489
+ scopeKey: finalizationScope.scopeKey,
1440
1490
  contextKey: opts.contextKey || null,
1441
1491
  topicKey: opts.topicKey || null,
1442
1492
  authority: opts.authority || 'verified_summary',
1443
1493
  candidates: Array.isArray(opts.candidates) ? opts.candidates : undefined,
1444
1494
  candidatePayload: opts.candidatePayload || null,
1495
+ candidateEnvelope: opts.candidateEnvelope || null,
1496
+ coverage: opts.coverage || null,
1445
1497
  metadata,
1446
1498
  });
1447
1499
  const humanReviewText = result.humanReviewText || '';
@@ -1594,6 +1646,8 @@ async function finalizeCodexSession(aquifer, input = {}, opts = {}) {
1594
1646
  currentMemory: input.currentMemory !== undefined ? input.currentMemory : opts.currentMemory,
1595
1647
  currentMemoryLimit: input.currentMemoryLimit || opts.currentMemoryLimit || null,
1596
1648
  includeCurrentMemory: input.includeCurrentMemory !== undefined ? input.includeCurrentMemory : opts.includeCurrentMemory,
1649
+ candidateEnvelope: input.candidateEnvelope || opts.candidateEnvelope || null,
1650
+ coverage: input.coverage || opts.coverage || null,
1597
1651
  });
1598
1652
  }
1599
1653
 
@@ -1654,6 +1708,9 @@ module.exports = {
1654
1708
  findDbEligibleRecoveryCandidates,
1655
1709
  materializeRecoveryTranscriptView,
1656
1710
  buildFinalizationPrompt,
1711
+ buildActiveSessionCheckpointInput,
1712
+ buildActiveSessionCheckpointPrompt,
1713
+ prepareActiveSessionCheckpoint,
1657
1714
  prepareSessionStartRecovery,
1658
1715
  recordRecoveryDecision,
1659
1716
  finalizeTranscriptView,
package/consumers/mcp.js CHANGED
@@ -7,8 +7,9 @@
7
7
  * This is the primary integration surface for Aquifer. Agent hosts (Claude Code,
8
8
  * Codex, OpenCode, etc.) should integrate through this MCP server.
9
9
  *
10
- * Tools: session_recall, evidence_recall, session_feedback, memory_feedback,
11
- * feedback_stats, session_bootstrap, memory_stats, memory_pending
10
+ * Tools: memory_recall, historical_recall, session_recall, evidence_recall,
11
+ * session_feedback, memory_feedback, feedback_stats, session_bootstrap,
12
+ * memory_stats, memory_pending
12
13
  *
13
14
  * Usage:
14
15
  * npx aquifer mcp
@@ -38,6 +39,32 @@ function formatResults(results, query, explain) {
38
39
  return formatRecallResults(results, { query, showScore: true, showExplain: !!explain });
39
40
  }
40
41
 
42
+ function scopeLabel(config = {}) {
43
+ if (Array.isArray(config.memoryActiveScopePath) && config.memoryActiveScopePath.length > 0) {
44
+ return config.memoryActiveScopePath.join(' > ');
45
+ }
46
+ return config.memoryActiveScopeKey || 'none';
47
+ }
48
+
49
+ function sessionRecallLaneHeader(config = {}) {
50
+ const mode = config.memoryServingMode || 'legacy';
51
+ if (mode === 'curated') {
52
+ return `Serving lane: curated current memory (active scope: ${scopeLabel(config)})`;
53
+ }
54
+ return 'Serving lane: legacy evidence/session recall (not current memory)';
55
+ }
56
+
57
+ function memoryRecallLaneHeader(config = {}, params = {}) {
58
+ const scope = Array.isArray(params.activeScopePath) && params.activeScopePath.length > 0
59
+ ? params.activeScopePath.join(' > ')
60
+ : (params.activeScopeKey || scopeLabel(config));
61
+ return `Serving lane: explicit current memory recall (active scope: ${scope || 'none'})`;
62
+ }
63
+
64
+ function historicalRecallLaneHeader() {
65
+ return 'Serving lane: explicit historical/session recall';
66
+ }
67
+
41
68
  // ---------------------------------------------------------------------------
42
69
  // Start MCP server
43
70
  // ---------------------------------------------------------------------------
@@ -63,9 +90,93 @@ async function main() {
63
90
  version: packageVersion,
64
91
  });
65
92
 
93
+ server.tool(
94
+ 'memory_recall',
95
+ 'Explicit current-memory recall on the active curated memory corpus. Use this for current state / next-step lookup; use historical_recall for older session detail.',
96
+ {
97
+ query: z.string().min(1).describe('Search query (keyword or natural language)'),
98
+ limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
99
+ mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only), "hybrid" (default, FTS + vector), "vector" (vector only)'),
100
+ explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
101
+ activeScopeKey: z.string().optional().describe('Active curated memory scope key'),
102
+ activeScopePath: z.array(z.string()).optional().describe('Ordered curated scope path'),
103
+ },
104
+ async (params) => {
105
+ try {
106
+ const aquifer = getAquifer();
107
+ const recallOpts = {
108
+ limit: params.limit || 5,
109
+ activeScopeKey: params.activeScopeKey || undefined,
110
+ activeScopePath: params.activeScopePath || undefined,
111
+ };
112
+ if (params.mode) recallOpts.mode = params.mode;
113
+
114
+ const results = await aquifer.memoryRecall(params.query, recallOpts);
115
+ const text = [
116
+ memoryRecallLaneHeader(aquifer.getConfig ? aquifer.getConfig() : {}, params),
117
+ '',
118
+ formatResults(results, params.query, params.explain),
119
+ ].join('\n');
120
+ return { content: [{ type: 'text', text }] };
121
+ } catch (err) {
122
+ return {
123
+ content: [{ type: 'text', text: `memory_recall error: ${err.message}` }],
124
+ isError: true,
125
+ };
126
+ }
127
+ }
128
+ );
129
+
130
+ server.tool(
131
+ 'historical_recall',
132
+ 'Explicit historical/session recall over stored sessions and summaries. Use this for timeline, detail, and session context lookup; use evidence_recall only for audit/debug/provenance.',
133
+ {
134
+ query: z.string().min(1).describe('Search query (keyword or natural language)'),
135
+ limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
136
+ agentId: z.string().optional().describe('Filter by agent ID'),
137
+ source: z.string().optional().describe('Filter by source (e.g., gateway, cc)'),
138
+ dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
139
+ dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
140
+ entities: z.array(z.string()).optional().describe('Entity names to match'),
141
+ entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
142
+ mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
143
+ explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
144
+ },
145
+ async (params) => {
146
+ try {
147
+ const aquifer = getAquifer();
148
+ const recallOpts = {
149
+ limit: params.limit || 5,
150
+ agentId: params.agentId || undefined,
151
+ source: params.source || undefined,
152
+ dateFrom: params.dateFrom || undefined,
153
+ dateTo: params.dateTo || undefined,
154
+ };
155
+ if (params.entities && params.entities.length > 0) {
156
+ recallOpts.entities = params.entities;
157
+ recallOpts.entityMode = params.entityMode || 'any';
158
+ }
159
+ if (params.mode) recallOpts.mode = params.mode;
160
+
161
+ const results = await aquifer.historicalRecall(params.query, recallOpts);
162
+ const text = [
163
+ historicalRecallLaneHeader(),
164
+ '',
165
+ formatResults(results, params.query, params.explain),
166
+ ].join('\n');
167
+ return { content: [{ type: 'text', text }] };
168
+ } catch (err) {
169
+ return {
170
+ content: [{ type: 'text', text: `historical_recall error: ${err.message}` }],
171
+ isError: true,
172
+ };
173
+ }
174
+ }
175
+ );
176
+
66
177
  server.tool(
67
178
  'session_recall',
68
- 'Search Aquifer memory. In curated serving mode this searches active curated memory only; use evidence_recall for legacy session/evidence lookup.',
179
+ 'Compatibility recall surface. In curated serving mode this routes to current memory; in legacy serving mode it routes to historical/session recall. Prefer memory_recall for current state and historical_recall for timeline/detail lookup.',
69
180
  {
70
181
  query: z.string().min(1).describe('Search query (keyword or natural language)'),
71
182
  limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
@@ -100,7 +211,11 @@ async function main() {
100
211
  if (params.mode) recallOpts.mode = params.mode;
101
212
 
102
213
  const results = await aquifer.recall(params.query, recallOpts);
103
- const text = formatResults(results, params.query, params.explain);
214
+ const text = [
215
+ sessionRecallLaneHeader(aquifer.getConfig ? aquifer.getConfig() : {}),
216
+ '',
217
+ formatResults(results, params.query, params.explain),
218
+ ].join('\n');
104
219
  return { content: [{ type: 'text', text }] };
105
220
  } catch (err) {
106
221
  return {
@@ -146,7 +261,11 @@ async function main() {
146
261
  if (params.allowUnsafeDebug) recallOpts.allowUnsafeDebug = true;
147
262
 
148
263
  const results = await aquifer.evidenceRecall(params.query, recallOpts);
149
- const text = formatResults(results, params.query, params.explain);
264
+ const text = [
265
+ 'Serving lane: explicit legacy/evidence recall',
266
+ '',
267
+ formatResults(results, params.query, params.explain),
268
+ ].join('\n');
150
269
  return { content: [{ type: 'text', text }] };
151
270
  } catch (err) {
152
271
  return {
@@ -255,13 +374,16 @@ async function main() {
255
374
 
256
375
  server.tool(
257
376
  'memory_stats',
258
- 'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
377
+ 'Return storage statistics for the Aquifer memory store, serving mode, current-memory record coverage, and session date range.',
259
378
  {},
260
379
  async () => {
261
380
  try {
262
381
  const aquifer = getAquifer();
263
382
  const stats = await aquifer.getStats();
264
383
  const lines = [
384
+ `Backend: ${stats.backendKind || 'unknown'} (${stats.backendProfile || 'unknown'})`,
385
+ `Serving mode: ${stats.serving?.mode || 'legacy'}`,
386
+ `Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`,
265
387
  `Sessions: ${stats.sessionTotal} total`,
266
388
  ];
267
389
  for (const [status, count] of Object.entries(stats.sessions)) {
@@ -270,7 +392,23 @@ async function main() {
270
392
  lines.push(`Summaries: ${stats.summaries}`);
271
393
  lines.push(`Turn embeddings: ${stats.turnEmbeddings}`);
272
394
  lines.push(`Entities: ${stats.entities}`);
395
+ if (stats.memoryRecords) {
396
+ lines.push(`Memory records: ${stats.memoryRecords.total} total (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
397
+ if (stats.memoryRecords.latest) lines.push(`Memory record range: ${new Date(stats.memoryRecords.earliest).toISOString().slice(0, 10)} → ${new Date(stats.memoryRecords.latest).toISOString().slice(0, 10)}`);
398
+ }
399
+ if (stats.sessionFinalizations?.available) {
400
+ const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
401
+ .map(([status, count]) => `${status}: ${count}`)
402
+ .join(', ') || 'none';
403
+ lines.push(`Session finalizations: ${stats.sessionFinalizations.total} total (${statusText})`);
404
+ if (stats.sessionFinalizations.latestFinalizedAt) {
405
+ lines.push(`Latest finalization: ${new Date(stats.sessionFinalizations.latestFinalizedAt).toISOString().slice(0, 10)}`);
406
+ }
407
+ }
273
408
  if (stats.earliest) lines.push(`Date range: ${new Date(stats.earliest).toISOString().slice(0, 10)} → ${new Date(stats.latest).toISOString().slice(0, 10)}`);
409
+ if ((stats.serving?.mode || 'legacy') !== 'curated') {
410
+ lines.push('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
411
+ }
274
412
  return { content: [{ type: 'text', text: lines.join('\n') }] };
275
413
  } catch (err) {
276
414
  return {