@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.
- package/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- 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
|
+
};
|