@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
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
4
+
3
5
  const {
4
6
  buildFinalizationPrompt,
5
7
  finalizeTranscriptView,
@@ -148,23 +150,435 @@ function formatHandoffContextBlock(metadata = {}) {
148
150
  return lines.join('\n');
149
151
  }
150
152
 
153
+ function normalizeCheckpointItem(item) {
154
+ if (typeof item === 'string') return normalizeText(item);
155
+ return normalizeText(item && (
156
+ item.item
157
+ || item.decision
158
+ || item.state
159
+ || item.constraint
160
+ || item.conclusion
161
+ || item.summary
162
+ || item.text
163
+ ));
164
+ }
165
+
166
+ function optionalNonNegativeInteger(...values) {
167
+ for (const value of values) {
168
+ if (value === undefined || value === null || value === '') continue;
169
+ const n = Number(value);
170
+ if (Number.isInteger(n) && n >= 0) return n;
171
+ }
172
+ return null;
173
+ }
174
+
175
+ function compactCheckpointRow(row = {}) {
176
+ const structured = row.structuredSummary || row.structured_summary || row.payload?.structuredSummary || {};
177
+ const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
178
+ const coverage = row.coverage || row.coverageMetadata || row.coverage_metadata || row.metadata?.coverage || payload.coverage || {};
179
+ const transcriptCoverage = coverage.transcript && typeof coverage.transcript === 'object'
180
+ ? coverage.transcript
181
+ : {};
182
+ const summary = normalizeText(row.summaryText || row.summary_text || row.summary || row.payload?.summaryText);
183
+ const scopeKey = normalizeText(row.scopeKey || row.scope_key || row.targetScopeKey || row.target_scope_key);
184
+ const topicKey = normalizeText(row.topicKey || row.topic_key || row.topic || row.payload?.topicKey);
185
+ const status = normalizeText(row.status || row.lifecycle || 'accepted_process_material');
186
+ const trigger = normalizeText(row.trigger || row.triggerKind || row.trigger_kind || row.payload?.triggerKind);
187
+ const bucket = {
188
+ scopeKey,
189
+ topicKey,
190
+ status,
191
+ trigger,
192
+ summary,
193
+ decisions: [],
194
+ openLoops: [],
195
+ states: [],
196
+ constraints: [],
197
+ conclusions: [],
198
+ coverage: {
199
+ coveredUntilMessageIndex: optionalNonNegativeInteger(
200
+ coverage.coveredUntilMessageIndex,
201
+ coverage.covered_until_message_index,
202
+ coverage.messageIndex,
203
+ coverage.message_index,
204
+ row.coveredUntilMessageIndex,
205
+ row.covered_until_message_index
206
+ ),
207
+ coveredUntilChar: optionalNonNegativeInteger(
208
+ coverage.coveredUntilChar,
209
+ coverage.coveredUntilCharIndex,
210
+ coverage.covered_until_char,
211
+ coverage.covered_until_char_index,
212
+ row.coveredUntilChar,
213
+ row.covered_until_char
214
+ ),
215
+ coveredUntilLine: optionalNonNegativeInteger(
216
+ coverage.coveredUntilLine,
217
+ coverage.coveredUntilLineIndex,
218
+ coverage.covered_until_line,
219
+ coverage.covered_until_line_index,
220
+ coverage.line,
221
+ coverage.lineIndex,
222
+ coverage.line_index,
223
+ transcriptCoverage.coveredUntilLine,
224
+ transcriptCoverage.covered_until_line,
225
+ transcriptCoverage.line,
226
+ transcriptCoverage.lineIndex,
227
+ transcriptCoverage.line_index,
228
+ row.coveredUntilLine,
229
+ row.covered_until_line
230
+ ),
231
+ coveredUntilLineChar: optionalNonNegativeInteger(
232
+ coverage.coveredUntilLineChar,
233
+ coverage.coveredUntilLineCharIndex,
234
+ coverage.covered_until_line_char,
235
+ coverage.covered_until_line_char_index,
236
+ coverage.char,
237
+ coverage.charIndex,
238
+ coverage.char_index,
239
+ transcriptCoverage.coveredUntilLineChar,
240
+ transcriptCoverage.covered_until_line_char,
241
+ transcriptCoverage.char,
242
+ transcriptCoverage.charIndex,
243
+ transcriptCoverage.char_index,
244
+ row.coveredUntilLineChar,
245
+ row.covered_until_line_char
246
+ ),
247
+ },
248
+ };
249
+ for (const item of normalizeList(structured.decisions || row.decisions)) {
250
+ const text = normalizeCheckpointItem(item);
251
+ if (text) addUniqueByText(bucket.decisions, { item: text }, text);
252
+ }
253
+ for (const item of normalizeList(structured.open_loops || structured.openLoops || row.openLoops || row.open_loops)) {
254
+ const text = normalizeCheckpointItem(item);
255
+ if (text) addUniqueByText(bucket.openLoops, { item: text }, text);
256
+ }
257
+ for (const item of normalizeList(structured.states || row.states)) {
258
+ const text = normalizeCheckpointItem(item);
259
+ if (text) addUniqueByText(bucket.states, { item: text }, text);
260
+ }
261
+ for (const item of normalizeList(structured.constraints || row.constraints)) {
262
+ const text = normalizeCheckpointItem(item);
263
+ if (text) addUniqueByText(bucket.constraints, { item: text }, text);
264
+ }
265
+ for (const item of normalizeList(structured.conclusions || row.conclusions)) {
266
+ const text = normalizeCheckpointItem(item);
267
+ if (text) addUniqueByText(bucket.conclusions, { item: text }, text);
268
+ }
269
+ return bucket;
270
+ }
271
+
272
+ function messageText(message = {}) {
273
+ if (typeof message.content === 'string') return message.content;
274
+ if (Array.isArray(message.content)) {
275
+ return message.content
276
+ .map(part => typeof part === 'string' ? part : (part && (part.text || part.content) ? String(part.text || part.content) : ''))
277
+ .filter(Boolean)
278
+ .join('\n');
279
+ }
280
+ if (typeof message.text === 'string') return message.text;
281
+ return '';
282
+ }
283
+
284
+ function renderMessages(messages = []) {
285
+ return messages.map((message) => {
286
+ const role = normalizeText(message.role) || 'message';
287
+ return `[${role}]\n${messageText(message)}`;
288
+ }).join('\n\n');
289
+ }
290
+
291
+ function approxPromptTokens(text) {
292
+ return Math.ceil(String(text || '').length / 3);
293
+ }
294
+
295
+ function hashText(value) {
296
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex');
297
+ }
298
+
299
+ function offsetFromLineChar(text, lineIndex, charIndex) {
300
+ if (typeof text !== 'string') return null;
301
+ if (!Number.isInteger(lineIndex) || lineIndex < 0) return null;
302
+ if (!Number.isInteger(charIndex) || charIndex < 0) return null;
303
+ let offset = 0;
304
+ let currentLine = 0;
305
+ while (currentLine < lineIndex) {
306
+ const lineBreak = text.indexOf('\n', offset);
307
+ if (lineBreak === -1) return null;
308
+ offset = lineBreak + 1;
309
+ currentLine += 1;
310
+ }
311
+ const lineBreak = text.indexOf('\n', offset);
312
+ const lineEnd = lineBreak === -1 ? text.length : lineBreak;
313
+ if (charIndex > (lineEnd - offset)) return null;
314
+ return offset + charIndex;
315
+ }
316
+
317
+ function compactCheckpointSnapshot(checkpoints = [], opts = {}) {
318
+ const maxCheckpoints = Math.max(0, Math.min(12, opts.maxCheckpoints || opts.checkpointLimit || 6));
319
+ const rows = Array.isArray(checkpoints?.checkpoints)
320
+ ? checkpoints.checkpoints
321
+ : (Array.isArray(checkpoints?.items) ? checkpoints.items : checkpoints);
322
+ const compactRows = (Array.isArray(rows) ? rows : [])
323
+ .map(compactCheckpointRow)
324
+ .filter(row => row.summary || row.decisions.length || row.openLoops.length || row.states.length || row.constraints.length || row.conclusions.length)
325
+ .slice(0, maxCheckpoints);
326
+ return {
327
+ checkpoints: compactRows,
328
+ meta: {
329
+ source: checkpoints?.meta?.source || 'checkpoint_runs',
330
+ role: 'handoff_process_material',
331
+ count: compactRows.length,
332
+ truncated: Boolean(checkpoints?.meta?.truncated || (Array.isArray(rows) && rows.length > compactRows.length)),
333
+ },
334
+ };
335
+ }
336
+
337
+ function buildUncoveredTailView(view = {}, checkpoints = null) {
338
+ const snapshot = compactCheckpointSnapshot(checkpoints || []);
339
+ let coveredUntilMessageIndex = null;
340
+ let coveredUntilChar = null;
341
+ let coveredUntilLine = null;
342
+ let coveredUntilLineChar = null;
343
+ for (const row of snapshot.checkpoints) {
344
+ const messageIndex = row.coverage.coveredUntilMessageIndex;
345
+ if (Number.isInteger(messageIndex) && messageIndex >= 0) {
346
+ coveredUntilMessageIndex = Math.max(coveredUntilMessageIndex ?? -1, messageIndex);
347
+ }
348
+ const charIndex = row.coverage.coveredUntilChar;
349
+ if (Number.isInteger(charIndex) && charIndex >= 0) {
350
+ coveredUntilChar = Math.max(coveredUntilChar ?? -1, charIndex);
351
+ }
352
+ const lineIndex = row.coverage.coveredUntilLine;
353
+ const lineChar = row.coverage.coveredUntilLineChar;
354
+ if (Number.isInteger(lineIndex) && lineIndex >= 0 && Number.isInteger(lineChar) && lineChar >= 0) {
355
+ const shouldReplace = coveredUntilLine === null
356
+ || lineIndex > coveredUntilLine
357
+ || (lineIndex === coveredUntilLine && lineChar > coveredUntilLineChar);
358
+ if (shouldReplace) {
359
+ coveredUntilLine = lineIndex;
360
+ coveredUntilLineChar = lineChar;
361
+ }
362
+ }
363
+ }
364
+ let bestTail = null;
365
+ if (Number.isInteger(coveredUntilMessageIndex) && Array.isArray(view.messages)) {
366
+ if (coveredUntilMessageIndex < view.messages.length) {
367
+ const tailMessages = view.messages.slice(coveredUntilMessageIndex + 1);
368
+ const text = renderMessages(tailMessages);
369
+ bestTail = {
370
+ ...view,
371
+ messages: tailMessages,
372
+ text,
373
+ charCount: text.length,
374
+ approxPromptTokens: approxPromptTokens(text),
375
+ checkpointTail: {
376
+ sourceMessageCount: view.messages.length,
377
+ coveredUntilMessageIndex,
378
+ tailMessageCount: tailMessages.length,
379
+ },
380
+ };
381
+ }
382
+ }
383
+ if (Number.isInteger(coveredUntilChar) && typeof view.text === 'string') {
384
+ if (coveredUntilChar <= view.text.length) {
385
+ const text = view.text.slice(coveredUntilChar);
386
+ const candidate = {
387
+ ...view,
388
+ text,
389
+ charCount: text.length,
390
+ approxPromptTokens: approxPromptTokens(text),
391
+ checkpointTail: {
392
+ sourceCharCount: view.text.length,
393
+ coveredUntilChar,
394
+ tailCharCount: text.length,
395
+ },
396
+ };
397
+ if (!bestTail || candidate.text.length < bestTail.text.length) bestTail = candidate;
398
+ }
399
+ }
400
+ if (Number.isInteger(coveredUntilLine) && Number.isInteger(coveredUntilLineChar) && typeof view.text === 'string') {
401
+ const start = offsetFromLineChar(view.text, coveredUntilLine, coveredUntilLineChar);
402
+ if (start !== null) {
403
+ const text = view.text.slice(start);
404
+ const candidate = {
405
+ ...view,
406
+ text,
407
+ charCount: text.length,
408
+ approxPromptTokens: approxPromptTokens(text),
409
+ checkpointTail: {
410
+ sourceCharCount: view.text.length,
411
+ coveredUntilLine,
412
+ coveredUntilLineChar,
413
+ tailCharCount: text.length,
414
+ },
415
+ };
416
+ if (!bestTail || candidate.text.length < bestTail.text.length) bestTail = candidate;
417
+ }
418
+ }
419
+ return bestTail || view;
420
+ }
421
+
422
+ function formatCheckpointContextBlock(checkpoints = null, opts = {}) {
423
+ const snapshot = compactCheckpointSnapshot(checkpoints || [], opts);
424
+ if (snapshot.checkpoints.length === 0) return '';
425
+ const lines = [
426
+ `<checkpoint_context source="${snapshot.meta.source}" role="${snapshot.meta.role}" count="${snapshot.meta.count}" truncated="${snapshot.meta.truncated}">`,
427
+ 'Checkpoint context is producer process material, not current truth. Use it only to reduce transcript replay and reconcile against current_memory and the uncovered transcript tail.',
428
+ ];
429
+ for (const row of snapshot.checkpoints) {
430
+ const attrs = [
431
+ row.scopeKey ? `scope=${row.scopeKey}` : null,
432
+ row.topicKey ? `topic=${row.topicKey}` : null,
433
+ row.status ? `status=${row.status}` : null,
434
+ row.trigger ? `trigger=${row.trigger}` : null,
435
+ ].filter(Boolean).join(' ');
436
+ lines.push(`checkpoint${attrs ? ` ${attrs}` : ''}: ${row.summary || 'process material'}`);
437
+ for (const decision of row.decisions) lines.push(` decision: ${decision.item}`);
438
+ for (const state of row.states) lines.push(` state: ${state.item}`);
439
+ for (const constraint of row.constraints) lines.push(` constraint: ${constraint.item}`);
440
+ for (const conclusion of row.conclusions) lines.push(` conclusion: ${conclusion.item}`);
441
+ for (const loop of row.openLoops) lines.push(` open_loop: ${loop.item}`);
442
+ }
443
+ lines.push('</checkpoint_context>');
444
+ return lines.join('\n');
445
+ }
446
+
447
+ function compactPreviousBootstrapContext(input = null, opts = {}) {
448
+ const source = input !== undefined && input !== null
449
+ ? input
450
+ : (opts.previousBootstrap !== undefined ? opts.previousBootstrap : null);
451
+ if (!source) return null;
452
+ const rawText = typeof source === 'string'
453
+ ? source
454
+ : normalizeText(source.text || source.context || source.sessionStartText || source.session_start_text || '');
455
+ const memories = Array.isArray(source.memories) ? source.memories : [];
456
+ const renderedMemories = memories
457
+ .map(item => normalizeText(item && (item.summary || item.title || item.text || item.state || item.decision || item.item)))
458
+ .filter(Boolean)
459
+ .slice(0, 12);
460
+ const text = rawText || renderedMemories.map(item => `- ${item}`).join('\n');
461
+ if (!text) return null;
462
+ const maxChars = Math.max(240, Math.min(6000, opts.previousBootstrapMaxChars || 3000));
463
+ const clipped = text.length > maxChars ? text.slice(0, maxChars) : text;
464
+ const meta = source && typeof source === 'object' && source.meta && typeof source.meta === 'object'
465
+ ? source.meta
466
+ : {};
467
+ return {
468
+ text: clipped,
469
+ meta: {
470
+ source: 'previous_bootstrap',
471
+ originalSource: meta.source || source.source || null,
472
+ activeScopePath: Array.isArray(meta.activeScopePath) ? meta.activeScopePath : undefined,
473
+ truncated: text.length > clipped.length,
474
+ hash: hashText(text),
475
+ },
476
+ };
477
+ }
478
+
479
+ function formatPreviousBootstrapContextBlock(previousBootstrap = null, opts = {}) {
480
+ const compact = compactPreviousBootstrapContext(previousBootstrap, opts);
481
+ if (!compact) return '';
482
+ const lines = [
483
+ `<previous_bootstrap_context source="${compact.meta.source}" truncated="${compact.meta.truncated}">`,
484
+ 'Previous bootstrap context is producer process material, not current truth. Use it to reconcile what should carry forward, close, supersede, or be dropped.',
485
+ 'Treat previous_bootstrap_context as producer process material and reconcile it against current_memory and the transcript before creating candidates.',
486
+ compact.text,
487
+ '</previous_bootstrap_context>',
488
+ ];
489
+ return lines.join('\n');
490
+ }
491
+
492
+ async function resolveCheckpointsForHandoff(aquifer, payload = {}, opts = {}) {
493
+ if (opts.includeCheckpoints === false) return null;
494
+ const provided = opts.checkpoints !== undefined ? opts.checkpoints : payload.checkpoints;
495
+ if (provided !== undefined) return compactCheckpointSnapshot(provided, opts);
496
+ const listFn = aquifer?.checkpoints?.listForHandoff || aquifer?.checkpoints?.listAcceptedForHandoff;
497
+ if (typeof listFn !== 'function') return null;
498
+ try {
499
+ const rows = await listFn.call(aquifer.checkpoints, {
500
+ tenantId: opts.tenantId,
501
+ sessionId: payload.sessionId || opts.sessionId,
502
+ activeScopeKey: opts.activeScopeKey || opts.scopeKey,
503
+ activeScopePath: opts.activeScopePath,
504
+ limit: opts.checkpointLimit || opts.maxCheckpoints || 6,
505
+ });
506
+ return compactCheckpointSnapshot(rows, opts);
507
+ } catch (err) {
508
+ return {
509
+ checkpoints: [],
510
+ meta: {
511
+ source: 'checkpoint_runs',
512
+ role: 'handoff_process_material',
513
+ count: 0,
514
+ truncated: false,
515
+ degraded: true,
516
+ error: err.message,
517
+ },
518
+ };
519
+ }
520
+ }
521
+
522
+ async function resolvePreviousBootstrapForHandoff(aquifer, payload = {}, opts = {}) {
523
+ if (opts.includePreviousBootstrap === false) return null;
524
+ if (opts.previousBootstrap !== undefined) return compactPreviousBootstrapContext(opts.previousBootstrap, opts);
525
+ if (payload.previousBootstrap !== undefined) return compactPreviousBootstrapContext(payload.previousBootstrap, opts);
526
+
527
+ const bootstrapFn = typeof aquifer?.memory?.bootstrap === 'function'
528
+ ? aquifer.memory.bootstrap
529
+ : (typeof aquifer?.bootstrap === 'function' ? aquifer.bootstrap : null);
530
+ if (typeof bootstrapFn !== 'function') return null;
531
+
532
+ const bootstrapOwner = typeof aquifer?.memory?.bootstrap === 'function' ? aquifer.memory : aquifer;
533
+ const bootstrapOpts = {
534
+ tenantId: opts.tenantId,
535
+ scopeId: opts.scopeId,
536
+ activeScopeKey: opts.activeScopeKey || opts.scopeKey,
537
+ activeScopePath: opts.activeScopePath,
538
+ asOf: opts.previousBootstrapAsOf || opts.asOf,
539
+ limit: opts.previousBootstrapLimit || opts.bootstrapLimit || 20,
540
+ maxChars: opts.previousBootstrapMaxChars || 3000,
541
+ format: 'both',
542
+ };
543
+ if (bootstrapOwner === aquifer) bootstrapOpts.servingMode = 'curated';
544
+ try {
545
+ const result = await bootstrapFn.call(bootstrapOwner, bootstrapOpts);
546
+ return compactPreviousBootstrapContext(result, opts);
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+
151
552
  function buildHandoffSynthesisPrompt(payload = {}, view = {}, opts = {}) {
152
553
  if (!view || view.status !== 'ok') {
153
554
  throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
154
555
  }
155
556
  const metadata = buildHandoffMetadata(payload);
156
- const basePrompt = buildFinalizationPrompt(view, opts);
557
+ const checkpoints = opts.checkpoints || payload.checkpoints;
558
+ const previousBootstrap = opts.previousBootstrap !== undefined ? opts.previousBootstrap : payload.previousBootstrap;
559
+ const promptView = buildUncoveredTailView(view, checkpoints);
560
+ const basePrompt = buildFinalizationPrompt(promptView, opts);
561
+ const checkpointBlock = formatCheckpointContextBlock(checkpoints, opts);
562
+ const previousBootstrapBlock = formatPreviousBootstrapContextBlock(previousBootstrap, opts);
157
563
  const handoffBlock = [
564
+ checkpointBlock,
565
+ checkpointBlock ? '' : null,
566
+ previousBootstrapBlock,
567
+ previousBootstrapBlock ? '' : null,
158
568
  formatHandoffContextBlock(metadata),
159
569
  '',
160
570
  '<handoff_synthesis_rules>',
161
571
  'Treat handoff_context as producer process material, not current truth by itself.',
572
+ 'Treat checkpoint_context as producer process material, not current truth by itself.',
573
+ 'Treat previous_bootstrap_context as producer process material, not current truth by itself.',
162
574
  'Use the sanitized transcript and current_memory to decide what should become current memory candidates.',
575
+ 'When checkpoint_context is present, use it to avoid replaying already-covered session ranges, but reconcile it against the transcript tail instead of promoting it directly.',
576
+ 'When previous_bootstrap_context is present, use it only to reconcile carry-forward intent; do not copy it directly into current memory candidates.',
163
577
  'Do not copy old current_memory unchanged unless this session confirms it should carry forward.',
164
578
  'Represent resolved, superseded, revoked, or uncertain items explicitly in structuredSummary payload fields when applicable.',
165
579
  'Do not include raw transcript, tool output, debug ids, DB ids, hashes, secrets, or injected context in memory candidates.',
166
580
  '</handoff_synthesis_rules>',
167
- ].join('\n');
581
+ ].filter(line => line !== null && line !== undefined).join('\n');
168
582
  return basePrompt.replace('<sanitized_transcript>', `${handoffBlock}\n\n<sanitized_transcript>`);
169
583
  }
170
584
 
@@ -174,12 +588,16 @@ async function prepareHandoffSynthesis(aquifer, payload = {}, opts = {}) {
174
588
  throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
175
589
  }
176
590
  const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
591
+ const checkpoints = await resolveCheckpointsForHandoff(aquifer, payload, opts);
592
+ const previousBootstrap = await resolvePreviousBootstrapForHandoff(aquifer, payload, opts);
177
593
  return {
178
594
  status: 'needs_agent_summary',
179
595
  outputSchemaVersion: 'handoff_current_memory_synthesis_v1',
180
596
  view,
181
597
  currentMemory,
182
- prompt: buildHandoffSynthesisPrompt(payload, view, { ...opts, currentMemory }),
598
+ checkpoints,
599
+ previousBootstrap,
600
+ prompt: buildHandoffSynthesisPrompt(payload, view, { ...opts, currentMemory, checkpoints, previousBootstrap }),
183
601
  };
184
602
  }
185
603
 
@@ -201,7 +619,20 @@ async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
201
619
  };
