@shadowforge0/aquifer-memory 1.5.9 → 1.6.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 +23 -0
- package/README.md +96 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +374 -39
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +131 -7
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +44 -4
- package/consumers/shared/config.js +28 -0
- package/consumers/shared/factory.js +2 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +53 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +384 -18
- package/core/finalization-review.js +319 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +69 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +456 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +31 -9
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -119
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { extractCandidatesFromStructuredSummary, createMemoryPromotion } = require('./memory-promotion');
|
|
5
|
+
const { createMemoryRecords } = require('./memory-records');
|
|
6
|
+
|
|
7
|
+
const ALLOWED_CADENCES = new Set(['session', 'daily', 'weekly', 'monthly', 'manual']);
|
|
8
|
+
const OPERATOR_CADENCES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
9
|
+
const DEFAULT_CLAIM_LEASE_SECONDS = 600;
|
|
10
|
+
const DEFAULT_OPERATOR_SNAPSHOT_LIMIT = 1000;
|
|
11
|
+
const MAX_OPERATOR_SNAPSHOT_LIMIT = 5000;
|
|
12
|
+
|
|
13
|
+
function stableJson(value) {
|
|
14
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
15
|
+
if (value && typeof value === 'object') {
|
|
16
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stableJson(value[k])}`).join(',')}}`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hashSnapshot(value) {
|
|
22
|
+
return crypto.createHash('sha256').update(stableJson(value)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function advisoryLockKeys(namespace, value) {
|
|
26
|
+
const digest = crypto.createHash('sha256').update(`${namespace}:${value}`).digest();
|
|
27
|
+
return [digest.readInt32BE(0), digest.readInt32BE(4)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalInstant(value) {
|
|
31
|
+
const t = timeMs(value);
|
|
32
|
+
return t === null ? String(value || '') : new Date(t).toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function timeMs(value) {
|
|
36
|
+
const t = Date.parse(value || '');
|
|
37
|
+
return Number.isFinite(t) ? t : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeClaimLeaseSeconds(value) {
|
|
41
|
+
const n = Number(value);
|
|
42
|
+
if (!Number.isFinite(n)) return DEFAULT_CLAIM_LEASE_SECONDS;
|
|
43
|
+
return Math.max(10, Math.floor(n));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requirePeriod(opts = {}) {
|
|
47
|
+
const cadence = opts.cadence || 'manual';
|
|
48
|
+
if (!ALLOWED_CADENCES.has(cadence)) {
|
|
49
|
+
throw new Error(`memory.consolidation.plan invalid cadence: ${cadence}`);
|
|
50
|
+
}
|
|
51
|
+
const periodStart = opts.periodStart || opts.from || null;
|
|
52
|
+
const periodEnd = opts.periodEnd || opts.to || null;
|
|
53
|
+
if (!periodStart || !periodEnd) {
|
|
54
|
+
throw new Error('memory.consolidation.plan requires periodStart and periodEnd');
|
|
55
|
+
}
|
|
56
|
+
const startMs = timeMs(periodStart);
|
|
57
|
+
const endMs = timeMs(periodEnd);
|
|
58
|
+
if (startMs === null || endMs === null) {
|
|
59
|
+
throw new Error('memory.consolidation.plan requires valid periodStart and periodEnd');
|
|
60
|
+
}
|
|
61
|
+
if (endMs <= startMs) {
|
|
62
|
+
throw new Error('memory.consolidation.plan requires periodEnd after periodStart');
|
|
63
|
+
}
|
|
64
|
+
return { cadence, periodStart, periodEnd, startMs, endMs };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function utcDayStart(ms) {
|
|
68
|
+
const d = new Date(ms);
|
|
69
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function utcWeekStart(ms) {
|
|
73
|
+
const dayStart = utcDayStart(ms);
|
|
74
|
+
const d = new Date(dayStart);
|
|
75
|
+
const mondayOffset = (d.getUTCDay() + 6) % 7;
|
|
76
|
+
return dayStart - (mondayOffset * 86400000);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function utcMonthStart(ms) {
|
|
80
|
+
const d = new Date(ms);
|
|
81
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveOperatorCadence(value) {
|
|
85
|
+
const cadence = String(value || 'manual').trim().toLowerCase();
|
|
86
|
+
if (!OPERATOR_CADENCES.has(cadence)) {
|
|
87
|
+
throw new Error(`memory.consolidation.job invalid cadence: ${cadence}`);
|
|
88
|
+
}
|
|
89
|
+
return cadence;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveOperatorAnchorMs(input = {}) {
|
|
93
|
+
const value = input.anchorTime || input.now || input.asOf || input.snapshotAsOf || Date.now();
|
|
94
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
95
|
+
const parsed = Date.parse(value);
|
|
96
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
97
|
+
throw new Error('memory.consolidation.job requires a valid anchorTime');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveOperatorWindow(input = {}) {
|
|
101
|
+
const cadence = resolveOperatorCadence(input.cadence);
|
|
102
|
+
const hasExplicitWindow = Boolean(input.periodStart || input.periodEnd || input.from || input.to);
|
|
103
|
+
|
|
104
|
+
if (cadence === 'manual' || hasExplicitWindow) {
|
|
105
|
+
const period = requirePeriod({
|
|
106
|
+
cadence,
|
|
107
|
+
periodStart: input.periodStart || input.from || null,
|
|
108
|
+
periodEnd: input.periodEnd || input.to || null,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
cadence,
|
|
112
|
+
periodStart: canonicalInstant(period.periodStart),
|
|
113
|
+
periodEnd: canonicalInstant(period.periodEnd),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const anchorMs = resolveOperatorAnchorMs(input);
|
|
118
|
+
let periodStartMs;
|
|
119
|
+
let periodEndMs;
|
|
120
|
+
|
|
121
|
+
if (cadence === 'daily') {
|
|
122
|
+
periodEndMs = utcDayStart(anchorMs);
|
|
123
|
+
periodStartMs = periodEndMs - 86400000;
|
|
124
|
+
} else if (cadence === 'weekly') {
|
|
125
|
+
periodEndMs = utcWeekStart(anchorMs);
|
|
126
|
+
periodStartMs = periodEndMs - (7 * 86400000);
|
|
127
|
+
} else if (cadence === 'monthly') {
|
|
128
|
+
periodEndMs = utcMonthStart(anchorMs);
|
|
129
|
+
const d = new Date(periodEndMs);
|
|
130
|
+
periodStartMs = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - 1, 1);
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error(`memory.consolidation.job invalid cadence: ${cadence}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
cadence,
|
|
137
|
+
periodStart: new Date(periodStartMs).toISOString(),
|
|
138
|
+
periodEnd: new Date(periodEndMs).toISOString(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeOperatorSnapshotLimit(value) {
|
|
143
|
+
if (value === null || value === undefined || value === '') return DEFAULT_OPERATOR_SNAPSHOT_LIMIT;
|
|
144
|
+
const n = Number(value);
|
|
145
|
+
if (!Number.isFinite(n)) return DEFAULT_OPERATOR_SNAPSHOT_LIMIT;
|
|
146
|
+
return Math.max(1, Math.min(MAX_OPERATOR_SNAPSHOT_LIMIT, Math.floor(n)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeStringList(value) {
|
|
150
|
+
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean);
|
|
151
|
+
if (value === null || value === undefined || value === '') return [];
|
|
152
|
+
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function recordKey(record) {
|
|
156
|
+
return String(record.canonicalKey || record.canonical_key || record.id || record.memory_id || '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeRecordId(value) {
|
|
160
|
+
if (value === null || value === undefined || value === '') return null;
|
|
161
|
+
const number = Number(value);
|
|
162
|
+
if (Number.isSafeInteger(number) && number > 0 && String(number) === String(value)) return number;
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeRecord(record) {
|
|
167
|
+
return {
|
|
168
|
+
id: normalizeRecordId(record.id || record.memory_id),
|
|
169
|
+
memoryType: record.memoryType || record.memory_type || null,
|
|
170
|
+
canonicalKey: recordKey(record),
|
|
171
|
+
status: record.status || null,
|
|
172
|
+
scopeKind: record.scopeKind || record.scope_kind || null,
|
|
173
|
+
scopeKey: record.scopeKey || record.scope_key || null,
|
|
174
|
+
contextKey: record.contextKey || record.context_key || null,
|
|
175
|
+
topicKey: record.topicKey || record.topic_key || null,
|
|
176
|
+
summary: record.summary || '',
|
|
177
|
+
validFrom: record.validFrom || record.valid_from || null,
|
|
178
|
+
validTo: record.validTo || record.valid_to || null,
|
|
179
|
+
staleAfter: record.staleAfter || record.stale_after || null,
|
|
180
|
+
acceptedAt: record.acceptedAt || record.accepted_at || null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function aggregateCandidateCadence(cadence) {
|
|
185
|
+
return cadence === 'daily' || cadence === 'weekly' || cadence === 'monthly';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function safeKeyPart(value, fallback) {
|
|
189
|
+
const text = String(value || fallback || '').trim().toLowerCase();
|
|
190
|
+
return text.replace(/\s+/g, '-').replace(/[^a-z0-9:._/-]/g, '-');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function compactSummary(record) {
|
|
194
|
+
const summary = String(record.summary || '').trim().replace(/\s+/g, ' ');
|
|
195
|
+
if (!summary) return record.canonicalKey;
|
|
196
|
+
return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
200
|
+
const { cadence, periodStart, periodEnd } = opts;
|
|
201
|
+
if (!aggregateCandidateCadence(cadence)) return [];
|
|
202
|
+
|
|
203
|
+
const staleIds = new Set(statusUpdates.map(update => String(update.memoryId)));
|
|
204
|
+
const staleKeys = new Set(statusUpdates.map(update => String(update.canonicalKey || '')));
|
|
205
|
+
const active = normalized.filter(record => {
|
|
206
|
+
if (record.status !== 'active') return false;
|
|
207
|
+
if (record.id !== null && staleIds.has(String(record.id))) return false;
|
|
208
|
+
if (staleKeys.has(record.canonicalKey)) return false;
|
|
209
|
+
return Boolean(record.canonicalKey);
|
|
210
|
+
});
|
|
211
|
+
if (active.length === 0) return [];
|
|
212
|
+
|
|
213
|
+
const tenantId = opts.tenantId || 'default';
|
|
214
|
+
const policyVersion = opts.policyVersion || 'v1';
|
|
215
|
+
const windowStart = canonicalInstant(periodStart);
|
|
216
|
+
const windowEnd = canonicalInstant(periodEnd);
|
|
217
|
+
const groups = new Map();
|
|
218
|
+
|
|
219
|
+
for (const record of active) {
|
|
220
|
+
const scopeKind = record.scopeKind || 'unspecified';
|
|
221
|
+
const scopeKey = record.scopeKey || 'unspecified';
|
|
222
|
+
const contextKey = record.contextKey || null;
|
|
223
|
+
const topicKey = record.topicKey || null;
|
|
224
|
+
const groupKey = stableJson({ scopeKind, scopeKey, contextKey, topicKey });
|
|
225
|
+
if (!groups.has(groupKey)) {
|
|
226
|
+
groups.set(groupKey, {
|
|
227
|
+
scopeKind,
|
|
228
|
+
scopeKey,
|
|
229
|
+
contextKey,
|
|
230
|
+
topicKey,
|
|
231
|
+
records: [],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
groups.get(groupKey).records.push(record);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const candidates = [];
|
|
238
|
+
const sortedGroups = [...groups.values()].sort((a, b) => {
|
|
239
|
+
const aKey = stableJson({ scopeKind: a.scopeKind, scopeKey: a.scopeKey, contextKey: a.contextKey, topicKey: a.topicKey });
|
|
240
|
+
const bKey = stableJson({ scopeKind: b.scopeKind, scopeKey: b.scopeKey, contextKey: b.contextKey, topicKey: b.topicKey });
|
|
241
|
+
return aKey.localeCompare(bKey);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
for (const group of sortedGroups) {
|
|
245
|
+
const records = group.records.sort((a, b) => {
|
|
246
|
+
if (a.memoryType !== b.memoryType) return String(a.memoryType).localeCompare(String(b.memoryType));
|
|
247
|
+
if (a.canonicalKey !== b.canonicalKey) return a.canonicalKey.localeCompare(b.canonicalKey);
|
|
248
|
+
return String(a.id).localeCompare(String(b.id));
|
|
249
|
+
});
|
|
250
|
+
const sourceMemoryIds = records.map(record => record.id).filter(id => id !== null);
|
|
251
|
+
const sourceCanonicalKeys = records.map(record => record.canonicalKey);
|
|
252
|
+
const subject = [
|
|
253
|
+
`tenant:${safeKeyPart(tenantId, 'default')}`,
|
|
254
|
+
`scope:${safeKeyPart(group.scopeKey, 'unspecified')}`,
|
|
255
|
+
`context:${safeKeyPart(group.contextKey, 'none')}`,
|
|
256
|
+
`topic:${safeKeyPart(group.topicKey, 'none')}`,
|
|
257
|
+
`cadence:${cadence}`,
|
|
258
|
+
].join('|');
|
|
259
|
+
const aspect = [
|
|
260
|
+
'aggregate',
|
|
261
|
+
`policy:${safeKeyPart(policyVersion, 'v1')}`,
|
|
262
|
+
`window:${safeKeyPart(windowStart, 'start')}_${safeKeyPart(windowEnd, 'end')}`,
|
|
263
|
+
].join('|');
|
|
264
|
+
const canonicalKey = [
|
|
265
|
+
'conclusion',
|
|
266
|
+
safeKeyPart(group.scopeKey, 'unspecified'),
|
|
267
|
+
subject,
|
|
268
|
+
aspect,
|
|
269
|
+
].join(':');
|
|
270
|
+
const candidateHash = hashSnapshot({
|
|
271
|
+
canonicalKey,
|
|
272
|
+
cadence,
|
|
273
|
+
policyVersion,
|
|
274
|
+
periodStart: windowStart,
|
|
275
|
+
periodEnd: windowEnd,
|
|
276
|
+
sourceMemoryIds,
|
|
277
|
+
sourceCanonicalKeys,
|
|
278
|
+
});
|
|
279
|
+
const title = `${cadence} memory rollup candidate for ${group.scopeKey}`;
|
|
280
|
+
const summary = [
|
|
281
|
+
`${cadence} rollup candidate for ${group.scopeKind}:${group.scopeKey} covering ${records.length} active curated memories from ${windowStart} to ${windowEnd}.`,
|
|
282
|
+
...records.map(record => `- ${record.memoryType}:${record.canonicalKey}: ${compactSummary(record)}`),
|
|
283
|
+
].join('\n');
|
|
284
|
+
|
|
285
|
+
candidates.push({
|
|
286
|
+
memoryType: 'conclusion',
|
|
287
|
+
status: 'candidate',
|
|
288
|
+
canonicalKey,
|
|
289
|
+
scopeKind: group.scopeKind,
|
|
290
|
+
scopeKey: group.scopeKey,
|
|
291
|
+
contextKey: group.contextKey,
|
|
292
|
+
topicKey: group.topicKey,
|
|
293
|
+
inheritanceMode: cadence === 'daily' ? 'defaultable' : 'additive',
|
|
294
|
+
title,
|
|
295
|
+
summary,
|
|
296
|
+
candidateHash,
|
|
297
|
+
payload: {
|
|
298
|
+
kind: 'compaction_rollup',
|
|
299
|
+
cadence,
|
|
300
|
+
policyVersion,
|
|
301
|
+
periodStart: windowStart,
|
|
302
|
+
periodEnd: windowEnd,
|
|
303
|
+
candidateHash,
|
|
304
|
+
sourceMemoryIds,
|
|
305
|
+
sourceCanonicalKeys,
|
|
306
|
+
sourceRecordCount: records.length,
|
|
307
|
+
},
|
|
308
|
+
authority: 'system',
|
|
309
|
+
evidenceRefs: records.map(record => ({
|
|
310
|
+
sourceKind: 'external',
|
|
311
|
+
sourceRef: record.id !== null
|
|
312
|
+
? `memory_record:${record.id}`
|
|
313
|
+
: `memory_record:${record.canonicalKey}`,
|
|
314
|
+
relationKind: 'derived_from',
|
|
315
|
+
metadata: {
|
|
316
|
+
compaction: true,
|
|
317
|
+
cadence,
|
|
318
|
+
periodStart: windowStart,
|
|
319
|
+
periodEnd: windowEnd,
|
|
320
|
+
canonicalKey: record.canonicalKey,
|
|
321
|
+
},
|
|
322
|
+
})),
|
|
323
|
+
visibleInBootstrap: true,
|
|
324
|
+
visibleInRecall: true,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return candidates;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildCoverage(normalized, statusUpdates, candidates) {
|
|
332
|
+
const active = normalized.filter(record => record.status === 'active');
|
|
333
|
+
const activeOpenLoops = active.filter(record => record.memoryType === 'open_loop');
|
|
334
|
+
return {
|
|
335
|
+
sourceCoverage: {
|
|
336
|
+
recordCount: normalized.length,
|
|
337
|
+
activeCount: active.length,
|
|
338
|
+
activeOpenLoopCount: activeOpenLoops.length,
|
|
339
|
+
},
|
|
340
|
+
outputCoverage: {
|
|
341
|
+
candidateCount: candidates.length,
|
|
342
|
+
statusUpdateCount: statusUpdates.length,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function createApplySummary(statusUpdates) {
|
|
348
|
+
return {
|
|
349
|
+
applied: 0,
|
|
350
|
+
skipped: 0,
|
|
351
|
+
unsupported: 0,
|
|
352
|
+
statusUpdates: statusUpdates.length,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function applyStatusUpdatesWithRecords(statusUpdates, targetRecords, tenantId, summary) {
|
|
357
|
+
for (const update of statusUpdates) {
|
|
358
|
+
if (update.status !== 'stale') {
|
|
359
|
+
summary.unsupported++;
|
|
360
|
+
summary.skipped++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const row = await targetRecords.updateMemoryStatusIfCurrent({
|
|
364
|
+
tenantId,
|
|
365
|
+
memoryId: update.memoryId,
|
|
366
|
+
fromStatus: 'active',
|
|
367
|
+
status: 'stale',
|
|
368
|
+
validTo: update.validTo || null,
|
|
369
|
+
});
|
|
370
|
+
if (row) summary.applied++;
|
|
371
|
+
else summary.skipped++;
|
|
372
|
+
}
|
|
373
|
+
return summary;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function summarizePromotionResults(results = []) {
|
|
377
|
+
const summary = {
|
|
378
|
+
candidates: results.length,
|
|
379
|
+
planned: 0,
|
|
380
|
+
promoted: 0,
|
|
381
|
+
quarantined: 0,
|
|
382
|
+
skipped: 0,
|
|
383
|
+
errored: 0,
|
|
384
|
+
reasons: {},
|
|
385
|
+
};
|
|
386
|
+
for (const result of results) {
|
|
387
|
+
const action = result && result.action ? result.action : 'error';
|
|
388
|
+
const reason = result && result.reason ? result.reason : 'unknown';
|
|
389
|
+
if (action === 'planned') summary.planned++;
|
|
390
|
+
else if (action === 'promote') summary.promoted++;
|
|
391
|
+
else if (action === 'quarantine') summary.quarantined++;
|
|
392
|
+
else if (action === 'error') summary.errored++;
|
|
393
|
+
else summary.skipped++;
|
|
394
|
+
summary.reasons[reason] = (summary.reasons[reason] || 0) + 1;
|
|
395
|
+
}
|
|
396
|
+
return summary;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function normalizeCandidateLineage(candidate = {}) {
|
|
400
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
401
|
+
const sourceCanonicalKeys = Array.isArray(payload.sourceCanonicalKeys)
|
|
402
|
+
? payload.sourceCanonicalKeys.map(key => String(key || '')).filter(Boolean)
|
|
403
|
+
: [];
|
|
404
|
+
const rawSourceMemoryIds = Array.isArray(payload.sourceMemoryIds) ? payload.sourceMemoryIds : [];
|
|
405
|
+
const sourceMemoryIds = rawSourceMemoryIds
|
|
406
|
+
.filter(id => id !== null && id !== undefined)
|
|
407
|
+
.map(id => Number(id));
|
|
408
|
+
|
|
409
|
+
if (sourceMemoryIds.some(id => !Number.isSafeInteger(id) || id <= 0)) {
|
|
410
|
+
throw new Error('memory.consolidation.compaction_candidates requires positive integer sourceMemoryIds');
|
|
411
|
+
}
|
|
412
|
+
if (sourceMemoryIds.length !== sourceCanonicalKeys.length) {
|
|
413
|
+
throw new Error('memory.consolidation.compaction_candidates requires one sourceMemoryId for each sourceCanonicalKey');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { payload, sourceMemoryIds, sourceCanonicalKeys };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function classifySkippedRun(existingRun, plan) {
|
|
420
|
+
if (!existingRun) return 'claim_not_acquired';
|
|
421
|
+
const sameSnapshot = existingRun.input_hash === plan.inputHash;
|
|
422
|
+
if (sameSnapshot && existingRun.status === 'applied') return 'already_applied';
|
|
423
|
+
if (sameSnapshot && existingRun.status === 'applying') return 'already_claimed';
|
|
424
|
+
if (existingRun.status === 'applied' || existingRun.status === 'applying') return 'window_winner_exists';
|
|
425
|
+
return 'claim_not_acquired';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function planCompaction(records = [], opts = {}) {
|
|
429
|
+
const { cadence, periodStart, periodEnd, endMs } = requirePeriod(opts);
|
|
430
|
+
const normalized = records.map(normalizeRecord).sort((a, b) => {
|
|
431
|
+
if (a.canonicalKey !== b.canonicalKey) return a.canonicalKey.localeCompare(b.canonicalKey);
|
|
432
|
+
return String(a.id).localeCompare(String(b.id));
|
|
433
|
+
});
|
|
434
|
+
const inputHash = hashSnapshot({
|
|
435
|
+
cadence,
|
|
436
|
+
periodStart,
|
|
437
|
+
periodEnd,
|
|
438
|
+
policyVersion: opts.policyVersion || 'v1',
|
|
439
|
+
records: normalized,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const statusUpdates = [];
|
|
443
|
+
for (const record of normalized) {
|
|
444
|
+
if (record.status !== 'active') continue;
|
|
445
|
+
if (record.memoryType !== 'open_loop') continue;
|
|
446
|
+
const validTo = timeMs(record.validTo);
|
|
447
|
+
const staleAfter = timeMs(record.staleAfter);
|
|
448
|
+
if ((validTo !== null && validTo <= endMs) || (staleAfter !== null && staleAfter <= endMs)) {
|
|
449
|
+
statusUpdates.push({
|
|
450
|
+
memoryId: record.id,
|
|
451
|
+
canonicalKey: record.canonicalKey,
|
|
452
|
+
status: 'stale',
|
|
453
|
+
reason: validTo !== null && validTo <= endMs ? 'valid_to_elapsed' : 'stale_after_elapsed',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const candidates = buildAggregateCandidates(normalized, statusUpdates, {
|
|
458
|
+
...opts,
|
|
459
|
+
cadence,
|
|
460
|
+
periodStart,
|
|
461
|
+
periodEnd,
|
|
462
|
+
});
|
|
463
|
+
const coverage = buildCoverage(normalized, statusUpdates, candidates);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
cadence,
|
|
467
|
+
periodStart,
|
|
468
|
+
periodEnd,
|
|
469
|
+
policyVersion: opts.policyVersion || 'v1',
|
|
470
|
+
inputHash,
|
|
471
|
+
candidates,
|
|
472
|
+
statusUpdates,
|
|
473
|
+
sourceCoverage: coverage.sourceCoverage,
|
|
474
|
+
outputCoverage: coverage.outputCoverage,
|
|
475
|
+
meta: {
|
|
476
|
+
activeConflictRate: 0,
|
|
477
|
+
deterministic: true,
|
|
478
|
+
recordCount: normalized.length,
|
|
479
|
+
sourceCoverage: coverage.sourceCoverage,
|
|
480
|
+
outputCoverage: coverage.outputCoverage,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function distillArchiveSnapshot(snapshot = {}, opts = {}) {
|
|
486
|
+
const sessions = Array.isArray(snapshot.sessions) ? snapshot.sessions : [];
|
|
487
|
+
const candidates = [];
|
|
488
|
+
for (const session of sessions) {
|
|
489
|
+
const structuredSummary = session.structuredSummary || session.structured_summary || {};
|
|
490
|
+
const sessionId = session.sessionId || session.session_id || session.id || null;
|
|
491
|
+
const sourceRef = session.archiveRef || session.sourceRef || sessionId || 'archive';
|
|
492
|
+
const extracted = extractCandidatesFromStructuredSummary({
|
|
493
|
+
structuredSummary,
|
|
494
|
+
sessionId,
|
|
495
|
+
scopeKind: session.scopeKind || opts.scopeKind || 'session',
|
|
496
|
+
scopeKey: session.scopeKey || (sessionId ? `session:${sessionId}` : opts.scopeKey || 'archive'),
|
|
497
|
+
contextKey: session.contextKey || opts.contextKey || null,
|
|
498
|
+
topicKey: session.topicKey || opts.topicKey || null,
|
|
499
|
+
authority: opts.authority || 'raw_transcript',
|
|
500
|
+
evidenceRefs: [{
|
|
501
|
+
sourceKind: 'external',
|
|
502
|
+
sourceRef,
|
|
503
|
+
relationKind: 'imported_from',
|
|
504
|
+
metadata: { archive: true },
|
|
505
|
+
}],
|
|
506
|
+
});
|
|
507
|
+
for (const candidate of extracted) {
|
|
508
|
+
candidates.push({
|
|
509
|
+
...candidate,
|
|
510
|
+
status: 'candidate',
|
|
511
|
+
visibleInBootstrap: false,
|
|
512
|
+
visibleInRecall: false,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
inputHash: hashSnapshot(snapshot),
|
|
518
|
+
candidates: candidates.sort((a, b) => a.canonicalKey.localeCompare(b.canonicalKey)),
|
|
519
|
+
meta: {
|
|
520
|
+
candidateCount: candidates.length,
|
|
521
|
+
bypassedPromotion: false,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function createMemoryConsolidation({ pool, schema, defaultTenantId, records = null }) {
|
|
527
|
+
function makeApplyToken() {
|
|
528
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
529
|
+
return crypto.randomBytes(16).toString('hex');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function recordRunWith(queryable, input = {}) {
|
|
533
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
534
|
+
const plan = input.plan || input;
|
|
535
|
+
const sourceCoverage = input.sourceCoverage || plan.sourceCoverage || plan.meta?.sourceCoverage || {};
|
|
536
|
+
const outputCoverage = input.outputCoverage || plan.outputCoverage || plan.meta?.outputCoverage || {};
|
|
537
|
+
const result = await queryable.query(
|
|
538
|
+
`INSERT INTO ${schema}.compaction_runs (
|
|
539
|
+
tenant_id, cadence, period_start, period_end, input_hash,
|
|
540
|
+
policy_version, status, output, error, applied_at,
|
|
541
|
+
source_coverage, output_coverage
|
|
542
|
+
)
|
|
543
|
+
VALUES ($1,$2,$3,$4,$5,COALESCE($6,'v1'),COALESCE($7,'planned'),COALESCE($8::jsonb,'{}'::jsonb),$9,$10,
|
|
544
|
+
COALESCE($11::jsonb,'{}'::jsonb),COALESCE($12::jsonb,'{}'::jsonb))
|
|
545
|
+
ON CONFLICT (tenant_id, cadence, period_start, period_end, input_hash, policy_version)
|
|
546
|
+
DO UPDATE SET
|
|
547
|
+
status = CASE
|
|
548
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
549
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
550
|
+
THEN compaction_runs.status
|
|
551
|
+
ELSE EXCLUDED.status
|
|
552
|
+
END,
|
|
553
|
+
output = CASE
|
|
554
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
555
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
556
|
+
THEN compaction_runs.output
|
|
557
|
+
ELSE EXCLUDED.output
|
|
558
|
+
END,
|
|
559
|
+
error = CASE
|
|
560
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
561
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
562
|
+
THEN compaction_runs.error
|
|
563
|
+
ELSE EXCLUDED.error
|
|
564
|
+
END,
|
|
565
|
+
applied_at = CASE
|
|
566
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
567
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
568
|
+
THEN compaction_runs.applied_at
|
|
569
|
+
ELSE EXCLUDED.applied_at
|
|
570
|
+
END,
|
|
571
|
+
source_coverage = CASE
|
|
572
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
573
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
574
|
+
THEN compaction_runs.source_coverage
|
|
575
|
+
ELSE EXCLUDED.source_coverage
|
|
576
|
+
END,
|
|
577
|
+
output_coverage = CASE
|
|
578
|
+
WHEN compaction_runs.status IN ('applying','applied')
|
|
579
|
+
AND EXCLUDED.status <> compaction_runs.status
|
|
580
|
+
THEN compaction_runs.output_coverage
|
|
581
|
+
ELSE EXCLUDED.output_coverage
|
|
582
|
+
END
|
|
583
|
+
RETURNING *`,
|
|
584
|
+
[
|
|
585
|
+
tenantId,
|
|
586
|
+
plan.cadence,
|
|
587
|
+
plan.periodStart,
|
|
588
|
+
plan.periodEnd,
|
|
589
|
+
plan.inputHash,
|
|
590
|
+
plan.policyVersion || 'v1',
|
|
591
|
+
input.status || plan.status || 'planned',
|
|
592
|
+
JSON.stringify(input.output || plan),
|
|
593
|
+
input.error || null,
|
|
594
|
+
input.appliedAt || null,
|
|
595
|
+
JSON.stringify(sourceCoverage),
|
|
596
|
+
JSON.stringify(outputCoverage),
|
|
597
|
+
]
|
|
598
|
+
);
|
|
599
|
+
return result.rows[0] || null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function recordRun(input = {}) {
|
|
603
|
+
return recordRunWith(pool, input);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function claimRunWith(queryable, input = {}) {
|
|
607
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
608
|
+
const plan = input.plan || input;
|
|
609
|
+
const workerId = input.workerId || 'aquifer';
|
|
610
|
+
const applyToken = input.applyToken || makeApplyToken();
|
|
611
|
+
const claimLeaseSeconds = normalizeClaimLeaseSeconds(input.claimLeaseSeconds ?? input.staleAfterSeconds);
|
|
612
|
+
const [lockKey1, lockKey2] = advisoryLockKeys(
|
|
613
|
+
'aquifer.compaction_runs.claim_window',
|
|
614
|
+
`${schema}:${tenantId}:${plan.cadence}:${canonicalInstant(plan.periodStart)}:${canonicalInstant(plan.periodEnd)}:${plan.policyVersion || 'v1'}`,
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
await queryable.query('SELECT pg_advisory_xact_lock($1, $2)', [lockKey1, lockKey2]);
|
|
618
|
+
|
|
619
|
+
if (input.reclaimStaleClaims !== false) {
|
|
620
|
+
await queryable.query(
|
|
621
|
+
`UPDATE ${schema}.compaction_runs
|
|
622
|
+
SET status = 'failed',
|
|
623
|
+
error = COALESCE(error, 'claim lease expired before finalize'),
|
|
624
|
+
reclaimed_at = transaction_timestamp(),
|
|
625
|
+
reclaimed_by_worker_id = $6
|
|
626
|
+
WHERE tenant_id = $1
|
|
627
|
+
AND cadence = $2
|
|
628
|
+
AND period_start = $3
|
|
629
|
+
AND period_end = $4
|
|
630
|
+
AND policy_version = $5
|
|
631
|
+
AND status = 'applying'
|
|
632
|
+
AND lease_expires_at < transaction_timestamp()
|
|
633
|
+
RETURNING *`,
|
|
634
|
+
[
|
|
635
|
+
tenantId,
|
|
636
|
+
plan.cadence,
|
|
637
|
+
plan.periodStart,
|
|
638
|
+
plan.periodEnd,
|
|
639
|
+
plan.policyVersion || 'v1',
|
|
640
|
+
workerId,
|
|
641
|
+
]
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await recordRunWith(queryable, {
|
|
646
|
+
tenantId,
|
|
647
|
+
plan,
|
|
648
|
+
status: 'planned',
|
|
649
|
+
output: input.output || plan,
|
|
650
|
+
sourceCoverage: input.sourceCoverage || plan.sourceCoverage || plan.meta?.sourceCoverage || {},
|
|
651
|
+
outputCoverage: input.outputCoverage || plan.outputCoverage || plan.meta?.outputCoverage || {},
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const result = await queryable.query(
|
|
655
|
+
`UPDATE ${schema}.compaction_runs AS cr
|
|
656
|
+
SET status = 'applying',
|
|
657
|
+
claimed_at = transaction_timestamp(),
|
|
658
|
+
lease_expires_at = transaction_timestamp() + ($7::int * interval '1 second'),
|
|
659
|
+
worker_id = $8,
|
|
660
|
+
apply_token = $9
|
|
661
|
+
WHERE cr.tenant_id = $1
|
|
662
|
+
AND cr.cadence = $2
|
|
663
|
+
AND cr.period_start = $3
|
|
664
|
+
AND cr.period_end = $4
|
|
665
|
+
AND cr.input_hash = $5
|
|
666
|
+
AND cr.policy_version = $6
|
|
667
|
+
AND cr.status = 'planned'
|
|
668
|
+
AND NOT EXISTS (
|
|
669
|
+
SELECT 1
|
|
670
|
+
FROM ${schema}.compaction_runs other
|
|
671
|
+
WHERE other.tenant_id = cr.tenant_id
|
|
672
|
+
AND other.cadence = cr.cadence
|
|
673
|
+
AND other.period_start = cr.period_start
|
|
674
|
+
AND other.period_end = cr.period_end
|
|
675
|
+
AND other.policy_version = cr.policy_version
|
|
676
|
+
AND other.status IN ('applying','applied')
|
|
677
|
+
AND other.id <> cr.id
|
|
678
|
+
)
|
|
679
|
+
RETURNING *`,
|
|
680
|
+
[
|
|
681
|
+
tenantId,
|
|
682
|
+
plan.cadence,
|
|
683
|
+
plan.periodStart,
|
|
684
|
+
plan.periodEnd,
|
|
685
|
+
plan.inputHash,
|
|
686
|
+
plan.policyVersion || 'v1',
|
|
687
|
+
claimLeaseSeconds,
|
|
688
|
+
workerId,
|
|
689
|
+
applyToken,
|
|
690
|
+
]
|
|
691
|
+
);
|
|
692
|
+
return result.rows[0] || null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function claimRun(input = {}) {
|
|
696
|
+
if (pool && typeof pool.connect === 'function') {
|
|
697
|
+
const client = await pool.connect();
|
|
698
|
+
try {
|
|
699
|
+
await client.query('BEGIN');
|
|
700
|
+
const claim = await claimRunWith(client, input);
|
|
701
|
+
await client.query('COMMIT');
|
|
702
|
+
return claim;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
705
|
+
throw error;
|
|
706
|
+
} finally {
|
|
707
|
+
client.release();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return claimRunWith(pool, input);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function finalizeClaimWith(queryable, input = {}) {
|
|
714
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
715
|
+
const plan = input.plan || input;
|
|
716
|
+
const claim = input.claim || {};
|
|
717
|
+
const status = input.status || 'applied';
|
|
718
|
+
const sourceCoverage = input.sourceCoverage || plan.sourceCoverage || plan.meta?.sourceCoverage || {};
|
|
719
|
+
const outputCoverage = input.outputCoverage || plan.outputCoverage || plan.meta?.outputCoverage || {};
|
|
720
|
+
const result = await queryable.query(
|
|
721
|
+
`UPDATE ${schema}.compaction_runs
|
|
722
|
+
SET status = $4,
|
|
723
|
+
output = COALESCE($5::jsonb,'{}'::jsonb),
|
|
724
|
+
error = $6,
|
|
725
|
+
applied_at = $7,
|
|
726
|
+
source_coverage = COALESCE($8::jsonb,'{}'::jsonb),
|
|
727
|
+
output_coverage = COALESCE($9::jsonb,'{}'::jsonb)
|
|
728
|
+
WHERE tenant_id = $1
|
|
729
|
+
AND id = $2
|
|
730
|
+
AND apply_token = $3
|
|
731
|
+
AND status = 'applying'
|
|
732
|
+
RETURNING *`,
|
|
733
|
+
[
|
|
734
|
+
tenantId,
|
|
735
|
+
claim.id,
|
|
736
|
+
claim.apply_token || claim.applyToken || input.applyToken,
|
|
737
|
+
status,
|
|
738
|
+
JSON.stringify(input.output || plan),
|
|
739
|
+
input.error || null,
|
|
740
|
+
status === 'applied' ? (input.appliedAt || new Date().toISOString()) : null,
|
|
741
|
+
JSON.stringify(sourceCoverage),
|
|
742
|
+
JSON.stringify(outputCoverage),
|
|
743
|
+
]
|
|
744
|
+
);
|
|
745
|
+
return result.rows[0] || null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function recordCompactionCandidateResultsWith(queryable, input = {}) {
|
|
749
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
750
|
+
const run = input.run || input.claim || {};
|
|
751
|
+
const candidates = input.candidates || [];
|
|
752
|
+
const results = input.results || [];
|
|
753
|
+
const rows = [];
|
|
754
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
755
|
+
const candidate = candidates[i] || {};
|
|
756
|
+
const result = results[i] || {};
|
|
757
|
+
const { payload, sourceMemoryIds, sourceCanonicalKeys } = normalizeCandidateLineage(candidate);
|
|
758
|
+
const candidateHash = candidate.candidateHash || payload.candidateHash || hashSnapshot(candidate);
|
|
759
|
+
const inserted = await queryable.query(
|
|
760
|
+
`INSERT INTO ${schema}.compaction_candidates (
|
|
761
|
+
tenant_id, compaction_run_id, candidate_index, candidate_hash,
|
|
762
|
+
action, reason, memory_type, canonical_key, scope_kind, scope_key,
|
|
763
|
+
context_key, topic_key, summary, payload, source_memory_ids,
|
|
764
|
+
source_canonical_keys, memory_record_id, fact_assertion_id
|
|
765
|
+
)
|
|
766
|
+
VALUES (
|
|
767
|
+
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,COALESCE($14::jsonb,'{}'::jsonb),
|
|
768
|
+
COALESCE($15::bigint[], ARRAY[]::bigint[]), COALESCE($16::jsonb,'[]'::jsonb), $17,$18
|
|
769
|
+
)
|
|
770
|
+
ON CONFLICT (tenant_id, compaction_run_id, candidate_index)
|
|
771
|
+
DO UPDATE SET
|
|
772
|
+
candidate_hash = EXCLUDED.candidate_hash,
|
|
773
|
+
action = EXCLUDED.action,
|
|
774
|
+
reason = EXCLUDED.reason,
|
|
775
|
+
memory_type = EXCLUDED.memory_type,
|
|
776
|
+
canonical_key = EXCLUDED.canonical_key,
|
|
777
|
+
scope_kind = EXCLUDED.scope_kind,
|
|
778
|
+
scope_key = EXCLUDED.scope_key,
|
|
779
|
+
context_key = EXCLUDED.context_key,
|
|
780
|
+
topic_key = EXCLUDED.topic_key,
|
|
781
|
+
summary = EXCLUDED.summary,
|
|
782
|
+
payload = EXCLUDED.payload,
|
|
783
|
+
source_memory_ids = EXCLUDED.source_memory_ids,
|
|
784
|
+
source_canonical_keys = EXCLUDED.source_canonical_keys,
|
|
785
|
+
memory_record_id = COALESCE(EXCLUDED.memory_record_id, ${schema}.compaction_candidates.memory_record_id),
|
|
786
|
+
fact_assertion_id = COALESCE(EXCLUDED.fact_assertion_id, ${schema}.compaction_candidates.fact_assertion_id),
|
|
787
|
+
updated_at = now()
|
|
788
|
+
RETURNING *`,
|
|
789
|
+
[
|
|
790
|
+
tenantId,
|
|
791
|
+
run.id,
|
|
792
|
+
i,
|
|
793
|
+
candidateHash,
|
|
794
|
+
result.action || 'error',
|
|
795
|
+
result.reason || null,
|
|
796
|
+
candidate.memoryType || candidate.memory_type || null,
|
|
797
|
+
candidate.canonicalKey || candidate.canonical_key || null,
|
|
798
|
+
candidate.scopeKind || candidate.scope_kind || null,
|
|
799
|
+
candidate.scopeKey || candidate.scope_key || null,
|
|
800
|
+
candidate.contextKey || candidate.context_key || null,
|
|
801
|
+
candidate.topicKey || candidate.topic_key || null,
|
|
802
|
+
candidate.summary || null,
|
|
803
|
+
JSON.stringify(payload),
|
|
804
|
+
sourceMemoryIds,
|
|
805
|
+
JSON.stringify(sourceCanonicalKeys),
|
|
806
|
+
result.memory ? result.memory.id : null,
|
|
807
|
+
result.backingFact ? result.backingFact.id : null,
|
|
808
|
+
]
|
|
809
|
+
);
|
|
810
|
+
rows.push(inserted.rows[0] || null);
|
|
811
|
+
}
|
|
812
|
+
return rows;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function applyPlan(input = {}) {
|
|
816
|
+
const plan = input.plan || input;
|
|
817
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
818
|
+
const statusUpdates = Array.isArray(plan.statusUpdates) ? plan.statusUpdates : [];
|
|
819
|
+
const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
|
|
820
|
+
const appliedAt = input.appliedAt || new Date().toISOString();
|
|
821
|
+
const summary = createApplySummary(statusUpdates);
|
|
822
|
+
|
|
823
|
+
const applyWithRecords = async targetRecords => {
|
|
824
|
+
return applyStatusUpdatesWithRecords(statusUpdates, targetRecords, tenantId, summary);
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
if (!records || typeof records.updateMemoryStatusIfCurrent !== 'function') {
|
|
828
|
+
throw new Error('memory.consolidation.applyPlan requires records.updateMemoryStatusIfCurrent');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const runInput = status => {
|
|
832
|
+
const output = {
|
|
833
|
+
...plan,
|
|
834
|
+
applyResult: summary,
|
|
835
|
+
};
|
|
836
|
+
const outputCoverage = {
|
|
837
|
+
...(plan.outputCoverage || plan.meta?.outputCoverage || {}),
|
|
838
|
+
appliedStatusUpdateCount: summary.applied,
|
|
839
|
+
skippedStatusUpdateCount: summary.skipped,
|
|
840
|
+
unsupportedStatusUpdateCount: summary.unsupported,
|
|
841
|
+
plannedCandidateCount: candidates.length,
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
tenantId,
|
|
845
|
+
plan,
|
|
846
|
+
status,
|
|
847
|
+
output,
|
|
848
|
+
sourceCoverage: plan.sourceCoverage || plan.meta?.sourceCoverage || {},
|
|
849
|
+
outputCoverage,
|
|
850
|
+
appliedAt: status === 'applied' ? appliedAt : null,
|
|
851
|
+
};
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
if (pool && typeof pool.connect === 'function') {
|
|
855
|
+
const client = await pool.connect();
|
|
856
|
+
try {
|
|
857
|
+
await client.query('BEGIN');
|
|
858
|
+
const claim = await claimRunWith(client, {
|
|
859
|
+
tenantId,
|
|
860
|
+
plan,
|
|
861
|
+
workerId: input.workerId,
|
|
862
|
+
applyToken: input.applyToken,
|
|
863
|
+
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
864
|
+
staleAfterSeconds: input.staleAfterSeconds,
|
|
865
|
+
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
866
|
+
});
|
|
867
|
+
if (!claim) {
|
|
868
|
+
summary.skipped += statusUpdates.length;
|
|
869
|
+
await client.query('COMMIT');
|
|
870
|
+
return { status: 'skipped', run: null, claim: null, plan, applyResult: summary };
|
|
871
|
+
}
|
|
872
|
+
const txRecords = createMemoryRecords({
|
|
873
|
+
pool: client,
|
|
874
|
+
schema,
|
|
875
|
+
defaultTenantId,
|
|
876
|
+
inTransaction: true,
|
|
877
|
+
});
|
|
878
|
+
await applyWithRecords(txRecords);
|
|
879
|
+
const candidateRows = candidates.length > 0
|
|
880
|
+
? await recordCompactionCandidateResultsWith(client, {
|
|
881
|
+
tenantId,
|
|
882
|
+
run: claim,
|
|
883
|
+
candidates,
|
|
884
|
+
results: candidates.map(candidate => ({
|
|
885
|
+
candidate,
|
|
886
|
+
action: 'planned',
|
|
887
|
+
reason: 'promotion_not_requested',
|
|
888
|
+
})),
|
|
889
|
+
})
|
|
890
|
+
: [];
|
|
891
|
+
const status = summary.applied > 0 ? 'applied' : 'skipped';
|
|
892
|
+
const run = await finalizeClaimWith(client, {
|
|
893
|
+
...runInput(status),
|
|
894
|
+
claim,
|
|
895
|
+
});
|
|
896
|
+
if (!run) {
|
|
897
|
+
throw new Error('memory.consolidation.applyPlan failed to finalize claimed run');
|
|
898
|
+
}
|
|
899
|
+
await client.query('COMMIT');
|
|
900
|
+
return { status, run, claim, plan, applyResult: summary, candidateRows };
|
|
901
|
+
} catch (error) {
|
|
902
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
903
|
+
throw error;
|
|
904
|
+
} finally {
|
|
905
|
+
client.release();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (typeof records.withTransaction === 'function') {
|
|
910
|
+
await records.withTransaction(txRecords => applyWithRecords(txRecords));
|
|
911
|
+
} else {
|
|
912
|
+
await applyWithRecords(records);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const status = summary.applied > 0 ? 'applied' : 'skipped';
|
|
916
|
+
const run = await recordRun(runInput(status));
|
|
917
|
+
return { status, run, plan, applyResult: summary };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function executePlan(input = {}) {
|
|
921
|
+
const plan = input.plan || input;
|
|
922
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
923
|
+
const statusUpdates = Array.isArray(plan.statusUpdates) ? plan.statusUpdates : [];
|
|
924
|
+
const candidates = Array.isArray(input.candidates) ? input.candidates : (Array.isArray(plan.candidates) ? plan.candidates : []);
|
|
925
|
+
const appliedAt = input.appliedAt || new Date().toISOString();
|
|
926
|
+
const promoteCandidates = input.promoteCandidates === true;
|
|
927
|
+
const summary = createApplySummary(statusUpdates);
|
|
928
|
+
|
|
929
|
+
if (!records || typeof records.updateMemoryStatusIfCurrent !== 'function') {
|
|
930
|
+
throw new Error('memory.consolidation.executePlan requires records.updateMemoryStatusIfCurrent');
|
|
931
|
+
}
|
|
932
|
+
if (!pool || typeof pool.connect !== 'function') {
|
|
933
|
+
throw new Error('memory.consolidation.executePlan requires DB pool transaction support');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const runInput = (status, promotionResult, outputCandidates) => {
|
|
937
|
+
const output = {
|
|
938
|
+
...plan,
|
|
939
|
+
candidates: outputCandidates,
|
|
940
|
+
applyResult: summary,
|
|
941
|
+
promotionResult,
|
|
942
|
+
};
|
|
943
|
+
const outputCoverage = {
|
|
944
|
+
...(plan.outputCoverage || plan.meta?.outputCoverage || {}),
|
|
945
|
+
appliedStatusUpdateCount: summary.applied,
|
|
946
|
+
skippedStatusUpdateCount: summary.skipped,
|
|
947
|
+
unsupportedStatusUpdateCount: summary.unsupported,
|
|
948
|
+
promotionCandidateCount: promotionResult.candidates,
|
|
949
|
+
plannedCandidateCount: promotionResult.planned,
|
|
950
|
+
promotedCandidateCount: promotionResult.promoted,
|
|
951
|
+
quarantinedCandidateCount: promotionResult.quarantined,
|
|
952
|
+
erroredCandidateCount: promotionResult.errored,
|
|
953
|
+
};
|
|
954
|
+
return {
|
|
955
|
+
tenantId,
|
|
956
|
+
plan,
|
|
957
|
+
status,
|
|
958
|
+
output,
|
|
959
|
+
sourceCoverage: plan.sourceCoverage || plan.meta?.sourceCoverage || {},
|
|
960
|
+
outputCoverage,
|
|
961
|
+
appliedAt: status === 'applied' ? appliedAt : null,
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const client = await pool.connect();
|
|
966
|
+
try {
|
|
967
|
+
await client.query('BEGIN');
|
|
968
|
+
const claim = await claimRunWith(client, {
|
|
969
|
+
tenantId,
|
|
970
|
+
plan,
|
|
971
|
+
workerId: input.workerId,
|
|
972
|
+
applyToken: input.applyToken,
|
|
973
|
+
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
974
|
+
staleAfterSeconds: input.staleAfterSeconds,
|
|
975
|
+
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
976
|
+
});
|
|
977
|
+
if (!claim) {
|
|
978
|
+
summary.skipped += statusUpdates.length;
|
|
979
|
+
await client.query('COMMIT');
|
|
980
|
+
return {
|
|
981
|
+
status: 'skipped',
|
|
982
|
+
run: null,
|
|
983
|
+
claim: null,
|
|
984
|
+
plan,
|
|
985
|
+
applyResult: summary,
|
|
986
|
+
promotionResult: summarizePromotionResults([]),
|
|
987
|
+
candidateRows: [],
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const txRecords = createMemoryRecords({
|
|
992
|
+
pool: client,
|
|
993
|
+
schema,
|
|
994
|
+
defaultTenantId,
|
|
995
|
+
inTransaction: true,
|
|
996
|
+
});
|
|
997
|
+
await applyStatusUpdatesWithRecords(statusUpdates, txRecords, tenantId, summary);
|
|
998
|
+
|
|
999
|
+
const promotion = promoteCandidates ? createMemoryPromotion({ records: txRecords }) : null;
|
|
1000
|
+
const promotionResults = promoteCandidates && candidates.length > 0
|
|
1001
|
+
? await promotion.promote(candidates, {
|
|
1002
|
+
tenantId,
|
|
1003
|
+
acceptedAt: input.acceptedAt || appliedAt,
|
|
1004
|
+
createdByCompactionRunId: claim.id,
|
|
1005
|
+
})
|
|
1006
|
+
: candidates.map(candidate => ({
|
|
1007
|
+
candidate,
|
|
1008
|
+
action: 'planned',
|
|
1009
|
+
reason: 'promotion_not_requested',
|
|
1010
|
+
}));
|
|
1011
|
+
const candidateRows = await recordCompactionCandidateResultsWith(client, {
|
|
1012
|
+
tenantId,
|
|
1013
|
+
run: claim,
|
|
1014
|
+
candidates,
|
|
1015
|
+
results: promotionResults,
|
|
1016
|
+
});
|
|
1017
|
+
const promotionResult = summarizePromotionResults(promotionResults);
|
|
1018
|
+
const status = summary.applied > 0 || promotionResult.promoted > 0 || promotionResult.planned > 0
|
|
1019
|
+
? 'applied'
|
|
1020
|
+
: 'skipped';
|
|
1021
|
+
const run = await finalizeClaimWith(client, {
|
|
1022
|
+
...runInput(status, promotionResult, candidates),
|
|
1023
|
+
claim,
|
|
1024
|
+
});
|
|
1025
|
+
if (!run) {
|
|
1026
|
+
throw new Error('memory.consolidation.executePlan failed to finalize claimed run');
|
|
1027
|
+
}
|
|
1028
|
+
await client.query('COMMIT');
|
|
1029
|
+
return {
|
|
1030
|
+
status,
|
|
1031
|
+
run,
|
|
1032
|
+
claim,
|
|
1033
|
+
plan,
|
|
1034
|
+
applyResult: summary,
|
|
1035
|
+
promotionResult,
|
|
1036
|
+
promotionResults,
|
|
1037
|
+
candidateRows,
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
1041
|
+
throw error;
|
|
1042
|
+
} finally {
|
|
1043
|
+
client.release();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function loadActiveSnapshot(input = {}) {
|
|
1048
|
+
if (!records || typeof records.listActive !== 'function') {
|
|
1049
|
+
throw new Error('memory.consolidation.runJob requires records.listActive');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
1053
|
+
const scopeKeys = normalizeStringList(
|
|
1054
|
+
input.scopeKeys
|
|
1055
|
+
|| input.scopeKey
|
|
1056
|
+
|| input.activeScopeKey
|
|
1057
|
+
|| input.activeScopePath
|
|
1058
|
+
);
|
|
1059
|
+
const limit = normalizeOperatorSnapshotLimit(input.snapshotLimit ?? input.limit);
|
|
1060
|
+
const rows = await records.listActive({
|
|
1061
|
+
tenantId,
|
|
1062
|
+
scopeId: input.scopeId,
|
|
1063
|
+
scopeKeys: scopeKeys.length > 0 ? scopeKeys : undefined,
|
|
1064
|
+
asOf: input.snapshotAsOf || input.asOf || undefined,
|
|
1065
|
+
limit,
|
|
1066
|
+
});
|
|
1067
|
+
return {
|
|
1068
|
+
rows,
|
|
1069
|
+
scopeKeys,
|
|
1070
|
+
snapshotAsOf: input.snapshotAsOf || input.asOf || null,
|
|
1071
|
+
snapshotLimit: limit,
|
|
1072
|
+
snapshotTruncated: rows.length >= limit,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function findExistingRun(input = {}) {
|
|
1077
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
1078
|
+
const plan = input.plan || input;
|
|
1079
|
+
const result = await pool.query(
|
|
1080
|
+
`SELECT *
|
|
1081
|
+
FROM ${schema}.compaction_runs
|
|
1082
|
+
WHERE tenant_id = $1
|
|
1083
|
+
AND cadence = $2
|
|
1084
|
+
AND period_start = $3
|
|
1085
|
+
AND period_end = $4
|
|
1086
|
+
AND policy_version = $5
|
|
1087
|
+
ORDER BY CASE
|
|
1088
|
+
WHEN input_hash = $6 AND status = 'applied' THEN 0
|
|
1089
|
+
WHEN status = 'applied' THEN 1
|
|
1090
|
+
WHEN input_hash = $6 AND status = 'applying' THEN 2
|
|
1091
|
+
WHEN status = 'applying' THEN 3
|
|
1092
|
+
WHEN input_hash = $6 AND status = 'planned' THEN 4
|
|
1093
|
+
ELSE 5
|
|
1094
|
+
END,
|
|
1095
|
+
id DESC
|
|
1096
|
+
LIMIT 1`,
|
|
1097
|
+
[
|
|
1098
|
+
tenantId,
|
|
1099
|
+
plan.cadence,
|
|
1100
|
+
plan.periodStart,
|
|
1101
|
+
plan.periodEnd,
|
|
1102
|
+
plan.policyVersion || 'v1',
|
|
1103
|
+
plan.inputHash,
|
|
1104
|
+
]
|
|
1105
|
+
);
|
|
1106
|
+
return result.rows[0] || null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function runJob(input = {}) {
|
|
1110
|
+
const job = String(input.job || 'compaction').trim().toLowerCase();
|
|
1111
|
+
if (job === 'archive-distill') {
|
|
1112
|
+
if (input.apply === true || input.promoteCandidates === true) {
|
|
1113
|
+
throw new Error('memory.consolidation.runJob archive-distill is dry-run only');
|
|
1114
|
+
}
|
|
1115
|
+
const archiveSnapshot = input.archiveSnapshot || input.snapshot || null;
|
|
1116
|
+
if (!archiveSnapshot || typeof archiveSnapshot !== 'object') {
|
|
1117
|
+
throw new Error('memory.consolidation.runJob archive-distill requires archiveSnapshot');
|
|
1118
|
+
}
|
|
1119
|
+
const distill = distillArchiveSnapshot(archiveSnapshot, input);
|
|
1120
|
+
return {
|
|
1121
|
+
job,
|
|
1122
|
+
status: 'planned',
|
|
1123
|
+
dryRun: true,
|
|
1124
|
+
inputHash: distill.inputHash,
|
|
1125
|
+
candidates: distill.candidates,
|
|
1126
|
+
meta: distill.meta,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
1131
|
+
const window = resolveOperatorWindow(input);
|
|
1132
|
+
const snapshot = Array.isArray(input.records)
|
|
1133
|
+
? {
|
|
1134
|
+
rows: input.records,
|
|
1135
|
+
scopeKeys: normalizeStringList(
|
|
1136
|
+
input.scopeKeys
|
|
1137
|
+
|| input.scopeKey
|
|
1138
|
+
|| input.activeScopeKey
|
|
1139
|
+
|| input.activeScopePath
|
|
1140
|
+
),
|
|
1141
|
+
snapshotAsOf: input.snapshotAsOf || input.asOf || window.periodEnd,
|
|
1142
|
+
snapshotLimit: Array.isArray(input.records) ? input.records.length : null,
|
|
1143
|
+
snapshotTruncated: false,
|
|
1144
|
+
}
|
|
1145
|
+
: await loadActiveSnapshot({
|
|
1146
|
+
tenantId,
|
|
1147
|
+
scopeId: input.scopeId,
|
|
1148
|
+
scopeKeys: input.scopeKeys || input.scopeKey || input.activeScopePath || input.activeScopeKey,
|
|
1149
|
+
snapshotAsOf: input.snapshotAsOf || input.asOf || window.periodEnd,
|
|
1150
|
+
snapshotLimit: input.snapshotLimit ?? input.limit,
|
|
1151
|
+
});
|
|
1152
|
+
const plan = planCompaction(snapshot.rows, {
|
|
1153
|
+
tenantId,
|
|
1154
|
+
cadence: window.cadence,
|
|
1155
|
+
periodStart: window.periodStart,
|
|
1156
|
+
periodEnd: window.periodEnd,
|
|
1157
|
+
policyVersion: input.policyVersion || 'v1',
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
if (input.apply !== true) {
|
|
1161
|
+
return {
|
|
1162
|
+
job,
|
|
1163
|
+
status: 'planned',
|
|
1164
|
+
dryRun: true,
|
|
1165
|
+
plan,
|
|
1166
|
+
cadence: plan.cadence,
|
|
1167
|
+
periodStart: plan.periodStart,
|
|
1168
|
+
periodEnd: plan.periodEnd,
|
|
1169
|
+
snapshotCount: snapshot.rows.length,
|
|
1170
|
+
snapshotLimit: snapshot.snapshotLimit,
|
|
1171
|
+
snapshotTruncated: snapshot.snapshotTruncated,
|
|
1172
|
+
snapshotAsOf: snapshot.snapshotAsOf,
|
|
1173
|
+
scopeKeys: snapshot.scopeKeys,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const result = input.promoteCandidates === true
|
|
1178
|
+
? await executePlan({
|
|
1179
|
+
plan,
|
|
1180
|
+
tenantId,
|
|
1181
|
+
workerId: input.workerId,
|
|
1182
|
+
applyToken: input.applyToken,
|
|
1183
|
+
appliedAt: input.appliedAt,
|
|
1184
|
+
promoteCandidates: true,
|
|
1185
|
+
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
1186
|
+
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
1187
|
+
})
|
|
1188
|
+
: await applyPlan({
|
|
1189
|
+
plan,
|
|
1190
|
+
tenantId,
|
|
1191
|
+
workerId: input.workerId,
|
|
1192
|
+
applyToken: input.applyToken,
|
|
1193
|
+
appliedAt: input.appliedAt,
|
|
1194
|
+
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
1195
|
+
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
1196
|
+
});
|
|
1197
|
+
const existingRun = result.run || await findExistingRun({ tenantId, plan });
|
|
1198
|
+
return {
|
|
1199
|
+
...result,
|
|
1200
|
+
job,
|
|
1201
|
+
dryRun: false,
|
|
1202
|
+
cadence: plan.cadence,
|
|
1203
|
+
periodStart: plan.periodStart,
|
|
1204
|
+
periodEnd: plan.periodEnd,
|
|
1205
|
+
snapshotCount: snapshot.rows.length,
|
|
1206
|
+
snapshotLimit: snapshot.snapshotLimit,
|
|
1207
|
+
snapshotTruncated: snapshot.snapshotTruncated,
|
|
1208
|
+
snapshotAsOf: snapshot.snapshotAsOf,
|
|
1209
|
+
scopeKeys: snapshot.scopeKeys,
|
|
1210
|
+
existingRun,
|
|
1211
|
+
skipReason: result.run ? null : classifySkippedRun(existingRun, plan),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return {
|
|
1216
|
+
plan: planCompaction,
|
|
1217
|
+
distillArchiveSnapshot,
|
|
1218
|
+
runJob,
|
|
1219
|
+
recordRun,
|
|
1220
|
+
claimRun,
|
|
1221
|
+
applyPlan,
|
|
1222
|
+
executePlan,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
module.exports = {
|
|
1227
|
+
stableJson,
|
|
1228
|
+
hashSnapshot,
|
|
1229
|
+
advisoryLockKeys,
|
|
1230
|
+
canonicalInstant,
|
|
1231
|
+
normalizeClaimLeaseSeconds,
|
|
1232
|
+
resolveOperatorWindow,
|
|
1233
|
+
planCompaction,
|
|
1234
|
+
distillArchiveSnapshot,
|
|
1235
|
+
createMemoryConsolidation,
|
|
1236
|
+
};
|