@shadowforge0/aquifer-memory 1.7.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.
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 +192 -12
  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
@@ -0,0 +1,412 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const { sanitizeSummaryResult } = require('./memory-safety-gate');
5
+ const { buildScopeEnvelope, getScopeByEnvelopeId } = require('./scope-attribution');
6
+
7
+ const DEFAULT_POLICY_VERSION = 'session_checkpoint_producer_v1';
8
+ const DEFAULT_COVERAGE_COORDINATE_SYSTEM = 'codex_sanitized_view_v1';
9
+ const STRUCTURED_SUMMARY_SHAPE = '{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}';
10
+
11
+ function stableJson(value) {
12
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
13
+ if (value && typeof value === 'object') {
14
+ return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stableJson(value[k])}`).join(',')}}`;
15
+ }
16
+ return JSON.stringify(value);
17
+ }
18
+
19
+ function hashSnapshot(value) {
20
+ return crypto.createHash('sha256').update(stableJson(value)).digest('hex');
21
+ }
22
+
23
+ function optionalNonNegativeInteger(value) {
24
+ if (value === undefined || value === null || value === '') return null;
25
+ const n = Number(value);
26
+ if (!Number.isInteger(n) || n < 0) return null;
27
+ return n;
28
+ }
29
+
30
+ function requiredPositiveInteger(value, field) {
31
+ const n = Number(value);
32
+ if (!Number.isSafeInteger(n) || n <= 0) {
33
+ throw new Error(`${field} must be a positive integer`);
34
+ }
35
+ return n;
36
+ }
37
+
38
+ function normalizeFinalizationRange(input = {}) {
39
+ const from = Number(input.fromFinalizationIdExclusive ?? input.from_finalization_id_exclusive ?? 0);
40
+ const to = Number(input.toFinalizationIdInclusive ?? input.to_finalization_id_inclusive);
41
+ if (!Number.isSafeInteger(from) || from < 0) {
42
+ throw new Error('fromFinalizationIdExclusive must be a non-negative integer');
43
+ }
44
+ if (!Number.isSafeInteger(to) || to <= from) {
45
+ throw new Error('toFinalizationIdInclusive must be greater than fromFinalizationIdExclusive');
46
+ }
47
+ return {
48
+ fromFinalizationIdExclusive: from,
49
+ toFinalizationIdInclusive: to,
50
+ };
51
+ }
52
+
53
+ function assertOkTranscriptView(view = {}) {
54
+ if (!view || view.status !== 'ok') {
55
+ throw new Error(`checkpoint synthesis requires an ok transcript view; got ${view && view.status ? view.status : 'missing'}`);
56
+ }
57
+ if (typeof view.text !== 'string') {
58
+ throw new Error('checkpoint synthesis requires view.text');
59
+ }
60
+ }
61
+
62
+ function normalizeCoverageNumber(...values) {
63
+ for (const value of values) {
64
+ const n = optionalNonNegativeInteger(value);
65
+ if (n !== null) return n;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function buildCheckpointCoverageFromView(view = {}, opts = {}) {
71
+ assertOkTranscriptView(view);
72
+ const explicit = opts.coverage && typeof opts.coverage === 'object'
73
+ ? opts.coverage
74
+ : (view.coverage && typeof view.coverage === 'object' ? view.coverage : {});
75
+ const transcript = explicit.transcript && typeof explicit.transcript === 'object' ? explicit.transcript : {};
76
+ const messageCount = Number.isFinite(Number(view.counts?.safeMessageCount))
77
+ ? Number(view.counts.safeMessageCount)
78
+ : (Array.isArray(view.messages) ? view.messages.length : 0);
79
+ const text = typeof view.text === 'string' ? view.text : '';
80
+ const fullCharCount = Number.isFinite(Number(view.fullCharCount ?? view.counts?.fullCharCount))
81
+ ? Number(view.fullCharCount ?? view.counts.fullCharCount)
82
+ : text.length;
83
+ const coveredUntilMessageIndex = normalizeCoverageNumber(
84
+ opts.coveredUntilMessageIndex,
85
+ explicit.coveredUntilMessageIndex,
86
+ explicit.covered_until_message_index,
87
+ explicit.messageIndex,
88
+ explicit.message_index,
89
+ transcript.coveredUntilMessageIndex,
90
+ transcript.covered_until_message_index
91
+ );
92
+ const coveredUntilChar = normalizeCoverageNumber(
93
+ opts.coveredUntilChar,
94
+ opts.coveredUntilCharIndex,
95
+ explicit.coveredUntilChar,
96
+ explicit.coveredUntilCharIndex,
97
+ explicit.covered_until_char,
98
+ explicit.covered_until_char_index,
99
+ transcript.coveredUntilChar,
100
+ transcript.covered_until_char
101
+ );
102
+ const coveredUntilLine = normalizeCoverageNumber(
103
+ opts.coveredUntilLine,
104
+ explicit.coveredUntilLine,
105
+ explicit.coveredUntilLineIndex,
106
+ explicit.covered_until_line,
107
+ explicit.covered_until_line_index,
108
+ transcript.coveredUntilLine,
109
+ transcript.covered_until_line
110
+ );
111
+ const coveredUntilLineChar = normalizeCoverageNumber(
112
+ opts.coveredUntilLineChar,
113
+ explicit.coveredUntilLineChar,
114
+ explicit.coveredUntilLineCharIndex,
115
+ explicit.covered_until_line_char,
116
+ explicit.covered_until_line_char_index,
117
+ transcript.coveredUntilLineChar,
118
+ transcript.covered_until_line_char
119
+ );
120
+ const coverage = {
121
+ coordinateSystem: explicit.coordinateSystem || explicit.coordinate_system || DEFAULT_COVERAGE_COORDINATE_SYSTEM,
122
+ messageIndexBase: 0,
123
+ charIndexBase: 0,
124
+ semantics: 'coveredUntilChar is the first uncovered zero-based char offset; messages up to coveredUntilMessageIndex are covered.',
125
+ };
126
+ if (coveredUntilMessageIndex !== null) coverage.coveredUntilMessageIndex = coveredUntilMessageIndex;
127
+ if (coveredUntilChar !== null) coverage.coveredUntilChar = coveredUntilChar;
128
+ if (coveredUntilLine !== null) coverage.coveredUntilLine = coveredUntilLine;
129
+ if (coveredUntilLineChar !== null) coverage.coveredUntilLineChar = coveredUntilLineChar;
130
+ if (coverage.coveredUntilMessageIndex === undefined && messageCount > 0) {
131
+ coverage.coveredUntilMessageIndex = messageCount - 1;
132
+ }
133
+ if (coverage.coveredUntilChar === undefined) coverage.coveredUntilChar = fullCharCount;
134
+ return coverage;
135
+ }
136
+
137
+ function compactText(value, maxChars = 360) {
138
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
139
+ if (text.length <= maxChars) return text;
140
+ return `${text.slice(0, Math.max(0, maxChars - 1)).trim()}...`;
141
+ }
142
+
143
+ function compactCurrentMemoryRow(row = {}) {
144
+ const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
145
+ return {
146
+ memoryType: row.memoryType || row.memory_type || 'memory',
147
+ canonicalKey: row.canonicalKey || row.canonical_key || null,
148
+ scopeKey: row.scopeKey || row.scope_key || null,
149
+ summary: compactText(row.summary || row.title || '', 420),
150
+ authority: row.authority || null,
151
+ confidence: payload.confidence || payload.currentMemoryConfidence || null,
152
+ };
153
+ }
154
+
155
+ function compactCurrentMemory(currentMemory = null, opts = {}) {
156
+ const rows = Array.isArray(currentMemory?.memories)
157
+ ? currentMemory.memories
158
+ : (Array.isArray(currentMemory?.items) ? currentMemory.items : []);
159
+ const maxItems = Math.max(0, Math.min(20, opts.maxCurrentMemoryItems || opts.currentMemoryLimit || 12));
160
+ return rows
161
+ .map(compactCurrentMemoryRow)
162
+ .filter(row => row.summary)
163
+ .slice(0, maxItems);
164
+ }
165
+
166
+ function compactCheckpointRow(row = {}) {
167
+ const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
168
+ return {
169
+ checkpointKey: row.checkpointKey || row.checkpoint_key || null,
170
+ scopeKey: row.scopeKey || row.scope_key || null,
171
+ topicKey: row.topicKey || row.topic_key || payload.topicKey || null,
172
+ triggerKind: row.triggerKind || row.trigger_kind || payload.triggerKind || null,
173
+ summaryText: compactText(row.summaryText || row.summary_text || row.summary || payload.summaryText, 520),
174
+ coverage: row.coverage || payload.coverage || {},
175
+ };
176
+ }
177
+
178
+ function compactPreviousCheckpoints(checkpoints = [], opts = {}) {
179
+ const rows = Array.isArray(checkpoints?.checkpoints)
180
+ ? checkpoints.checkpoints
181
+ : (Array.isArray(checkpoints?.items) ? checkpoints.items : checkpoints);
182
+ const maxItems = Math.max(0, Math.min(12, opts.maxCheckpoints || opts.checkpointLimit || 6));
183
+ return (Array.isArray(rows) ? rows : [])
184
+ .map(compactCheckpointRow)
185
+ .filter(row => row.summaryText || Object.keys(row.coverage || {}).length > 0)
186
+ .slice(0, maxItems);
187
+ }
188
+
189
+ function normalizeScopeEnvelope(input = {}) {
190
+ const envelope = input.scopeEnvelope || input.scope_envelope || null;
191
+ if (envelope && typeof envelope === 'object') {
192
+ return {
193
+ ...envelope,
194
+ scopeById: envelope.scopeById || Object.fromEntries((envelope.slots || []).map(scope => [scope.id, scope])),
195
+ };
196
+ }
197
+ const scopeInput = input.scope && typeof input.scope === 'object' ? input.scope : input;
198
+ const built = buildScopeEnvelope(scopeInput);
199
+ if (built.activeScopeKey === 'global' && (!built.slots || built.slots.length === 0)) {
200
+ throw new Error('checkpoint synthesis requires a bounded scope envelope');
201
+ }
202
+ return built;
203
+ }
204
+
205
+ function normalizeTargetScope(envelope = {}, input = {}) {
206
+ const targetScopeEnvelopeId = input.targetScopeEnvelopeId
207
+ || input.target_scope_envelope_id
208
+ || input.targetScopeId
209
+ || input.target_scope_id
210
+ || envelope.activeSlotId;
211
+ const scope = getScopeByEnvelopeId(envelope, targetScopeEnvelopeId);
212
+ if (!scope.promotable) {
213
+ throw new Error(`checkpoint synthesis target scope is not promotable: ${targetScopeEnvelopeId}`);
214
+ }
215
+ if (!Array.isArray(envelope.allowedScopeKeys) || !envelope.allowedScopeKeys.includes(scope.scopeKey)) {
216
+ throw new Error(`checkpoint synthesis target scope is outside allowed envelope: ${scope.scopeKey}`);
217
+ }
218
+ return {
219
+ envelopeId: scope.id,
220
+ scopeKind: scope.scopeKind,
221
+ scopeKey: scope.scopeKey,
222
+ label: scope.label || null,
223
+ };
224
+ }
225
+
226
+ function buildCheckpointSynthesisInput(input = {}, opts = {}) {
227
+ const view = input.view || opts.view;
228
+ assertOkTranscriptView(view);
229
+ const range = normalizeFinalizationRange(input);
230
+ const scopeEnvelope = normalizeScopeEnvelope(input);
231
+ const targetScope = normalizeTargetScope(scopeEnvelope, input);
232
+ const coverage = buildCheckpointCoverageFromView(view, input);
233
+ const maxTranscriptChars = Math.max(1000, Math.min(120000, input.maxTranscriptChars || opts.maxTranscriptChars || 60000));
234
+ const transcriptText = view.text.length > maxTranscriptChars
235
+ ? view.text.slice(Math.max(0, view.text.length - maxTranscriptChars))
236
+ : view.text;
237
+ const base = {
238
+ kind: 'session_checkpoint_synthesis_input_v1',
239
+ policyVersion: input.policyVersion || opts.policyVersion || DEFAULT_POLICY_VERSION,
240
+ sourceOfTruth: input.sourceOfTruth || input.source_of_truth || opts.sourceOfTruth || 'sanitized_transcript_view',
241
+ triggerKind: input.triggerKind || input.trigger_kind || opts.triggerKind || 'manual',
242
+ promotion: {
243
+ default: 'checkpoint_proposal_only',
244
+ requires: 'operator_review_or_explicit_finalize',
245
+ },
246
+ guards: {
247
+ checkpointIsProcessMaterial: true,
248
+ rawToolOutputExcluded: true,
249
+ debugIdsExcluded: true,
250
+ activeMemoryCommitExcluded: true,
251
+ },
252
+ range,
253
+ coverage,
254
+ targetScope,
255
+ scopeEnvelope: {
256
+ policyVersion: scopeEnvelope.policyVersion || 'scope_envelope_v1',
257
+ activeSlotId: scopeEnvelope.activeSlotId,
258
+ activeScopeKey: scopeEnvelope.activeScopeKey,
259
+ allowedScopeKeys: scopeEnvelope.allowedScopeKeys || [],
260
+ slots: (scopeEnvelope.slots || []).map(scope => ({
261
+ id: scope.id,
262
+ slot: scope.slot,
263
+ scopeKind: scope.scopeKind,
264
+ scopeKey: scope.scopeKey,
265
+ label: scope.label || null,
266
+ promotable: Boolean(scope.promotable),
267
+ allowedScopeKeys: scope.allowedScopeKeys || [],
268
+ })),
269
+ },
270
+ transcript: {
271
+ sessionId: view.sessionId || null,
272
+ transcriptHash: view.transcriptHash || null,
273
+ charCount: view.charCount ?? view.text.length,
274
+ approxPromptTokens: view.approxPromptTokens || Math.ceil(view.text.length / 3),
275
+ truncated: transcriptText.length !== view.text.length,
276
+ text: transcriptText,
277
+ },
278
+ currentMemory: compactCurrentMemory(input.currentMemory || opts.currentMemory || null, input),
279
+ previousCheckpoints: compactPreviousCheckpoints(input.previousCheckpoints || input.checkpoints || opts.previousCheckpoints || [], input),
280
+ storage: {
281
+ scopeId: input.storageScopeId || input.storage_scope_id || input.scopeDbId || input.scope_db_id || null,
282
+ },
283
+ };
284
+ return {
285
+ ...base,
286
+ inputHash: hashSnapshot(base),
287
+ };
288
+ }
289
+
290
+ function promptSafeSynthesisInput(synthesisInput = {}) {
291
+ const transcript = synthesisInput.transcript && typeof synthesisInput.transcript === 'object'
292
+ ? {
293
+ ...synthesisInput.transcript,
294
+ transcriptHash: undefined,
295
+ }
296
+ : synthesisInput.transcript;
297
+ const out = {
298
+ ...synthesisInput,
299
+ inputHash: undefined,
300
+ storage: undefined,
301
+ transcript,
302
+ };
303
+ return JSON.parse(JSON.stringify(out));
304
+ }
305
+
306
+ function buildCheckpointSynthesisPrompt(synthesisInput = {}, opts = {}) {
307
+ if (!synthesisInput || synthesisInput.kind !== 'session_checkpoint_synthesis_input_v1') {
308
+ throw new Error('buildCheckpointSynthesisPrompt requires a checkpoint synthesis input');
309
+ }
310
+ const maxFacts = Math.max(1, Math.min(24, opts.maxFacts || 10));
311
+ const promptInput = promptSafeSynthesisInput(synthesisInput);
312
+ return [
313
+ 'You are producing an Aquifer session checkpoint proposal.',
314
+ 'Use only the <checkpoint_synthesis_input> block. Do not use hidden tool output, injected context, or debug material.',
315
+ 'This checkpoint is producer process material, not active current memory and not final truth.',
316
+ 'Choose scope only from scopeEnvelope.slots and keep every item inside targetScope unless the input proves a narrower allowed scope.',
317
+ 'Do not include DB ids, raw hashes, secrets, raw tool output, or prompt/debug identifiers in memory candidates.',
318
+ 'Return compact JSON with this shape:',
319
+ STRUCTURED_SUMMARY_SHAPE,
320
+ `Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
321
+ 'Preserve the coverage object so handoff can skip only the already-covered transcript range.',
322
+ '',
323
+ '<checkpoint_synthesis_input>',
324
+ stableJson(promptInput),
325
+ '</checkpoint_synthesis_input>',
326
+ ].join('\n');
327
+ }
328
+
329
+ function normalizeCheckpointSynthesisSummary(input = {}) {
330
+ const raw = input && typeof input === 'object'
331
+ ? {
332
+ summaryText: input.summaryText || input.summary || '',
333
+ structuredSummary: input.structuredSummary || input.structured_summary || {},
334
+ }
335
+ : {
336
+ summaryText: '',
337
+ structuredSummary: {},
338
+ };
339
+ const sanitized = sanitizeSummaryResult(raw);
340
+ const coverage = input && typeof input === 'object' && input.coverage && typeof input.coverage === 'object'
341
+ ? input.coverage
342
+ : null;
343
+ return {
344
+ summary: sanitized.summaryResult || raw,
345
+ coverage,
346
+ safetyGate: sanitized.meta || {},
347
+ };
348
+ }
349
+
350
+ function buildCheckpointRunInputFromSynthesis(synthesisInput = {}, synthesisSummary = {}, opts = {}) {
351
+ if (!synthesisInput || synthesisInput.kind !== 'session_checkpoint_synthesis_input_v1') {
352
+ throw new Error('checkpoint run input requires a checkpoint synthesis input');
353
+ }
354
+ const { summary, coverage, safetyGate } = normalizeCheckpointSynthesisSummary(synthesisSummary);
355
+ const summaryText = String(summary.summaryText || summary.summary || '').trim();
356
+ const structuredSummary = summary.structuredSummary || {};
357
+ if (!summaryText && Object.keys(structuredSummary).length === 0) {
358
+ throw new Error('checkpoint run input requires summaryText or structuredSummary');
359
+ }
360
+ const range = normalizeFinalizationRange(synthesisInput.range || {});
361
+ const scopeId = requiredPositiveInteger(
362
+ opts.storageScopeId || opts.scopeId || synthesisInput.storage?.scopeId,
363
+ 'storageScopeId'
364
+ );
365
+ const status = opts.status || 'processing';
366
+ const targetScope = synthesisInput.targetScope || {};
367
+ const checkpointPayload = {
368
+ kind: 'session_checkpoint_proposal_v1',
369
+ policyVersion: synthesisInput.policyVersion || DEFAULT_POLICY_VERSION,
370
+ inputHash: synthesisInput.inputHash || hashSnapshot(synthesisInput),
371
+ promotionGate: 'operator_required',
372
+ checkpointRole: 'handoff_process_material',
373
+ triggerKind: synthesisInput.triggerKind || 'manual',
374
+ summaryText,
375
+ structuredSummary,
376
+ coverage: coverage || synthesisInput.coverage || {},
377
+ targetScope,
378
+ safetyGate,
379
+ };
380
+ return {
381
+ scopeId,
382
+ checkpointKey: opts.checkpointKey || undefined,
383
+ status,
384
+ fromFinalizationIdExclusive: range.fromFinalizationIdExclusive,
385
+ toFinalizationIdInclusive: range.toFinalizationIdInclusive,
386
+ scopeSnapshot: {
387
+ scopeKind: targetScope.scopeKind || null,
388
+ scopeKey: targetScope.scopeKey || null,
389
+ targetScopeEnvelopeId: targetScope.envelopeId || null,
390
+ policyVersion: synthesisInput.scopeEnvelope?.policyVersion || 'scope_envelope_v1',
391
+ },
392
+ checkpointText: summaryText || null,
393
+ checkpointPayload,
394
+ metadata: {
395
+ source: 'session_checkpoint_producer',
396
+ inputHash: checkpointPayload.inputHash,
397
+ triggerKind: checkpointPayload.triggerKind,
398
+ policyVersion: checkpointPayload.policyVersion,
399
+ },
400
+ };
401
+ }
402
+
403
+ module.exports = {
404
+ stableJson,
405
+ hashSnapshot,
406
+ buildCheckpointCoverageFromView,
407
+ buildCheckpointSynthesisInput,
408
+ buildCheckpointSynthesisPrompt,
409
+ promptSafeSynthesisInput,
410
+ normalizeCheckpointSynthesisSummary,
411
+ buildCheckpointRunInputFromSynthesis,
412
+ };