202
620
  }
203
621
  const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
622
+ const checkpoints = await resolveCheckpointsForHandoff(aquifer, payload, opts);
623
+ const previousBootstrap = await resolvePreviousBootstrapForHandoff(aquifer, payload, opts);
204
624
  if (currentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(currentMemory, opts);
625
+ if (checkpoints) metadata.checkpoints = checkpoints;
626
+ const candidateEnvelope = usedSynthesis
627
+ ? {
628
+ version: 'handoff_current_memory_synthesis_v1',
629
+ inputContext: {
630
+ previousBootstrap: previousBootstrap ? previousBootstrap.meta : null,
631
+ checkpoints: checkpoints ? checkpoints.meta : null,
632
+ currentMemory: metadata.currentMemory ? metadata.currentMemory.meta : null,
633
+ },
634
+ }
635
+ : opts.candidateEnvelope || null;
205
636
  const result = await finalizeTranscriptView(aquifer, view, summary, {
206
637
  ...opts,
207
638
  mode: 'handoff',
@@ -209,6 +640,8 @@ async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
209
640
  metadata,
210
641
  authority: opts.authority || (usedSynthesis ? 'verified_summary' : 'manual'),
211
642
  candidates,
643
+ candidateEnvelope,
644
+ coverage: opts.coverage || payload.coverage || null,
212
645
  candidatePayload: usedSynthesis
213
646
  ? {
214
647
  kind: 'handoff_synthesis',
@@ -252,6 +685,12 @@ async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
252
685
  module.exports = {
253
686
  buildHandoffMetadata,
254
687
  buildHandoffSynthesisPrompt,
688
+ compactCheckpointSnapshot,
689
+ buildUncoveredTailView,
690
+ formatCheckpointContextBlock,
691
+ compactPreviousBootstrapContext,
692
+ formatPreviousBootstrapContextBlock,
693
+ resolvePreviousBootstrapForHandoff,
255
694
  prepareHandoffSynthesis,
256
695
  resolveHandoffSummary,
257
696
  finalizeHandoff,