@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.
Files changed (44) hide show
  1. package/.env.example +8 -0
  2. package/README.md +72 -0
  3. package/README_CN.md +17 -0
  4. package/README_TW.md +4 -0
  5. package/aquifer.config.example.json +19 -0
  6. package/consumers/cli.js +259 -12
  7. package/consumers/codex-active-checkpoint.js +186 -0
  8. package/consumers/codex-current-memory.js +106 -0
  9. package/consumers/codex-handoff.js +551 -6
  10. package/consumers/codex.js +209 -25
  11. package/consumers/mcp.js +144 -6
  12. package/consumers/shared/config.js +60 -1
  13. package/consumers/shared/factory.js +10 -3
  14. package/core/aquifer.js +357 -838
  15. package/core/backends/capabilities.js +89 -0
  16. package/core/backends/local.js +430 -0
  17. package/core/legacy-bootstrap.js +140 -0
  18. package/core/mcp-manifest.js +66 -2
  19. package/core/memory-bootstrap.js +20 -8
  20. package/core/memory-consolidation.js +365 -11
  21. package/core/memory-promotion.js +157 -26
  22. package/core/memory-recall.js +341 -22
  23. package/core/memory-records.js +347 -11
  24. package/core/memory-serving.js +132 -0
  25. package/core/postgres-migrations.js +533 -0
  26. package/core/public-session-filter.js +40 -0
  27. package/core/recall-runtime.js +115 -0
  28. package/core/scope-attribution.js +279 -0
  29. package/core/session-checkpoint-producer.js +412 -0
  30. package/core/session-checkpoints.js +432 -0
  31. package/core/session-finalization.js +98 -2
  32. package/core/storage-checkpoints.js +546 -0
  33. package/core/storage.js +121 -8
  34. package/docs/getting-started.md +6 -0
  35. package/docs/setup.md +66 -3
  36. package/package.json +8 -4
  37. package/schema/014-v1-checkpoint-runs.sql +349 -0
  38. package/schema/015-v1-evidence-items.sql +92 -0
  39. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  40. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  41. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  42. package/scripts/codex-checkpoint-commands.js +464 -0
  43. package/scripts/codex-checkpoint-runtime.js +520 -0
  44. package/scripts/codex-recovery.js +246 -1
@@ -1,6 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const { finalizeTranscriptView } = require('./codex');
3
+ const crypto = require('crypto');
4
+
5
+ const {
6
+ buildFinalizationPrompt,
7
+ finalizeTranscriptView,
8
+ resolveCurrentMemoryForFinalization,
9
+ compactCurrentMemorySnapshot,
10
+ } = require('./codex');
4
11
  const { buildFinalizationReview } = require('../core/finalization-review');
5
12
 
6
13
  function normalizeText(value) {
@@ -95,25 +102,554 @@ function buildHandoffMetadata(payload = {}) {
95
102
  };
96
103
  }
97
104
 
