@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,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
|
|
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 });
|