@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
@@ -0,0 +1,432 @@
1
+ 'use strict';
2
+
3
+ const storage = require('./storage');
4
+ const checkpointProducer = require('./session-checkpoint-producer');
5
+
6
+ function qi(identifier) {
7
+ return `"${String(identifier).replace(/"/g, '""')}"`;
8
+ }
9
+
10
+ function clampLimit(value, fallback = 6, max = 50) {
11
+ const n = Number(value || fallback);
12
+ if (!Number.isFinite(n)) return fallback;
13
+ return Math.max(1, Math.min(max, Math.trunc(n)));
14
+ }
15
+
16
+ function normalizeScopePath(input = {}) {
17
+ const path = Array.isArray(input.activeScopePath)
18
+ ? input.activeScopePath
19
+ : (Array.isArray(input.scopePath) ? input.scopePath : []);
20
+ const out = [];
21
+ for (const value of path) {
22
+ const text = String(value || '').trim();
23
+ if (text && !out.includes(text)) out.push(text);
24
+ }
25
+ const active = String(input.activeScopeKey || input.scopeKey || '').trim();
26
+ if (active && !out.includes(active)) out.push(active);
27
+ return out;
28
+ }
29
+
30
+ function stableJson(value) {
31
+ return checkpointProducer.stableJson(value);
32
+ }
33
+
34
+ function hashSnapshot(value) {
35
+ return checkpointProducer.hashSnapshot(value);
36
+ }
37
+
38
+ function parsePositiveInt(value, fallback = 10, max = 200) {
39
+ const n = Number(value === undefined || value === null || value === '' ? fallback : value);
40
+ if (!Number.isFinite(n)) return fallback;
41
+ return Math.max(1, Math.min(max, Math.trunc(n)));
42
+ }
43
+
44
+ function compactStructuredSummary(value = {}) {
45
+ if (!value || typeof value !== 'object') return {};
46
+ const out = {};
47
+ for (const key of ['facts', 'decisions', 'open_loops', 'openLoops', 'preferences', 'constraints', 'conclusions', 'entity_notes', 'entityNotes', 'states']) {
48
+ const rows = Array.isArray(value[key]) ? value[key] : [];
49
+ if (rows.length > 0) out[key] = rows.slice(0, 8);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function compactFinalizationRow(row = {}, index = 0) {
55
+ return {
56
+ index,
57
+ finalizationId: row.id,
58
+ sessionId: row.session_id || row.sessionId || null,
59
+ source: row.source || null,
60
+ agentId: row.agent_id || row.agentId || null,
61
+ mode: row.mode || null,
62
+ finalizedAt: row.finalized_at || row.finalizedAt || null,
63
+ summaryText: String(row.summary_text || row.summaryText || '').replace(/\s+/g, ' ').trim(),
64
+ structuredSummary: compactStructuredSummary(row.structured_summary || row.structuredSummary || {}),
65
+ scopeId: row.scope_id || row.scopeId || null,
66
+ scopeSnapshot: row.scope_snapshot || row.scopeSnapshot || {},
67
+ };
68
+ }
69
+
70
+ function renderFinalizationCheckpointView(rows = [], input = {}) {
71
+ const finalizations = rows.map(compactFinalizationRow);
72
+ const text = finalizations.map((row, index) => {
73
+ const lines = [
74
+ `[finalization ${index + 1}]`,
75
+ `mode: ${row.mode || 'unknown'}`,
76
+ `summary: ${row.summaryText || 'none'}`,
77
+ ];
78
+ const structured = stableJson(row.structuredSummary || {});
79
+ if (structured !== '{}') lines.push(`structuredSummary: ${structured}`);
80
+ return lines.join('\n');
81
+ }).join('\n\n');
82
+ const transcriptHash = hashSnapshot({
83
+ kind: 'checkpoint_finalization_view_v1',
84
+ scopeId: input.scopeId || input.scope_id || null,
85
+ range: {
86
+ from: input.fromFinalizationIdExclusive ?? input.from_finalization_id_exclusive ?? 0,
87
+ to: finalizations.length ? finalizations[finalizations.length - 1].finalizationId : null,
88
+ },
89
+ finalizations: finalizations.map(row => ({
90
+ finalizationId: row.finalizationId,
91
+ summaryText: row.summaryText,
92
+ structuredSummary: row.structuredSummary,
93
+ })),
94
+ });
95
+ return {
96
+ status: 'ok',
97
+ sessionId: input.sessionId || `checkpoint-scope-${input.scopeId || input.scope_id || 'unknown'}`,
98
+ transcriptHash,
99
+ messages: finalizations.map((row, index) => ({
100
+ role: 'assistant',
101
+ content: `[finalization ${index + 1}]\n${row.summaryText || stableJson(row.structuredSummary || {})}`,
102
+ })),
103
+ text,
104
+ charCount: text.length,
105
+ approxPromptTokens: Math.ceil(text.length / 3),
106
+ finalizations,
107
+ metadata: {
108
+ source: 'session_finalizations',
109
+ sourceOfTruth: 'finalized_session_summaries',
110
+ },
111
+ };
112
+ }
113
+
114
+ async function resolveScope(pool, input = {}, { schema, tenantId }) {
115
+ if (input.scopeId || input.scope_id) {
116
+ const result = await pool.query(
117
+ `SELECT *
118
+ FROM ${qi(schema)}.scopes
119
+ WHERE tenant_id = $1 AND id = $2
120
+ LIMIT 1`,
121
+ [tenantId, input.scopeId || input.scope_id],
122
+ );
123
+ const row = result.rows[0] || null;
124
+ if (!row) throw new Error(`checkpoint scope not found: ${input.scopeId || input.scope_id}`);
125
+ return row;
126
+ }
127
+ const scopeKey = String(input.scopeKey || input.scope_key || '').trim();
128
+ if (!scopeKey) throw new Error('scopeId or scopeKey is required for checkpoint planning');
129
+ const params = [tenantId, scopeKey];
130
+ const where = ['tenant_id = $1', 'scope_key = $2'];
131
+ if (input.scopeKind || input.scope_kind) {
132
+ params.push(input.scopeKind || input.scope_kind);
133
+ where.push(`scope_kind = $${params.length}`);
134
+ }
135
+ const result = await pool.query(
136
+ `SELECT *
137
+ FROM ${qi(schema)}.scopes
138
+ WHERE ${where.join(' AND ')}
139
+ ORDER BY id DESC
140
+ LIMIT 1`,
141
+ params,
142
+ );
143
+ const row = result.rows[0] || null;
144
+ if (!row) throw new Error(`checkpoint scope not found: ${scopeKey}`);
145
+ return row;
146
+ }
147
+
148
+ function buildScopeEnvelopeFromScope(scope = {}) {
149
+ const slotId = ['workspace', 'project', 'repo', 'host_runtime'].includes(scope.scope_kind)
150
+ ? (scope.scope_kind === 'host_runtime' ? 'host' : scope.scope_kind)
151
+ : 'target';
152
+ const slot = {
153
+ id: slotId,
154
+ slot: slotId,
155
+ scopeKind: scope.scope_kind,
156
+ scopeKey: scope.scope_key,
157
+ label: scope.scope_key,
158
+ promotable: true,
159
+ allowedScopeKeys: ['global', scope.scope_key].filter(Boolean),
160
+ };
161
+ return {
162
+ policyVersion: 'scope_envelope_v1',
163
+ activeSlotId: slot.id,
164
+ activeScopeKey: scope.scope_key,
165
+ allowedScopeKeys: slot.allowedScopeKeys,
166
+ slots: [slot],
167
+ scopeById: { [slot.id]: slot },
168
+ };
169
+ }
170
+
171
+ async function findLatestCheckpoint(pool, input = {}, { schema, tenantId }) {
172
+ const result = await pool.query(
173
+ `SELECT *
174
+ FROM ${qi(schema)}.checkpoint_runs
175
+ WHERE tenant_id = $1
176
+ AND scope_id = $2
177
+ AND status = ANY($3::text[])
178
+ AND to_finalization_id_inclusive IS NOT NULL
179
+ ORDER BY to_finalization_id_inclusive DESC, id DESC
180
+ LIMIT 1`,
181
+ [tenantId, input.scopeId || input.scope_id, ['processing', 'finalized']],
182
+ );
183
+ return result.rows[0] || null;
184
+ }
185
+
186
+ async function listFinalizationsForCheckpoint(pool, input = {}, { schema, tenantId }) {
187
+ const params = [
188
+ tenantId,
189
+ input.scopeId || input.scope_id,
190
+ input.fromFinalizationIdExclusive ?? input.from_finalization_id_exclusive ?? 0,
191
+ ];
192
+ const where = [
193
+ 'tenant_id = $1',
194
+ 'scope_id = $2',
195
+ "status = 'finalized'",
196
+ 'id > $3',
197
+ ];
198
+ if (input.source) {
199
+ params.push(input.source);
200
+ where.push(`source = $${params.length}`);
201
+ }
202
+ if (input.agentId || input.agent_id) {
203
+ params.push(input.agentId || input.agent_id);
204
+ where.push(`agent_id = $${params.length}`);
205
+ }
206
+ params.push(Math.max(1, Math.min(200, input.limit || 50)));
207
+ const result = await pool.query(
208
+ `SELECT *
209
+ FROM ${qi(schema)}.session_finalizations
210
+ WHERE ${where.join(' AND ')}
211
+ ORDER BY id ASC
212
+ LIMIT $${params.length}`,
213
+ params,
214
+ );
215
+ return result.rows;
216
+ }
217
+
218
+ function mapCheckpointRun(row = {}) {
219
+ const payload = row.checkpoint_payload && typeof row.checkpoint_payload === 'object'
220
+ ? row.checkpoint_payload
221
+ : {};
222
+ return {
223
+ id: row.id,
224
+ checkpointKey: row.checkpoint_key,
225
+ status: row.status,
226
+ scopeId: row.scope_id,
227
+ scopeKind: row.scope_kind || row.scope_snapshot?.scopeKind || null,
228
+ scopeKey: row.scope_key || row.scope_snapshot?.scopeKey || null,
229
+ fromFinalizationIdExclusive: row.from_finalization_id_exclusive ?? null,
230
+ toFinalizationIdInclusive: row.to_finalization_id_inclusive ?? null,
231
+ topicKey: payload.topicKey || payload.topic_key || row.scope_snapshot?.topicKey || null,
232
+ triggerKind: payload.triggerKind || payload.trigger_kind || row.metadata?.triggerKind || row.metadata?.trigger_kind || null,
233
+ summaryText: row.checkpoint_text || payload.summaryText || payload.summary || '',
234
+ structuredSummary: payload.structuredSummary || payload.structured_summary || {},
235
+ coverage: payload.coverage || {},
236
+ metadata: {
237
+ source: 'checkpoint_runs',
238
+ checkpointKey: row.checkpoint_key,
239
+ status: row.status,
240
+ },
241
+ };
242
+ }
243
+
244
+ function createSessionCheckpoints({ pool, schema, defaultTenantId = 'default' }) {
245
+ async function planFromFinalizations(input = {}) {
246
+ const tenantId = input.tenantId || defaultTenantId;
247
+ const scope = await resolveScope(pool, input, { schema, tenantId });
248
+ const minFinalizations = parsePositiveInt(
249
+ input.minFinalizations || input.min_finalizations || input.checkpointEveryFinalizations,
250
+ 10,
251
+ 100,
252
+ );
253
+ const lastCheckpoint = input.fromFinalizationIdExclusive !== undefined || input.from_finalization_id_exclusive !== undefined
254
+ ? null
255
+ : await findLatestCheckpoint(pool, { scopeId: scope.id }, { schema, tenantId });
256
+ const fromFinalizationIdExclusive = Number(
257
+ input.fromFinalizationIdExclusive
258
+ ?? input.from_finalization_id_exclusive
259
+ ?? lastCheckpoint?.to_finalization_id_inclusive
260
+ ?? 0
261
+ );
262
+ const finalizations = await listFinalizationsForCheckpoint(pool, {
263
+ ...input,
264
+ scopeId: scope.id,
265
+ fromFinalizationIdExclusive,
266
+ limit: Math.max(minFinalizations, parsePositiveInt(input.limit, minFinalizations, 200)),
267
+ }, { schema, tenantId });
268
+ const due = input.force === true || finalizations.length >= minFinalizations;
269
+ const base = {
270
+ status: due ? 'needs_agent_summary' : 'not_ready',
271
+ due,
272
+ triggerKind: input.triggerKind || input.trigger_kind || 'finalization_count',
273
+ minFinalizations,
274
+ sourceFinalizationCount: finalizations.length,
275
+ scope: {
276
+ id: scope.id,
277
+ scopeKind: scope.scope_kind,
278
+ scopeKey: scope.scope_key,
279
+ },
280
+ lastCheckpoint: lastCheckpoint ? mapCheckpointRun(lastCheckpoint) : null,
281
+ fromFinalizationIdExclusive,
282
+ finalizations: finalizations.map(compactFinalizationRow),
283
+ };
284
+ if (!due || finalizations.length === 0) return base;
285
+ const toFinalizationIdInclusive = Number(finalizations[finalizations.length - 1].id);
286
+ const view = renderFinalizationCheckpointView(finalizations, {
287
+ scopeId: scope.id,
288
+ fromFinalizationIdExclusive,
289
+ });
290
+ const scopeEnvelope = buildScopeEnvelopeFromScope(scope);
291
+ const synthesisInput = checkpointProducer.buildCheckpointSynthesisInput({
292
+ view,
293
+ scopeEnvelope,
294
+ targetScopeEnvelopeId: scopeEnvelope.activeSlotId,
295
+ storageScopeId: scope.id,
296
+ fromFinalizationIdExclusive,
297
+ toFinalizationIdInclusive,
298
+ sourceOfTruth: 'finalized_session_summaries',
299
+ triggerKind: base.triggerKind,
300
+ coverage: {
301
+ coordinateSystem: 'checkpoint_finalization_view_v1',
302
+ coveredUntilMessageIndex: Math.max(0, finalizations.length - 1),
303
+ coveredUntilChar: view.text.length,
304
+ },
305
+ currentMemory: input.currentMemory || null,
306
+ previousCheckpoints: lastCheckpoint ? [mapCheckpointRun(lastCheckpoint)] : [],
307
+ }, input);
308
+ return {
309
+ ...base,
310
+ range: {
311
+ fromFinalizationIdExclusive,
312
+ toFinalizationIdInclusive,
313
+ },
314
+ view,
315
+ synthesisInput,
316
+ synthesisPrompt: input.includeSynthesisPrompt === false
317
+ ? undefined
318
+ : checkpointProducer.buildCheckpointSynthesisPrompt(synthesisInput, input),
319
+ };
320
+ }
321
+
322
+ async function runProducer(input = {}) {
323
+ const tenantId = input.tenantId || defaultTenantId;
324
+ const plan = await planFromFinalizations(input);
325
+ const synthesisSummary = input.synthesisSummary || input.synthesis_summary || null;
326
+ if (!plan.due || !synthesisSummary) return plan;
327
+ const runInput = checkpointProducer.buildCheckpointRunInputFromSynthesis(
328
+ plan.synthesisInput,
329
+ synthesisSummary,
330
+ {
331
+ scopeId: plan.scope.id,
332
+ status: input.finalize === true ? 'finalized' : (input.status || 'processing'),
333
+ checkpointKey: input.checkpointKey || input.checkpoint_key,
334
+ },
335
+ );
336
+ const shouldApply = input.apply === true;
337
+ if (!shouldApply) {
338
+ return {
339
+ ...plan,
340
+ runInput,
341
+ dryRun: true,
342
+ };
343
+ }
344
+ const run = await storage.upsertCheckpointRun(pool, {
345
+ ...runInput,
346
+ tenantId,
347
+ }, { schema, tenantId });
348
+ const sources = await storage.upsertCheckpointRunSources(pool, plan.finalizations.map((row, index) => ({
349
+ finalizationId: row.finalizationId,
350
+ sourceIndex: index,
351
+ finalization: row,
352
+ })), {
353
+ checkpointRunId: run.id,
354
+ tenantId,
355
+ }, { schema, tenantId });
356
+ return {
357
+ ...plan,
358
+ run,
359
+ sources,
360
+ dryRun: false,
361
+ };
362
+ }
363
+
364
+ async function listForHandoff(input = {}) {
365
+ const tenantId = input.tenantId || defaultTenantId;
366
+ const limit = clampLimit(input.limit || input.checkpointLimit || input.maxCheckpoints);
367
+ if (input.scopeId || input.scope_id) {
368
+ const rows = await storage.listCheckpointRuns(pool, {
369
+ tenantId,
370
+ scopeId: input.scopeId || input.scope_id,
371
+ status: input.status || 'finalized',
372
+ limit,
373
+ }, { schema, tenantId });
374
+ return rows.map(mapCheckpointRun);
375
+ }
376
+
377
+ const scopePath = normalizeScopePath(input);
378
+ if (scopePath.length === 0) return [];
379
+ const result = await pool.query(
380
+ `SELECT c.*, s.scope_kind, s.scope_key
381
+ FROM ${qi(schema)}.checkpoint_runs c
382
+ JOIN ${qi(schema)}.scopes s
383
+ ON s.tenant_id = c.tenant_id
384
+ AND s.id = c.scope_id
385
+ WHERE c.tenant_id = $1
386
+ AND c.status = $2
387
+ AND s.scope_key = ANY($3::text[])
388
+ ORDER BY array_position($3::text[], s.scope_key) DESC NULLS LAST,
389
+ c.finalized_at DESC NULLS LAST,
390
+ c.updated_at DESC,
391
+ c.id DESC
392
+ LIMIT $4`,
393
+ [tenantId, input.status || 'finalized', scopePath, limit]
394
+ );
395
+ return result.rows.map(mapCheckpointRun);
396
+ }
397
+
398
+ return {
399
+ upsertRun: (input = {}) => storage.upsertCheckpointRun(pool, input, {
400
+ schema,
401
+ tenantId: input.tenantId || defaultTenantId,
402
+ }),
403
+ updateRunStatus: (input = {}) => storage.updateCheckpointRunStatus(pool, input, {
404
+ schema,
405
+ tenantId: input.tenantId || defaultTenantId,
406
+ }),
407
+ listRuns: (input = {}) => storage.listCheckpointRuns(pool, input, {
408
+ schema,
409
+ tenantId: input.tenantId || defaultTenantId,
410
+ }),
411
+ upsertSources: (rows = [], input = {}) => storage.upsertCheckpointRunSources(pool, rows, input, {
412
+ schema,
413
+ tenantId: input.tenantId || defaultTenantId,
414
+ }),
415
+ listSources: (input = {}) => storage.listCheckpointRunSources(pool, input, {
416
+ schema,
417
+ tenantId: input.tenantId || defaultTenantId,
418
+ }),
419
+ buildSynthesisInput: checkpointProducer.buildCheckpointSynthesisInput,
420
+ buildSynthesisPrompt: checkpointProducer.buildCheckpointSynthesisPrompt,
421
+ buildRunInputFromSynthesis: checkpointProducer.buildCheckpointRunInputFromSynthesis,
422
+ planFromFinalizations,
423
+ runProducer,
424
+ listForHandoff,
425
+ listAcceptedForHandoff: listForHandoff,
426
+ };
427
+ }
428
+
429
+ module.exports = {
430
+ createSessionCheckpoints,
431
+ ...checkpointProducer,
432
+ };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const storage = require('./storage');
4
5
  const { createMemoryRecords } = require('./memory-records');
5
6
  const { createMemoryPromotion } = require('./memory-promotion');
@@ -40,6 +41,84 @@ function summarizeMemoryResults(results = [], extra = {}) {
40
41
  };
41
42
  }
42
43
 
44
+ function stableJson(value) {
45
+ if (value === null || value === undefined) return JSON.stringify(null);
46
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
47
+ if (typeof value === 'object') {
48
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
49
+ }
50
+ return JSON.stringify(value);
51
+ }
52
+
53
+ function hashStable(value) {
54
+ return crypto.createHash('sha256').update(stableJson(value)).digest('hex');
55
+ }
56
+
57
+ function publicCandidateEnvelopeRow(candidate = {}, index = 0) {
58
+ const rest = { ...(candidate || {}) };
59
+ delete rest.embedding;
60
+ delete rest._preparedEvidenceTexts;
61
+ return {
62
+ index,
63
+ memoryType: rest.memoryType || rest.memory_type || null,
64
+ canonicalKey: rest.canonicalKey || rest.canonical_key || null,
65
+ scopeKind: rest.scopeKind || rest.scope_kind || null,
66
+ scopeKey: rest.scopeKey || rest.scope_key || null,
67
+ contextKey: rest.contextKey || rest.context_key || null,
68
+ topicKey: rest.topicKey || rest.topic_key || null,
69
+ inheritanceMode: rest.inheritanceMode || rest.inheritance_mode || null,
70
+ authority: rest.authority || null,
71
+ title: rest.title || null,
72
+ summary: rest.summary || null,
73
+ payload: rest.payload || {},
74
+ visibleInBootstrap: rest.visibleInBootstrap === true || rest.visible_in_bootstrap === true,
75
+ visibleInRecall: rest.visibleInRecall === true || rest.visible_in_recall === true,
76
+ evidenceRefs: Array.isArray(rest.evidenceRefs || rest.evidence_refs)
77
+ ? (rest.evidenceRefs || rest.evidence_refs)
78
+ : [],
79
+ validFrom: rest.validFrom || rest.valid_from || null,
80
+ validTo: rest.validTo || rest.valid_to || null,
81
+ staleAfter: rest.staleAfter || rest.stale_after || null,
82
+ candidateHash: hashStable({
83
+ memoryType: rest.memoryType || rest.memory_type || null,
84
+ canonicalKey: rest.canonicalKey || rest.canonical_key || null,
85
+ summary: rest.summary || null,
86
+ payload: rest.payload || {},
87
+ evidenceRefs: rest.evidenceRefs || rest.evidence_refs || [],
88
+ }),
89
+ };
90
+ }
91
+
92
+ function buildCandidateEnvelope(input = {}, candidates = [], opts = {}) {
93
+ const provided = input.candidateEnvelope || input.candidate_envelope || {};
94
+ const version = provided.version
95
+ || input.candidateEnvelopeVersion
96
+ || input.candidate_envelope_version
97
+ || 'current_memory_candidate_envelope_v1';
98
+ return {
99
+ ...provided,
100
+ version,
101
+ source: provided.source || input.mode || 'finalization',
102
+ transcriptHash: opts.transcriptHash || input.transcriptHash || null,
103
+ inputContext: provided.inputContext || provided.input_context || {},
104
+ candidates: candidates.map(publicCandidateEnvelopeRow),
105
+ };
106
+ }
107
+
108
+ function decorateCandidates(candidates = [], input = {}) {
109
+ const candidatePayload = input.candidatePayload && typeof input.candidatePayload === 'object'
110
+ ? input.candidatePayload
111
+ : null;
112
+ if (!candidatePayload) return candidates;
113
+ return candidates.map(candidate => ({
114
+ ...candidate,
115
+ payload: {
116
+ ...(candidate.payload || {}),
117
+ ...candidatePayload,
118
+ },
119
+ }));
120
+ }
121
+
43
122
  function normalizeFinalizationInput(input = {}, defaults = {}) {
44
123
  const tenantId = input.tenantId || defaults.defaultTenantId || 'default';
45
124
  return {
@@ -59,6 +138,7 @@ function createSessionFinalization({
59
138
  schema,
60
139
  recordsSchema,
61
140
  defaultTenantId = 'default',
141
+ embedFn = null,
62
142
  }) {
63
143
  const memorySchema = recordsSchema || qi(schema);
64
144
 
@@ -93,6 +173,8 @@ function createSessionFinalization({
93
173
  scopeKey: input.scopeKey || null,
94
174
  contextKey: input.contextKey || null,
95
175
  topicKey: input.topicKey || null,
176
+ scopeId: input.scopeId || input.scope_id || null,
177
+ scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
96
178
  memoryResult: input.memoryResult || {},
97
179
  error: input.error || null,
98
180
  metadata: input.metadata || {},
@@ -182,6 +264,8 @@ function createSessionFinalization({
182
264
  scopeKey: input.scopeKey || null,
183
265
  contextKey: input.contextKey || null,
184
266
  topicKey: input.topicKey || null,
267
+ scopeId: input.scopeId || input.scope_id || null,
268
+ scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
185
269
  metadata: input.metadata || {},
186
270
  claimedAt: input.claimedAt || new Date().toISOString(),
187
271
  }, { schema, tenantId: base.tenantId });
@@ -215,7 +299,7 @@ function createSessionFinalization({
215
299
  defaultTenantId: base.tenantId,
216
300
  inTransaction: true,
217
301
  });
218
- const promotion = createMemoryPromotion({ records });
302
+ const promotion = createMemoryPromotion({ records, embedFn });
219
303
  const evidenceRefs = [{
220
304
  sourceKind: 'session_summary',
221
305
  sourceRef: base.sessionId,
@@ -227,7 +311,7 @@ function createSessionFinalization({
227
311
  phase: base.phase,
228
312
  },
229
313
  }];
230
- const candidates = Array.isArray(input.candidates)
314
+ const rawCandidates = Array.isArray(input.candidates)
231
315
  ? input.candidates
232
316
  : promotion.extractCandidates({
233
317
  sessionId: base.sessionId,
@@ -239,6 +323,10 @@ function createSessionFinalization({
239
323
  authority: input.authority || 'verified_summary',
240
324
  evidenceRefs,
241
325
  });
326
+ const candidates = decorateCandidates(rawCandidates, input);
327
+ const candidateEnvelope = buildCandidateEnvelope(input, candidates, {
328
+ transcriptHash: base.transcriptHash,
329
+ });
242
330
 
243
331
  const memoryResults = candidates.length > 0
244
332
  ? await promotion.promote(candidates, {
@@ -282,12 +370,18 @@ function createSessionFinalization({
282
370
  scopeKey: input.scopeKey || null,
283
371
  contextKey: input.contextKey || null,
284
372
  topicKey: input.topicKey || null,
373
+ scopeId: input.scopeId || input.scope_id || null,
374
+ scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
285
375
  summaryRowId: summaryRow ? summaryRow.session_row_id : session.id,
286
376
  memoryResult,
287
377
  summaryText: safeSummaryText,
288
378
  structuredSummary: safeStructuredSummary,
289
379
  humanReviewText,
290
380
  sessionStartText,
381
+ candidateEnvelope,
382
+ candidateEnvelopeHash: hashStable(candidateEnvelope),
383
+ candidateEnvelopeVersion: candidateEnvelope.version,
384
+ coverage: input.coverage || candidateEnvelope.coverage || {},
291
385
  metadata: {
292
386
  ...(input.metadata || {}),
293
387
  safetyGate: sanitized.meta || {},
@@ -322,6 +416,8 @@ function createSessionFinalization({
322
416
  scopeKey: input.scopeKey || null,
323
417
  contextKey: input.contextKey || null,
324
418
  topicKey: input.topicKey || null,
419
+ scopeId: input.scopeId || input.scope_id || null,
420
+ scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
325
421
  metadata: input.metadata || {},
326
422
  error: error.message,
327
423
  }, { schema, tenantId: base.tenantId });