@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,279 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const SLOT_ORDER = ['host', 'workspace', 'project', 'repo', 'session', 'task'];
6
+ const PROMOTABLE_SLOT_IDS = new Set(['host', 'workspace', 'project', 'repo']);
7
+ const GENERIC_KEYS = new Set([
8
+ '',
9
+ 'default',
10
+ 'global',
11
+ 'main',
12
+ 'na',
13
+ 'n/a',
14
+ 'none',
15
+ 'null',
16
+ 'unknown',
17
+ 'unset',
18
+ ]);
19
+
20
+ function normalizeText(value) {
21
+ if (value === undefined || value === null) return null;
22
+ const text = String(value).trim();
23
+ return text ? text : null;
24
+ }
25
+
26
+ function collapseWhitespace(value) {
27
+ const text = normalizeText(value);
28
+ return text ? text.replace(/\s+/g, ' ') : null;
29
+ }
30
+
31
+ function slugify(value) {
32
+ const text = normalizeText(value);
33
+ if (!text) return null;
34
+ const slug = text
35
+ .normalize('NFKD')
36
+ .replace(/[^\w\s:/.-]+/g, ' ')
37
+ .trim()
38
+ .replace(/\s+/g, '-')
39
+ .replace(/-+/g, '-')
40
+ .replace(/^-|-$/g, '')
41
+ .toLowerCase();
42
+ return slug || null;
43
+ }
44
+
45
+ function isGenericKey(value) {
46
+ const key = slugify(value) || String(value || '').trim().toLowerCase();
47
+ return GENERIC_KEYS.has(key);
48
+ }
49
+
50
+ function toFactObject(value, aliases = []) {
51
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
52
+ const text = normalizeText(value);
53
+ if (!text) return null;
54
+ const key = aliases[0] || 'value';
55
+ return { [key]: text };
56
+ }
57
+
58
+ function pickValue(fact, keys = []) {
59
+ if (!fact) return null;
60
+ for (const key of keys) {
61
+ const value = normalizeText(fact[key]);
62
+ if (value) return value;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function pickLabel(fact, fallback) {
68
+ return collapseWhitespace(pickValue(fact, ['label', 'title', 'name', 'displayName']) || fallback);
69
+ }
70
+
71
+ function normalizePrefixedScope(scopeKey, expectedPrefix) {
72
+ const raw = normalizeText(scopeKey);
73
+ if (!raw) return null;
74
+ const match = raw.match(/^([a-z_]+):(.*)$/i);
75
+ if (!match) return null;
76
+ const [, prefix, rest] = match;
77
+ if (prefix !== expectedPrefix) return null;
78
+ const body = normalizeText(rest);
79
+ if (!body) return null;
80
+ return `${expectedPrefix}:${body}`;
81
+ }
82
+
83
+ function normalizePathScope(prefix, rawPath) {
84
+ const value = normalizeText(rawPath);
85
+ if (!value) return null;
86
+ return `${prefix}:${path.resolve(value)}`;
87
+ }
88
+
89
+ function buildHostScope(rawFact) {
90
+ const fact = toFactObject(rawFact, ['host']);
91
+ if (!fact) return null;
92
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'host_runtime');
93
+ const key = fromScopeKey
94
+ || (() => {
95
+ const value = pickValue(fact, ['key', 'id', 'host', 'runtime', 'source', 'name', 'label']);
96
+ if (!value || isGenericKey(value)) return null;
97
+ const slug = slugify(value);
98
+ return slug ? `host_runtime:${slug}` : null;
99
+ })();
100
+ if (!key) return null;
101
+ return {
102
+ id: 'host',
103
+ slot: 'host',
104
+ scopeKind: 'host_runtime',
105
+ scopeKey: key,
106
+ label: pickLabel(fact, key.slice('host_runtime:'.length)),
107
+ raw: fact,
108
+ };
109
+ }
110
+
111
+ function buildWorkspaceScope(rawFact) {
112
+ const fact = toFactObject(rawFact, ['path']);
113
+ if (!fact) return null;
114
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'workspace');
115
+ const key = fromScopeKey || normalizePathScope('workspace', pickValue(fact, ['path', 'workspacePath', 'root', 'id']));
116
+ if (!key) return null;
117
+ const scopePath = key.slice('workspace:'.length);
118
+ return {
119
+ id: 'workspace',
120
+ slot: 'workspace',
121
+ scopeKind: 'workspace',
122
+ scopeKey: key,
123
+ label: pickLabel(fact, scopePath),
124
+ raw: fact,
125
+ };
126
+ }
127
+
128
+ function buildProjectScope(rawFact) {
129
+ const fact = toFactObject(rawFact, ['key']);
130
+ if (!fact) return null;
131
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'project');
132
+ const key = fromScopeKey
133
+ || (() => {
134
+ const value = pickValue(fact, ['key', 'slug', 'projectKey', 'projectSlug', 'id', 'name', 'label']);
135
+ if (!value || isGenericKey(value)) return null;
136
+ const slug = slugify(value);
137
+ return slug ? `project:${slug}` : null;
138
+ })();
139
+ if (!key) return null;
140
+ return {
141
+ id: 'project',
142
+ slot: 'project',
143
+ scopeKind: 'project',
144
+ scopeKey: key,
145
+ label: pickLabel(fact, key.slice('project:'.length)),
146
+ raw: fact,
147
+ };
148
+ }
149
+
150
+ function buildRepoScope(rawFact) {
151
+ const fact = toFactObject(rawFact, ['path']);
152
+ if (!fact) return null;
153
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'repo');
154
+ const key = fromScopeKey || normalizePathScope('repo', pickValue(fact, ['path', 'repoPath', 'root', 'repoRoot']));
155
+ if (!key) return null;
156
+ const repoPath = key.slice('repo:'.length);
157
+ return {
158
+ id: 'repo',
159
+ slot: 'repo',
160
+ scopeKind: 'repo',
161
+ scopeKey: key,
162
+ label: pickLabel(fact, path.basename(repoPath) || repoPath),
163
+ raw: fact,
164
+ };
165
+ }
166
+
167
+ function buildSessionScope(rawFact) {
168
+ const fact = toFactObject(rawFact, ['id']);
169
+ if (!fact) return null;
170
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'session');
171
+ const key = fromScopeKey
172
+ || (() => {
173
+ const value = pickValue(fact, ['id', 'key', 'sessionId', 'sessionKey']);
174
+ return value ? `session:${value}` : null;
175
+ })();
176
+ if (!key) return null;
177
+ return {
178
+ id: 'session',
179
+ slot: 'session',
180
+ scopeKind: 'session',
181
+ scopeKey: key,
182
+ label: pickLabel(fact, key.slice('session:'.length)),
183
+ raw: fact,
184
+ };
185
+ }
186
+
187
+ function buildTaskScope(rawFact) {
188
+ const fact = toFactObject(rawFact, ['id']);
189
+ if (!fact) return null;
190
+ const fromScopeKey = normalizePrefixedScope(pickValue(fact, ['scopeKey', 'scope_key']), 'task');
191
+ const key = fromScopeKey
192
+ || (() => {
193
+ const value = pickValue(fact, ['id', 'key', 'taskId', 'taskKey']);
194
+ return value ? `task:${value}` : null;
195
+ })();
196
+ if (!key) return null;
197
+ return {
198
+ id: 'task',
199
+ slot: 'task',
200
+ scopeKind: 'task',
201
+ scopeKey: key,
202
+ label: pickLabel(fact, key.slice('task:'.length)),
203
+ raw: fact,
204
+ };
205
+ }
206
+
207
+ function buildScopeForSlot(slotId, input) {
208
+ switch (slotId) {
209
+ case 'host':
210
+ return buildHostScope(input.host || input.hostRuntime || input.source);
211
+ case 'workspace':
212
+ return buildWorkspaceScope(input.workspace || input.workspacePath);
213
+ case 'project':
214
+ return buildProjectScope(input.project || input.projectKey || input.projectSlug);
215
+ case 'repo':
216
+ return buildRepoScope(input.repo || input.repoPath);
217
+ case 'session':
218
+ return buildSessionScope(input.session || input.sessionId || input.sessionKey);
219
+ case 'task':
220
+ return buildTaskScope(input.task || input.taskId || input.taskKey);
221
+ default:
222
+ return null;
223
+ }
224
+ }
225
+
226
+ function allowedScopeKeysForSlots(scopes) {
227
+ const seen = new Set(['global']);
228
+ const active = ['global'];
229
+ return scopes.map((scope) => {
230
+ if (PROMOTABLE_SLOT_IDS.has(scope.id) && !seen.has(scope.scopeKey)) {
231
+ seen.add(scope.scopeKey);
232
+ active.push(scope.scopeKey);
233
+ }
234
+ return active.slice();
235
+ });
236
+ }
237
+
238
+ function buildScopeEnvelope(input = {}) {
239
+ const scopes = [];
240
+ const seenIds = new Set();
241
+
242
+ for (const slotId of SLOT_ORDER) {
243
+ const scope = buildScopeForSlot(slotId, input);
244
+ if (!scope || seenIds.has(scope.id)) continue;
245
+ seenIds.add(scope.id);
246
+ scopes.push(scope);
247
+ }
248
+
249
+ const slotAllowedScopeKeys = allowedScopeKeysForSlots(scopes);
250
+ const allowedScopeKeys = slotAllowedScopeKeys[slotAllowedScopeKeys.length - 1] || ['global'];
251
+ const promotableScopes = scopes.filter(scope => PROMOTABLE_SLOT_IDS.has(scope.id));
252
+ const activeScope = promotableScopes[promotableScopes.length - 1] || null;
253
+ const slots = scopes.map((scope, index) => ({
254
+ ...scope,
255
+ promotable: PROMOTABLE_SLOT_IDS.has(scope.id),
256
+ allowedScopeKeys: slotAllowedScopeKeys[index],
257
+ }));
258
+ const scopeById = Object.fromEntries(slots.map(scope => [scope.id, scope]));
259
+
260
+ return {
261
+ policyVersion: 'scope_envelope_v1',
262
+ activeSlotId: activeScope ? activeScope.id : 'global',
263
+ activeScopeKey: activeScope ? activeScope.scopeKey : 'global',
264
+ allowedScopeKeys,
265
+ slots,
266
+ scopeById,
267
+ };
268
+ }
269
+
270
+ function getScopeByEnvelopeId(envelope, id) {
271
+ const scope = envelope && envelope.scopeById ? envelope.scopeById[id] : null;
272
+ if (!scope) throw new Error(`Unknown scope envelope id: ${id}`);
273
+ return scope;
274
+ }
275
+
276
+ module.exports = {
277
+ buildScopeEnvelope,
278
+ getScopeByEnvelopeId,
279
+ };
@@ -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
+ };