105
+ function resolveHandoffSummary(payload = {}, opts = {}) {
106
+ const synthesisSummary = opts.synthesisSummary
107
+ || opts.handoffSynthesisSummary
108
+ || payload.synthesisSummary
109
+ || payload.handoffSynthesisSummary
110
+ || null;
111
+ if (synthesisSummary) {
112
+ return {
113
+ summary: synthesisSummary,
114
+ candidates: Array.isArray(synthesisSummary.candidates) ? synthesisSummary.candidates : undefined,
115
+ usedSynthesis: true,
116
+ };
117
+ }
118
+ return {
119
+ summary: opts.summary || payload.summary || {
120
+ summaryText: opts.summaryText || payload.summaryText,
121
+ structuredSummary: opts.structuredSummary || payload.structuredSummary,
122
+ },
123
+ candidates: Array.isArray(opts.candidates) ? opts.candidates : undefined,
124
+ usedSynthesis: false,
125
+ };
126
+ }
127
+
128
+ function formatHandoffContextBlock(metadata = {}) {
129
+ const handoff = metadata.handoff || {};
130
+ const lines = [
131
+ `<handoff_context source="${metadata.source || 'codex_handoff'}">`,
132
+ `title: ${handoff.title || 'untitled'}`,
133
+ `overview: ${handoff.overview || 'none'}`,
134
+ `status: ${handoff.status || 'unknown'}`,
135
+ `lastStep: ${handoff.lastStep || 'none'}`,
136
+ `next: ${handoff.next || 'none'}`,
137
+ ];
138
+ for (const decision of handoff.decisions || []) {
139
+ lines.push(`decision: ${decision.decision}${decision.reason ? ` | ${decision.reason}` : ''}`);
140
+ }
141
+ for (const loop of handoff.openLoops || []) {
142
+ lines.push(`open_loop: ${loop.item}${loop.owner ? ` | owner=${loop.owner}` : ''}`);
143
+ }
144
+ for (const topic of handoff.topics || []) {
145
+ const name = normalizeText(topic && (topic.name || topic.topic || topic.title));
146
+ const summary = normalizeText(topic && (topic.summary || topic.text));
147
+ if (name || summary) lines.push(`topic: ${name || 'topic'}${summary ? ` | ${summary}` : ''}`);
148
+ }
149
+ lines.push('</handoff_context>');
150
+ return lines.join('\n');
151
+ }
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
+
552
+ function buildHandoffSynthesisPrompt(payload = {}, view = {}, opts = {}) {
553
+ if (!view || view.status !== 'ok') {
554
+ throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
555
+ }
556
+ const metadata = buildHandoffMetadata(payload);
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);
563
+ const handoffBlock = [
564
+ checkpointBlock,
565
+ checkpointBlock ? '' : null,
566
+ previousBootstrapBlock,
567
+ previousBootstrapBlock ? '' : null,
568
+ formatHandoffContextBlock(metadata),
569
+ '',
570
+ '<handoff_synthesis_rules>',
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.',
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.',
577
+ 'Do not copy old current_memory unchanged unless this session confirms it should carry forward.',
578
+ 'Represent resolved, superseded, revoked, or uncertain items explicitly in structuredSummary payload fields when applicable.',
579
+ 'Do not include raw transcript, tool output, debug ids, DB ids, hashes, secrets, or injected context in memory candidates.',
580
+ '</handoff_synthesis_rules>',
581
+ ].filter(line => line !== null && line !== undefined).join('\n');
582
+ return basePrompt.replace('<sanitized_transcript>', `${handoffBlock}\n\n<sanitized_transcript>`);
583
+ }
584
+
585
+ async function prepareHandoffSynthesis(aquifer, payload = {}, opts = {}) {
586
+ const view = opts.view || payload.view;
587
+ if (!view || view.status !== 'ok') {
588
+ throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
589
+ }
590
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
591
+ const checkpoints = await resolveCheckpointsForHandoff(aquifer, payload, opts);
592
+ const previousBootstrap = await resolvePreviousBootstrapForHandoff(aquifer, payload, opts);
593
+ return {
594
+ status: 'needs_agent_summary',
595
+ outputSchemaVersion: 'handoff_current_memory_synthesis_v1',
596
+ view,
597
+ currentMemory,
598
+ checkpoints,
599
+ previousBootstrap,
600
+ prompt: buildHandoffSynthesisPrompt(payload, view, { ...opts, currentMemory, checkpoints, previousBootstrap }),
601
+ };
602
+ }
603
+
98
604
  async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
99
605
  const view = opts.view || payload.view;
100
606
  if (!view || view.status !== 'ok') {
101
607
  throw new Error(`Codex handoff finalization requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
102
608
  }
103
- const summary = opts.summary || payload.summary || {
104
- summaryText: opts.summaryText || payload.summaryText,
105
- structuredSummary: opts.structuredSummary || payload.structuredSummary,
106
- };
609
+ const { summary, candidates, usedSynthesis } = resolveHandoffSummary(payload, opts);
107
610
  const metadata = {
108
611
  ...buildHandoffMetadata(payload),
109
612
  ...(opts.metadata || {}),
110
613
  };
614
+ if (usedSynthesis) {
615
+ metadata.handoffSynthesis = {
616
+ kind: 'handoff_current_memory_synthesis_v1',
617
+ source: 'operator_reviewed_summary',
618
+ promotionGate: 'core_finalization',
619
+ };
620
+ }
621
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
622
+ const checkpoints = await resolveCheckpointsForHandoff(aquifer, payload, opts);
623
+ const previousBootstrap = await resolvePreviousBootstrapForHandoff(aquifer, payload, opts);
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;
111
636
  const result = await finalizeTranscriptView(aquifer, view, summary, {
112
637
  ...opts,
113
638
  mode: 'handoff',
114
639
  metadataSource: 'codex_handoff',
115
640
  metadata,
116
- authority: opts.authority || 'manual',
641
+ authority: opts.authority || (usedSynthesis ? 'verified_summary' : 'manual'),
642
+ candidates,
643
+ candidateEnvelope,
644
+ coverage: opts.coverage || payload.coverage || null,
645
+ candidatePayload: usedSynthesis
646
+ ? {
647
+ kind: 'handoff_synthesis',
648
+ synthesisKind: 'handoff_current_memory_synthesis_v1',
649
+ currentMemoryRole: 'handoff_synthesis_candidate',
650
+ promotionGate: 'core_finalization',
651
+ }
652
+ : opts.candidatePayload || null,
117
653
  });
118
654
  const coreResult = result.finalization || {};
119
655
  const finalSummary = coreResult.summary || {
@@ -148,5 +684,14 @@ async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
148
684
 
149
685
  module.exports = {
150
686
  buildHandoffMetadata,
687
+ buildHandoffSynthesisPrompt,
688
+ compactCheckpointSnapshot,
689
+ buildUncoveredTailView,
690
+ formatCheckpointContextBlock,
691
+ compactPreviousBootstrapContext,
692
+ formatPreviousBootstrapContextBlock,
693
+ resolvePreviousBootstrapForHandoff,
694
+ prepareHandoffSynthesis,
695
+ resolveHandoffSummary,
151
696
  finalizeHandoff,
152
697
  };