@shadowforge0/aquifer-memory 1.7.0 → 1.8.1
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 +66 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +217 -14
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +442 -3
- package/consumers/codex.js +164 -107
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +351 -840
- 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-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +128 -8
- 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 +82 -1
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/setup.md +22 -0
- 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 +105 -0
|
@@ -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,70 @@ 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
|
+
|
|
43
108
|
function decorateCandidates(candidates = [], input = {}) {
|
|
44
109
|
const candidatePayload = input.candidatePayload && typeof input.candidatePayload === 'object'
|
|
45
110
|
? input.candidatePayload
|
|
@@ -73,6 +138,7 @@ function createSessionFinalization({
|
|
|
73
138
|
schema,
|
|
74
139
|
recordsSchema,
|
|
75
140
|
defaultTenantId = 'default',
|
|
141
|
+
embedFn = null,
|
|
76
142
|
}) {
|
|
77
143
|
const memorySchema = recordsSchema || qi(schema);
|
|
78
144
|
|
|
@@ -107,6 +173,8 @@ function createSessionFinalization({
|
|
|
107
173
|
scopeKey: input.scopeKey || null,
|
|
108
174
|
contextKey: input.contextKey || null,
|
|
109
175
|
topicKey: input.topicKey || null,
|
|
176
|
+
scopeId: input.scopeId || input.scope_id || null,
|
|
177
|
+
scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
|
|
110
178
|
memoryResult: input.memoryResult || {},
|
|
111
179
|
error: input.error || null,
|
|
112
180
|
metadata: input.metadata || {},
|
|
@@ -196,6 +264,8 @@ function createSessionFinalization({
|
|
|
196
264
|
scopeKey: input.scopeKey || null,
|
|
197
265
|
contextKey: input.contextKey || null,
|
|
198
266
|
topicKey: input.topicKey || null,
|
|
267
|
+
scopeId: input.scopeId || input.scope_id || null,
|
|
268
|
+
scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
|
|
199
269
|
metadata: input.metadata || {},
|
|
200
270
|
claimedAt: input.claimedAt || new Date().toISOString(),
|
|
201
271
|
}, { schema, tenantId: base.tenantId });
|
|
@@ -229,7 +299,7 @@ function createSessionFinalization({
|
|
|
229
299
|
defaultTenantId: base.tenantId,
|
|
230
300
|
inTransaction: true,
|
|
231
301
|
});
|
|
232
|
-
const promotion = createMemoryPromotion({ records });
|
|
302
|
+
const promotion = createMemoryPromotion({ records, embedFn });
|
|
233
303
|
const evidenceRefs = [{
|
|
234
304
|
sourceKind: 'session_summary',
|
|
235
305
|
sourceRef: base.sessionId,
|
|
@@ -254,6 +324,9 @@ function createSessionFinalization({
|
|
|
254
324
|
evidenceRefs,
|
|
255
325
|
});
|
|
256
326
|
const candidates = decorateCandidates(rawCandidates, input);
|
|
327
|
+
const candidateEnvelope = buildCandidateEnvelope(input, candidates, {
|
|
328
|
+
transcriptHash: base.transcriptHash,
|
|
329
|
+
});
|
|
257
330
|
|
|
258
331
|
const memoryResults = candidates.length > 0
|
|
259
332
|
? await promotion.promote(candidates, {
|
|
@@ -297,12 +370,18 @@ function createSessionFinalization({
|
|
|
297
370
|
scopeKey: input.scopeKey || null,
|
|
298
371
|
contextKey: input.contextKey || null,
|
|
299
372
|
topicKey: input.topicKey || null,
|
|
373
|
+
scopeId: input.scopeId || input.scope_id || null,
|
|
374
|
+
scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
|
|
300
375
|
summaryRowId: summaryRow ? summaryRow.session_row_id : session.id,
|
|
301
376
|
memoryResult,
|
|
302
377
|
summaryText: safeSummaryText,
|
|
303
378
|
structuredSummary: safeStructuredSummary,
|
|
304
379
|
humanReviewText,
|
|
305
380
|
sessionStartText,
|
|
381
|
+
candidateEnvelope,
|
|
382
|
+
candidateEnvelopeHash: hashStable(candidateEnvelope),
|
|
383
|
+
candidateEnvelopeVersion: candidateEnvelope.version,
|
|
384
|
+
coverage: input.coverage || candidateEnvelope.coverage || {},
|
|
306
385
|
metadata: {
|
|
307
386
|
...(input.metadata || {}),
|
|
308
387
|
safetyGate: sanitized.meta || {},
|
|
@@ -337,6 +416,8 @@ function createSessionFinalization({
|
|
|
337
416
|
scopeKey: input.scopeKey || null,
|
|
338
417
|
contextKey: input.contextKey || null,
|
|
339
418
|
topicKey: input.topicKey || null,
|
|
419
|
+
scopeId: input.scopeId || input.scope_id || null,
|
|
420
|
+
scopeSnapshot: input.scopeSnapshot || input.scope_snapshot || {},
|
|
340
421
|
metadata: input.metadata || {},
|
|
341
422
|
error: error.message,
|
|
342
423
|
}, { schema, tenantId: base.tenantId });
|