@shadowforge0/aquifer-memory 1.6.0 → 1.8.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/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- package/scripts/codex-recovery.js +246 -1
package/consumers/codex.js
CHANGED
|
@@ -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;
|
|
@@ -323,11 +333,10 @@ function matchesRecoveryProvenance(metadata = {}, opts = {}, defaults = {}) {
|
|
|
323
333
|
repoPath: opts.repoPath || null,
|
|
324
334
|
};
|
|
325
335
|
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
326
|
-
if (!expectedValue
|
|
336
|
+
if (!expectedValue) continue;
|
|
337
|
+
if (!metadata[key]) return false;
|
|
327
338
|
if (String(metadata[key]) !== String(expectedValue)) return false;
|
|
328
339
|
}
|
|
329
|
-
if (metadata.source && metadata.source !== expected.source) return false;
|
|
330
|
-
if (metadata.agentId && metadata.agentId !== expected.agentId) return false;
|
|
331
340
|
return true;
|
|
332
341
|
}
|
|
333
342
|
|
|
@@ -770,11 +779,13 @@ async function afterburnCandidate(aquifer, candidate, opts = {}) {
|
|
|
770
779
|
|
|
771
780
|
let resolvedSummary = summaryInput;
|
|
772
781
|
if (!hasFinalizationSummary(resolvedSummary) && typeof summaryProvider === 'function') {
|
|
782
|
+
const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
|
|
773
783
|
resolvedSummary = await summaryProvider(view.messages, {
|
|
774
784
|
aquifer,
|
|
775
785
|
candidate: recoveryCandidate,
|
|
776
786
|
existing,
|
|
777
787
|
view,
|
|
788
|
+
currentMemory,
|
|
778
789
|
agentId,
|
|
779
790
|
source,
|
|
780
791
|
sessionKey,
|
|
@@ -873,9 +884,18 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
|
|
|
873
884
|
const {
|
|
874
885
|
agentId = 'main',
|
|
875
886
|
source = 'codex',
|
|
887
|
+
sessionKey = null,
|
|
876
888
|
maxRecoveryCandidates = 3,
|
|
877
889
|
includeJsonlPreviews = false,
|
|
878
890
|
} = opts;
|
|
891
|
+
const provenance = {
|
|
892
|
+
source,
|
|
893
|
+
agentId,
|
|
894
|
+
sessionKey,
|
|
895
|
+
workspace: opts.workspace || opts.workspacePath || null,
|
|
896
|
+
project: opts.project || opts.projectKey || null,
|
|
897
|
+
repoPath: opts.repoPath || null,
|
|
898
|
+
};
|
|
879
899
|
if (!Number.isFinite(maxRecoveryCandidates) || maxRecoveryCandidates <= 0) return [];
|
|
880
900
|
ensureDirs(paths.importedDir, paths.afterburnedDir, paths.claimDir, paths.decisionDir);
|
|
881
901
|
|
|
@@ -961,7 +981,7 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
|
|
|
961
981
|
transcriptHash: null,
|
|
962
982
|
source,
|
|
963
983
|
agentId,
|
|
964
|
-
sessionKey
|
|
984
|
+
sessionKey,
|
|
965
985
|
userCount: null,
|
|
966
986
|
messageCount: null,
|
|
967
987
|
finalizationStatus: null,
|
|
@@ -972,8 +992,7 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
|
|
|
972
992
|
fileSessionId: safeFileSessionId,
|
|
973
993
|
size: entry.stat.size,
|
|
974
994
|
mtimeMs: entry.stat.mtimeMs,
|
|
975
|
-
|
|
976
|
-
agentId,
|
|
995
|
+
...provenance,
|
|
977
996
|
},
|
|
978
997
|
};
|
|
979
998
|
const localDecision = readRecoveryDecision(paths, candidatePreview);
|
|
@@ -1093,6 +1112,92 @@ function approxPromptTokens(text) {
|
|
|
1093
1112
|
return Math.ceil(String(text || '').length / 3);
|
|
1094
1113
|
}
|
|
1095
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
|
+
|
|
1096
1201
|
function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
1097
1202
|
const filePath = candidate.filePath || candidate.metadata?.filePath;
|
|
1098
1203
|
if (!filePath) {
|
|
@@ -1106,6 +1211,7 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1106
1211
|
const maxMessages = opts.maxRecoveryMessages ?? DEFAULT_RECOVERY_MAX_MESSAGES;
|
|
1107
1212
|
const maxChars = opts.maxRecoveryChars ?? DEFAULT_RECOVERY_MAX_CHARS;
|
|
1108
1213
|
const maxPromptTokens = opts.maxRecoveryPromptTokens ?? DEFAULT_RECOVERY_MAX_PROMPT_TOKENS;
|
|
1214
|
+
const allowTail = opts.tailOnMaxBudget === true || opts.tailOnBudget === true || opts.tail === true;
|
|
1109
1215
|
|
|
1110
1216
|
let stat;
|
|
1111
1217
|
try {
|
|
@@ -1113,7 +1219,7 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1113
1219
|
} catch {
|
|
1114
1220
|
return { status: 'not_found', sessionId: candidate.sessionId || null, filePath };
|
|
1115
1221
|
}
|
|
1116
|
-
if (Number.isFinite(maxBytes) && maxBytes > 0 && stat.size > maxBytes) {
|
|
1222
|
+
if (!allowTail && Number.isFinite(maxBytes) && maxBytes > 0 && stat.size > maxBytes) {
|
|
1117
1223
|
return { status: 'deferred', reason: 'max_bytes', sessionId: candidate.sessionId || null, filePath, size: stat.size };
|
|
1118
1224
|
}
|
|
1119
1225
|
|
|
@@ -1145,7 +1251,13 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1145
1251
|
safetyGate: safety.meta,
|
|
1146
1252
|
};
|
|
1147
1253
|
}
|
|
1148
|
-
|
|
1254
|
+
const bounded = buildBoundedRecoveryTranscript(safeMessages, {
|
|
1255
|
+
allowTail,
|
|
1256
|
+
maxMessages,
|
|
1257
|
+
maxChars,
|
|
1258
|
+
maxPromptTokens,
|
|
1259
|
+
});
|
|
1260
|
+
if (bounded.status === 'deferred' && bounded.reason === 'max_messages') {
|
|
1149
1261
|
return {
|
|
1150
1262
|
status: 'deferred',
|
|
1151
1263
|
reason: 'max_messages',
|
|
@@ -1157,20 +1269,16 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1157
1269
|
safetyGate: safety.meta,
|
|
1158
1270
|
};
|
|
1159
1271
|
}
|
|
1160
|
-
|
|
1161
|
-
const text = formatRecoveryTranscript(safeMessages);
|
|
1162
|
-
const promptTokens = approxPromptTokens(text);
|
|
1163
|
-
if ((Number.isFinite(maxChars) && maxChars > 0 && text.length > maxChars)
|
|
1164
|
-
|| (Number.isFinite(maxPromptTokens) && maxPromptTokens > 0 && promptTokens > maxPromptTokens)) {
|
|
1272
|
+
if (bounded.status === 'deferred') {
|
|
1165
1273
|
return {
|
|
1166
1274
|
status: 'deferred',
|
|
1167
|
-
reason:
|
|
1275
|
+
reason: bounded.reason,
|
|
1168
1276
|
sessionId: parsed.sessionId,
|
|
1169
1277
|
fileSessionId: parsed.fileSessionId,
|
|
1170
1278
|
filePath,
|
|
1171
1279
|
transcriptHash,
|
|
1172
|
-
charCount:
|
|
1173
|
-
approxPromptTokens:
|
|
1280
|
+
charCount: bounded.charCount,
|
|
1281
|
+
approxPromptTokens: bounded.approxPromptTokens,
|
|
1174
1282
|
safetyGate: safety.meta,
|
|
1175
1283
|
};
|
|
1176
1284
|
}
|
|
@@ -1181,16 +1289,23 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1181
1289
|
fileSessionId: parsed.fileSessionId,
|
|
1182
1290
|
filePath,
|
|
1183
1291
|
transcriptHash,
|
|
1184
|
-
messages:
|
|
1185
|
-
text,
|
|
1186
|
-
charCount:
|
|
1187
|
-
approxPromptTokens:
|
|
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,
|
|
1188
1301
|
safetyGate: safety.meta,
|
|
1189
1302
|
counts: {
|
|
1190
1303
|
messageCount: parsed.normalized.messages.length,
|
|
1191
1304
|
safeMessageCount: safeMessages.length,
|
|
1192
1305
|
userCount: parsed.normalized.userCount,
|
|
1193
1306
|
assistantCount: parsed.normalized.assistantCount,
|
|
1307
|
+
fullCharCount: bounded.fullCharCount,
|
|
1308
|
+
fullApproxPromptTokens: bounded.fullApproxPromptTokens,
|
|
1194
1309
|
},
|
|
1195
1310
|
metadata: {
|
|
1196
1311
|
model: parsed.normalized.model,
|
|
@@ -1203,12 +1318,20 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
|
|
|
1203
1318
|
};
|
|
1204
1319
|
}
|
|
1205
1320
|
|
|
1321
|
+
async function prepareActiveSessionCheckpoint(aquifer, opts = {}) {
|
|
1322
|
+
return prepareActiveSessionCheckpointImpl(aquifer, opts, {
|
|
1323
|
+
materializeRecoveryTranscriptView,
|
|
1324
|
+
resolveCurrentMemoryForFinalization,
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1206
1328
|
function buildFinalizationPrompt(view = {}, opts = {}) {
|
|
1207
1329
|
if (!view || view.status !== 'ok') {
|
|
1208
1330
|
throw new Error('buildFinalizationPrompt requires an ok transcript view');
|
|
1209
1331
|
}
|
|
1210
1332
|
const maxFacts = opts.maxFacts || 8;
|
|
1211
|
-
|
|
1333
|
+
const includeCurrentMemory = opts.includeCurrentMemory !== false;
|
|
1334
|
+
const lines = [
|
|
1212
1335
|
'You are finalizing an Aquifer memory session for Codex.',
|
|
1213
1336
|
'Use only the sanitized transcript below. Do not infer from hidden tool output or injected context.',
|
|
1214
1337
|
'Return compact JSON with this shape:',
|
|
@@ -1222,7 +1345,16 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
|
|
|
1222
1345
|
'<sanitized_transcript>',
|
|
1223
1346
|
view.text || '',
|
|
1224
1347
|
'</sanitized_transcript>',
|
|
1225
|
-
]
|
|
1348
|
+
];
|
|
1349
|
+
if (includeCurrentMemory) {
|
|
1350
|
+
lines.splice(
|
|
1351
|
+
2,
|
|
1352
|
+
0,
|
|
1353
|
+
'Use current_memory as the already-committed current state. Reconcile the transcript against it: keep valid state, supersede stale state, and mark uncertain items explicitly.',
|
|
1354
|
+
);
|
|
1355
|
+
lines.splice(10, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
|
|
1356
|
+
}
|
|
1357
|
+
return lines.join('\n');
|
|
1226
1358
|
}
|
|
1227
1359
|
|
|
1228
1360
|
function normalizeFinalizationSummary(summary = {}) {
|
|
@@ -1235,6 +1367,30 @@ function normalizeFinalizationSummary(summary = {}) {
|
|
|
1235
1367
|
return { summaryText, structuredSummary };
|
|
1236
1368
|
}
|
|
1237
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
|
+
|
|
1238
1394
|
async function ensureCommittedForFinalization(aquifer, view = {}, opts = {}) {
|
|
1239
1395
|
const {
|
|
1240
1396
|
agentId = 'main',
|
|
@@ -1304,6 +1460,15 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
|
|
|
1304
1460
|
trigger: mode,
|
|
1305
1461
|
...(opts.metadata || {}),
|
|
1306
1462
|
};
|
|
1463
|
+
let resolvedCurrentMemory = opts.currentMemory;
|
|
1464
|
+
if (!metadata.currentMemory) {
|
|
1465
|
+
resolvedCurrentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
|
|
1466
|
+
if (resolvedCurrentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(resolvedCurrentMemory, opts);
|
|
1467
|
+
}
|
|
1468
|
+
const finalizationScope = resolveFinalizationScope({
|
|
1469
|
+
...opts,
|
|
1470
|
+
currentMemory: resolvedCurrentMemory,
|
|
1471
|
+
});
|
|
1307
1472
|
const result = await finalizeSession({
|
|
1308
1473
|
sessionId: view.sessionId,
|
|
1309
1474
|
agentId,
|
|
@@ -1320,11 +1485,15 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
|
|
|
1320
1485
|
startedAt: view.metadata?.startedAt || null,
|
|
1321
1486
|
endedAt: view.metadata?.lastMessageAt || null,
|
|
1322
1487
|
embedding: opts.embedding || null,
|
|
1323
|
-
scopeKind:
|
|
1324
|
-
scopeKey:
|
|
1488
|
+
scopeKind: finalizationScope.scopeKind,
|
|
1489
|
+
scopeKey: finalizationScope.scopeKey,
|
|
1325
1490
|
contextKey: opts.contextKey || null,
|
|
1326
1491
|
topicKey: opts.topicKey || null,
|
|
1327
1492
|
authority: opts.authority || 'verified_summary',
|
|
1493
|
+
candidates: Array.isArray(opts.candidates) ? opts.candidates : undefined,
|
|
1494
|
+
candidatePayload: opts.candidatePayload || null,
|
|
1495
|
+
candidateEnvelope: opts.candidateEnvelope || null,
|
|
1496
|
+
coverage: opts.coverage || null,
|
|
1328
1497
|
metadata,
|
|
1329
1498
|
});
|
|
1330
1499
|
const humanReviewText = result.humanReviewText || '';
|
|
@@ -1432,11 +1601,13 @@ async function prepareSessionStartRecovery(aquifer, opts = {}) {
|
|
|
1432
1601
|
}
|
|
1433
1602
|
return { status: 'skipped_short', candidate: skippedCandidate, view, userCount };
|
|
1434
1603
|
}
|
|
1604
|
+
const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, recoveryOpts);
|
|
1435
1605
|
return {
|
|
1436
1606
|
status: 'needs_agent_summary',
|
|
1437
1607
|
candidate,
|
|
1438
1608
|
view,
|
|
1439
|
-
|
|
1609
|
+
currentMemory,
|
|
1610
|
+
prompt: buildFinalizationPrompt(view, { ...recoveryOpts, currentMemory }),
|
|
1440
1611
|
};
|
|
1441
1612
|
}
|
|
1442
1613
|
|
|
@@ -1470,6 +1641,13 @@ async function finalizeCodexSession(aquifer, input = {}, opts = {}) {
|
|
|
1470
1641
|
scopeKey: input.scopeKey || opts.scopeKey || null,
|
|
1471
1642
|
contextKey: input.contextKey || opts.contextKey || null,
|
|
1472
1643
|
topicKey: input.topicKey || opts.topicKey || null,
|
|
1644
|
+
activeScopeKey: input.activeScopeKey || opts.activeScopeKey || input.scopeKey || opts.scopeKey || null,
|
|
1645
|
+
activeScopePath: input.activeScopePath || opts.activeScopePath || null,
|
|
1646
|
+
currentMemory: input.currentMemory !== undefined ? input.currentMemory : opts.currentMemory,
|
|
1647
|
+
currentMemoryLimit: input.currentMemoryLimit || opts.currentMemoryLimit || null,
|
|
1648
|
+
includeCurrentMemory: input.includeCurrentMemory !== undefined ? input.includeCurrentMemory : opts.includeCurrentMemory,
|
|
1649
|
+
candidateEnvelope: input.candidateEnvelope || opts.candidateEnvelope || null,
|
|
1650
|
+
coverage: input.coverage || opts.coverage || null,
|
|
1473
1651
|
});
|
|
1474
1652
|
}
|
|
1475
1653
|
|
|
@@ -1530,6 +1708,9 @@ module.exports = {
|
|
|
1530
1708
|
findDbEligibleRecoveryCandidates,
|
|
1531
1709
|
materializeRecoveryTranscriptView,
|
|
1532
1710
|
buildFinalizationPrompt,
|
|
1711
|
+
buildActiveSessionCheckpointInput,
|
|
1712
|
+
buildActiveSessionCheckpointPrompt,
|
|
1713
|
+
prepareActiveSessionCheckpoint,
|
|
1533
1714
|
prepareSessionStartRecovery,
|
|
1534
1715
|
recordRecoveryDecision,
|
|
1535
1716
|
finalizeTranscriptView,
|
|
@@ -1546,4 +1727,7 @@ module.exports = {
|
|
|
1546
1727
|
markerPath,
|
|
1547
1728
|
hashNormalizedTranscript,
|
|
1548
1729
|
readMarkerMetadataFromContent,
|
|
1730
|
+
formatCurrentMemoryPromptBlock,
|
|
1731
|
+
compactCurrentMemorySnapshot,
|
|
1732
|
+
resolveCurrentMemoryForFinalization,
|
|
1549
1733
|
};
|
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:
|
|
11
|
-
*
|
|
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
|
-
'
|
|
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 =
|
|
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 =
|
|
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
|
|
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 {
|