@shadowforge0/aquifer-memory 1.8.1 → 1.9.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 +1 -0
- package/README.md +82 -26
- package/README_CN.md +33 -23
- package/README_TW.md +25 -24
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +587 -33
- package/consumers/codex-active-checkpoint.js +3 -1
- package/consumers/codex-current-memory.js +10 -6
- package/consumers/codex.js +6 -3
- package/consumers/default/daily-entries.js +2 -2
- package/consumers/default/index.js +40 -30
- package/consumers/default/prompts/summary.js +2 -2
- package/consumers/mcp.js +56 -46
- package/consumers/openclaw-ext/index.js +65 -7
- package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
- package/consumers/openclaw-ext/package.json +1 -1
- package/consumers/openclaw-install.js +326 -0
- package/consumers/openclaw-plugin.js +105 -24
- package/consumers/shared/compat-recall.js +101 -0
- package/consumers/shared/config.js +2 -0
- package/consumers/shared/openclaw-product-tools.js +130 -0
- package/consumers/shared/recall-format.js +2 -2
- package/core/aquifer.js +553 -41
- package/core/backends/local.js +169 -1
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/finalization-review.js +88 -42
- package/core/interface.js +629 -0
- package/core/mcp-manifest.js +11 -3
- package/core/memory-bootstrap.js +25 -27
- package/core/memory-consolidation.js +564 -42
- package/core/memory-explain.js +593 -0
- package/core/memory-promotion.js +392 -55
- package/core/memory-recall.js +75 -71
- package/core/memory-records.js +107 -108
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/memory-type-policy.js +298 -0
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +22 -0
- package/core/session-checkpoint-producer.js +3 -1
- package/core/session-checkpoints.js +1 -1
- package/core/session-finalization.js +78 -3
- package/core/storage.js +124 -8
- package/docs/getting-started.md +50 -4
- package/docs/setup.md +163 -24
- package/package.json +5 -4
- package/schema/004-completion.sql +4 -4
- package/schema/010-v1-finalization-review.sql +72 -0
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/schema/020-v1-assistant-shaping-memory.sql +30 -0
- package/scripts/backfill-canonical-key.js +1 -1
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
- package/scripts/diagnose-fts-zh.js +1 -1
- package/scripts/extract-insights-from-recent-sessions.js +4 -4
|
@@ -10,6 +10,49 @@ const OPERATOR_CADENCES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
|
10
10
|
const DEFAULT_CLAIM_LEASE_SECONDS = 600;
|
|
11
11
|
const DEFAULT_OPERATOR_SNAPSHOT_LIMIT = 1000;
|
|
12
12
|
const MAX_OPERATOR_SNAPSHOT_LIMIT = 5000;
|
|
13
|
+
const TEMPORAL_DISTILLATION_STANDARD_VERSION = 'temporal_distillation_standard_v1';
|
|
14
|
+
const ASSISTANT_BEHAVIOR_LANGUAGE_LEVELS = new Set(['user_behavior', 'assistant_behavior']);
|
|
15
|
+
const TEMPORAL_CADENCE_POLICY = {
|
|
16
|
+
daily: {
|
|
17
|
+
tier: 'daily',
|
|
18
|
+
windowKind: 'closed_utc_day',
|
|
19
|
+
outputSemantics: 'Carry forward still-current state, decisions, constraints, preferences, facts, conclusions, and open loops from a closed day.',
|
|
20
|
+
promotionRule: 'reviewed_synthesis_required',
|
|
21
|
+
aggregateCandidateRole: 'source_rollup_proposal',
|
|
22
|
+
promptGuidance: [
|
|
23
|
+
'Daily output may keep concrete same-day state and open loops only when they remain true after the day closes.',
|
|
24
|
+
'Do not promote transient test output, tool logs, progress narration, or resolved debugging paths.',
|
|
25
|
+
'Every returned item must include mergeKey, scopeClass, durability, and promotionTarget; temporal runtime state must include staleAfter or validTo.',
|
|
26
|
+
],
|
|
27
|
+
preferredMemoryTypes: ['state', 'open_loop', 'decision', 'constraint', 'preference', 'fact', 'conclusion'],
|
|
28
|
+
},
|
|
29
|
+
weekly: {
|
|
30
|
+
tier: 'weekly',
|
|
31
|
+
windowKind: 'closed_utc_week',
|
|
32
|
+
outputSemantics: 'Promote only cross-day patterns, durable decisions, continuing risks, direction, and unresolved open loops from a closed week.',
|
|
33
|
+
promotionRule: 'reviewed_synthesis_required',
|
|
34
|
+
aggregateCandidateRole: 'source_rollup_proposal',
|
|
35
|
+
promptGuidance: [
|
|
36
|
+
'Weekly output must not preserve one-off daily details unless they still affect future work.',
|
|
37
|
+
'Prefer durable decisions, constraints, preferences, stable facts, conclusions, risks, and still-open loops.',
|
|
38
|
+
'Do not return temporary runtime state unless it has an explicit expiry and still affects future work.',
|
|
39
|
+
],
|
|
40
|
+
preferredMemoryTypes: ['decision', 'constraint', 'preference', 'fact', 'conclusion', 'open_loop'],
|
|
41
|
+
},
|
|
42
|
+
monthly: {
|
|
43
|
+
tier: 'monthly',
|
|
44
|
+
windowKind: 'closed_utc_month',
|
|
45
|
+
outputSemantics: 'Keep only long-term principles, stable preferences, durable constraints, stable facts, architectural direction, and long-lived open loops from a closed month.',
|
|
46
|
+
promotionRule: 'reviewed_synthesis_required',
|
|
47
|
+
aggregateCandidateRole: 'source_rollup_proposal',
|
|
48
|
+
promptGuidance: [
|
|
49
|
+
'Monthly output must drop daily operational detail and stale project state.',
|
|
50
|
+
'Only durable principles, stable preferences, constraints, facts, direction, and long-lived open loops should remain.',
|
|
51
|
+
'Monthly output should not return workspace policy, process state, or one-off operator progress.',
|
|
52
|
+
],
|
|
53
|
+
preferredMemoryTypes: ['constraint', 'preference', 'decision', 'fact', 'open_loop'],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
13
56
|
|
|
14
57
|
function stableJson(value) {
|
|
15
58
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
@@ -186,6 +229,27 @@ function aggregateCandidateCadence(cadence) {
|
|
|
186
229
|
return cadence === 'daily' || cadence === 'weekly' || cadence === 'monthly';
|
|
187
230
|
}
|
|
188
231
|
|
|
232
|
+
function temporalPolicyForCadence(cadence) {
|
|
233
|
+
return TEMPORAL_CADENCE_POLICY[cadence] || null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isAggregateRollupCandidate(candidate = {}) {
|
|
237
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
238
|
+
return payload.kind === 'compaction_rollup'
|
|
239
|
+
|| payload.aggregateCandidateRole === 'source_rollup_proposal';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function assertPromotableTemporalCandidates(candidates = [], input = {}) {
|
|
243
|
+
if (input.promoteCandidates !== true) return;
|
|
244
|
+
const aggregateCandidates = candidates.filter(isAggregateRollupCandidate);
|
|
245
|
+
if (aggregateCandidates.length === 0) return;
|
|
246
|
+
if (input.unsafePromoteAggregateCandidates === true) return;
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Temporal aggregate rollup candidates are ledger/prompt material only. '
|
|
249
|
+
+ 'Attach a reviewed timer synthesis summary before promoting temporal candidates.'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
189
253
|
function safeKeyPart(value, fallback) {
|
|
190
254
|
const text = String(value || fallback || '').trim().toLowerCase();
|
|
191
255
|
return text.replace(/\s+/g, '-').replace(/[^a-z0-9:._/-]/g, '-');
|
|
@@ -197,9 +261,272 @@ function compactSummary(record) {
|
|
|
197
261
|
return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
|
|
198
262
|
}
|
|
199
263
|
|
|
264
|
+
function normalizeClassifierValue(value) {
|
|
265
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9:_./-]/g, '');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeSummaryKey(value) {
|
|
269
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function payloadValue(payload = {}, ...keys) {
|
|
273
|
+
for (const key of keys) {
|
|
274
|
+
if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') return payload[key];
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function extractAbsolutePaths(text) {
|
|
280
|
+
return String(text || '').match(/\/(?:home|mnt|etc|var|tmp|opt|root|usr)\/[^\s"'<>),]+/g) || [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizePathPrefix(value) {
|
|
284
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function detectScopeClassFromContent(candidate = {}, opts = {}) {
|
|
288
|
+
const text = normalizeSummaryKey(`${candidate.title || ''} ${candidate.summary || ''}`);
|
|
289
|
+
const productRepoPaths = normalizeStringList(
|
|
290
|
+
opts.productRepoPaths || opts.productRepoPath || opts.repoPath || opts.allowedProductPath,
|
|
291
|
+
).map(normalizePathPrefix).filter(Boolean);
|
|
292
|
+
const paths = extractAbsolutePaths(text);
|
|
293
|
+
if (paths.some(path => /(^|\/)(agents\.md|\.codex|\.openclaw)(\/|$)/.test(path.toLowerCase()))) {
|
|
294
|
+
return 'workspace_policy';
|
|
295
|
+
}
|
|
296
|
+
if (productRepoPaths.length > 0 && paths.some(path => (
|
|
297
|
+
!productRepoPaths.some(prefix => path === prefix || path.startsWith(`${prefix}/`))
|
|
298
|
+
))) {
|
|
299
|
+
return 'workspace_policy';
|
|
300
|
+
}
|
|
301
|
+
if (/\bdirty worktree\b|\brevert\b.*\b(current task|user changes|outside the current task)\b/.test(text)) {
|
|
302
|
+
return 'workspace_policy';
|
|
303
|
+
}
|
|
304
|
+
if (/\b(restart|running process|mcp process|gateway|latest|published|release|tag|npm latest|github release|local install|runtime)\b/.test(text)) {
|
|
305
|
+
return 'runtime_state';
|
|
306
|
+
}
|
|
307
|
+
if (/\b(package surface|evidence_items|memory_records|postgresql|schema|query contract|release gate)\b/.test(text)) {
|
|
308
|
+
return 'product_memory';
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function inferScopeClass(candidate = {}, opts = {}) {
|
|
314
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
315
|
+
const explicit = normalizeClassifierValue(payloadValue(payload, 'scopeClass', 'scope_class'));
|
|
316
|
+
const detected = detectScopeClassFromContent(candidate, opts);
|
|
317
|
+
if (detected === 'workspace_policy') return detected;
|
|
318
|
+
if (explicit) return explicit;
|
|
319
|
+
return detected || 'product_memory';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inferDurability(candidate = {}, scopeClass) {
|
|
323
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
324
|
+
const explicit = normalizeClassifierValue(payloadValue(payload, 'durability'));
|
|
325
|
+
if (explicit) return explicit;
|
|
326
|
+
if (scopeClass === 'runtime_state') return 'temporal';
|
|
327
|
+
const memoryType = normalizeClassifierValue(candidate.memoryType || candidate.memory_type);
|
|
328
|
+
if (memoryType === 'open_loop') return 'bounded';
|
|
329
|
+
return 'durable';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function candidateExpiry(candidate = {}) {
|
|
333
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
334
|
+
const staleness = payload.staleness && typeof payload.staleness === 'object' ? payload.staleness : {};
|
|
335
|
+
return payloadValue(candidate, 'staleAfter', 'stale_after', 'validTo', 'valid_to')
|
|
336
|
+
|| payloadValue(payload, 'staleAfter', 'stale_after', 'validTo', 'valid_to', 'expiresAt', 'expires_at')
|
|
337
|
+
|| payloadValue(staleness, 'staleAfter', 'stale_after', 'validTo', 'valid_to', 'expiresAt', 'expires_at');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function payloadObject(payload = {}, ...keys) {
|
|
341
|
+
for (const key of keys) {
|
|
342
|
+
const value = payload[key];
|
|
343
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
344
|
+
}
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function explicitBoolean(value) {
|
|
349
|
+
if (value === true || value === false) return value;
|
|
350
|
+
if (typeof value === 'string') {
|
|
351
|
+
const normalized = value.trim().toLowerCase();
|
|
352
|
+
if (normalized === 'true') return true;
|
|
353
|
+
if (normalized === 'false') return false;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function assistantBehaviorAbstraction(payload = {}) {
|
|
359
|
+
const abstraction = payloadObject(payload, 'abstraction', 'generalization', 'assistantBehaviorAbstraction');
|
|
360
|
+
const appliesBeyondSource = explicitBoolean(
|
|
361
|
+
abstraction.appliesBeyondSource
|
|
362
|
+
?? abstraction.applies_beyond_source
|
|
363
|
+
?? payload.appliesBeyondSource
|
|
364
|
+
?? payload.applies_beyond_source,
|
|
365
|
+
);
|
|
366
|
+
const sourceBound = explicitBoolean(
|
|
367
|
+
abstraction.sourceBound
|
|
368
|
+
?? abstraction.source_bound
|
|
369
|
+
?? payload.sourceBound
|
|
370
|
+
?? payload.source_bound,
|
|
371
|
+
);
|
|
372
|
+
const principle = String(
|
|
373
|
+
abstraction.principle
|
|
374
|
+
|| abstraction.behaviorPrinciple
|
|
375
|
+
|| abstraction.behavior_principle
|
|
376
|
+
|| payload.behaviorPrinciple
|
|
377
|
+
|| payload.behavior_principle
|
|
378
|
+
|| '',
|
|
379
|
+
).trim();
|
|
380
|
+
const languageLevel = normalizeClassifierValue(
|
|
381
|
+
abstraction.languageLevel
|
|
382
|
+
|| abstraction.language_level
|
|
383
|
+
|| payload.languageLevel
|
|
384
|
+
|| payload.language_level,
|
|
385
|
+
);
|
|
386
|
+
const generalized = appliesBeyondSource === true && sourceBound === false && Boolean(principle);
|
|
387
|
+
const behaviorLanguage = ASSISTANT_BEHAVIOR_LANGUAGE_LEVELS.has(languageLevel);
|
|
388
|
+
return {
|
|
389
|
+
appliesBeyondSource,
|
|
390
|
+
sourceBound,
|
|
391
|
+
principle,
|
|
392
|
+
languageLevel,
|
|
393
|
+
declared: appliesBeyondSource !== null || sourceBound !== null || Boolean(principle) || Boolean(languageLevel),
|
|
394
|
+
generalized,
|
|
395
|
+
behaviorLanguage,
|
|
396
|
+
eligible: generalized && behaviorLanguage,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function evaluateTemporalDistillationCandidate(candidate = {}, opts = {}) {
|
|
401
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
402
|
+
const cadence = normalizeClassifierValue(opts.cadence || candidate.payload?.cadence || 'manual');
|
|
403
|
+
const memoryType = normalizeClassifierValue(candidate.memoryType || candidate.memory_type);
|
|
404
|
+
const scopeClass = inferScopeClass(candidate, opts);
|
|
405
|
+
const promotionTarget = normalizeClassifierValue(payloadValue(payload, 'promotionTarget', 'promotion_target'))
|
|
406
|
+
|| (memoryType === 'assistant_shaping'
|
|
407
|
+
? 'assistant_behavior_memory'
|
|
408
|
+
: (scopeClass === 'product_memory' || scopeClass === 'runtime_state' ? 'project_current_memory' : scopeClass));
|
|
409
|
+
const durability = inferDurability(candidate, scopeClass);
|
|
410
|
+
const mergeKey = normalizeClassifierValue(payloadValue(payload, 'mergeKey', 'merge_key'));
|
|
411
|
+
const expiry = candidateExpiry(candidate);
|
|
412
|
+
const reasons = [];
|
|
413
|
+
const sourceCanonicalKeys = Array.isArray(candidate.sourceCanonicalKeys) ? candidate.sourceCanonicalKeys : [];
|
|
414
|
+
const sourceMemoryIds = Array.isArray(candidate.sourceMemoryIds) ? candidate.sourceMemoryIds : [];
|
|
415
|
+
const assistantBehaviorTarget = memoryType === 'assistant_shaping' && promotionTarget === 'assistant_behavior_memory';
|
|
416
|
+
const projectMemoryTarget = promotionTarget === 'project_current_memory';
|
|
417
|
+
|
|
418
|
+
if (!mergeKey) reasons.push('missing_merge_key');
|
|
419
|
+
if (candidate.sourceLineageError) reasons.push(candidate.sourceLineageError);
|
|
420
|
+
if (opts.requireSourceLineage === true && sourceCanonicalKeys.length === 0) reasons.push('missing_source_lineage');
|
|
421
|
+
if (sourceMemoryIds.length !== sourceCanonicalKeys.length) reasons.push('invalid_source_lineage');
|
|
422
|
+
if (scopeClass === 'workspace_policy' || promotionTarget === 'workspace_policy') reasons.push('workspace_policy_not_project_memory');
|
|
423
|
+
if (!assistantBehaviorTarget && !projectMemoryTarget) reasons.push('unsupported_promotion_target');
|
|
424
|
+
if (memoryType !== 'assistant_shaping' && promotionTarget === 'assistant_behavior_memory') {
|
|
425
|
+
reasons.push('assistant_behavior_target_requires_assistant_shaping');
|
|
426
|
+
}
|
|
427
|
+
if (assistantBehaviorTarget) {
|
|
428
|
+
const abstraction = assistantBehaviorAbstraction(payload);
|
|
429
|
+
if (!abstraction.generalized) reasons.push('assistant_behavior_requires_generalized_abstraction');
|
|
430
|
+
if (!abstraction.behaviorLanguage) reasons.push('assistant_behavior_requires_behavior_level_language');
|
|
431
|
+
}
|
|
432
|
+
if (durability === 'transient') reasons.push('transient_memory_not_promotable');
|
|
433
|
+
if (scopeClass === 'runtime_state' && !expiry) reasons.push('runtime_state_requires_expiry');
|
|
434
|
+
if ((cadence === 'weekly' || cadence === 'monthly') && scopeClass === 'runtime_state') reasons.push('runtime_state_not_promotable_for_cadence');
|
|
435
|
+
if (cadence === 'monthly' && memoryType === 'state') reasons.push('monthly_state_requires_long_lived_type');
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
eligible: reasons.length === 0,
|
|
439
|
+
reasons,
|
|
440
|
+
standardVersion: TEMPORAL_DISTILLATION_STANDARD_VERSION,
|
|
441
|
+
mergeKey: mergeKey || `derived:${memoryType}:${hashSnapshot(candidate.summary || candidate.title || candidate.canonicalKey || '')}`,
|
|
442
|
+
scopeClass,
|
|
443
|
+
promotionTarget,
|
|
444
|
+
durability,
|
|
445
|
+
staleAfter: payloadValue(payload, 'staleAfter', 'stale_after') || payloadValue(candidate, 'staleAfter', 'stale_after') || null,
|
|
446
|
+
validTo: payloadValue(payload, 'validTo', 'valid_to') || payloadValue(candidate, 'validTo', 'valid_to') || null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function temporalCandidatePriority(candidate = {}) {
|
|
451
|
+
const type = normalizeClassifierValue(candidate.memoryType || candidate.memory_type);
|
|
452
|
+
const priority = {
|
|
453
|
+
constraint: 80,
|
|
454
|
+
decision: 75,
|
|
455
|
+
fact: 70,
|
|
456
|
+
preference: 65,
|
|
457
|
+
open_loop: 60,
|
|
458
|
+
conclusion: 50,
|
|
459
|
+
state: 40,
|
|
460
|
+
entity_note: 30,
|
|
461
|
+
};
|
|
462
|
+
return priority[type] || 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function summarizeSafetyGate(safetyGate = {}) {
|
|
466
|
+
const stats = safetyGate.stats && typeof safetyGate.stats === 'object' ? safetyGate.stats : safetyGate;
|
|
467
|
+
return {
|
|
468
|
+
redacted: Number(stats.redacted || 0),
|
|
469
|
+
dropped: Number(stats.dropped || 0),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function applyTemporalDistillationStandard(candidates = [], opts = {}) {
|
|
474
|
+
const acceptedByMergeKey = new Map();
|
|
475
|
+
const rejected = [];
|
|
476
|
+
const acceptedOrder = [];
|
|
477
|
+
|
|
478
|
+
for (const candidate of candidates) {
|
|
479
|
+
const evaluation = evaluateTemporalDistillationCandidate(candidate, opts);
|
|
480
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
481
|
+
const next = {
|
|
482
|
+
...candidate,
|
|
483
|
+
staleAfter: candidate.staleAfter || candidate.stale_after || evaluation.staleAfter || null,
|
|
484
|
+
validTo: candidate.validTo || candidate.valid_to || evaluation.validTo || null,
|
|
485
|
+
payload: {
|
|
486
|
+
...payload,
|
|
487
|
+
distillation: {
|
|
488
|
+
standardVersion: evaluation.standardVersion,
|
|
489
|
+
mergeKey: evaluation.mergeKey,
|
|
490
|
+
scopeClass: evaluation.scopeClass,
|
|
491
|
+
promotionTarget: evaluation.promotionTarget,
|
|
492
|
+
durability: evaluation.durability,
|
|
493
|
+
staleAfter: evaluation.staleAfter,
|
|
494
|
+
validTo: evaluation.validTo,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
if (!evaluation.eligible) {
|
|
499
|
+
rejected.push({ candidate: next, reasons: evaluation.reasons });
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const existing = acceptedByMergeKey.get(evaluation.mergeKey);
|
|
504
|
+
if (!existing) {
|
|
505
|
+
acceptedByMergeKey.set(evaluation.mergeKey, next);
|
|
506
|
+
acceptedOrder.push(evaluation.mergeKey);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const incomingPriority = temporalCandidatePriority(next);
|
|
511
|
+
const existingPriority = temporalCandidatePriority(existing);
|
|
512
|
+
if (incomingPriority > existingPriority) {
|
|
513
|
+
acceptedByMergeKey.set(evaluation.mergeKey, next);
|
|
514
|
+
rejected.push({ candidate: existing, reasons: ['duplicate_merge_key_superseded'] });
|
|
515
|
+
} else {
|
|
516
|
+
rejected.push({ candidate: next, reasons: ['duplicate_merge_key'] });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
candidates: acceptedOrder.map(key => acceptedByMergeKey.get(key)).filter(Boolean),
|
|
522
|
+
rejected,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
200
526
|
function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
201
527
|
const { cadence, periodStart, periodEnd } = opts;
|
|
202
528
|
if (!aggregateCandidateCadence(cadence)) return [];
|
|
529
|
+
const temporalPolicy = temporalPolicyForCadence(cadence);
|
|
203
530
|
|
|
204
531
|
const staleIds = new Set(statusUpdates.map(update => String(update.memoryId)));
|
|
205
532
|
const staleKeys = new Set(statusUpdates.map(update => String(update.canonicalKey || '')));
|
|
@@ -298,8 +625,11 @@ function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
|
298
625
|
payload: {
|
|
299
626
|
kind: 'compaction_rollup',
|
|
300
627
|
synthesisKind: 'timer_current_memory_synthesis_v1',
|
|
301
|
-
currentMemoryRole: `${cadence}
|
|
302
|
-
|
|
628
|
+
currentMemoryRole: `${cadence}_source_rollup_proposal`,
|
|
629
|
+
temporalTier: cadence,
|
|
630
|
+
temporalPolicy,
|
|
631
|
+
aggregateCandidateRole: 'source_rollup_proposal',
|
|
632
|
+
promotionGate: 'reviewed_synthesis_required',
|
|
303
633
|
cadence,
|
|
304
634
|
policyVersion,
|
|
305
635
|
periodStart: windowStart,
|
|
@@ -324,8 +654,8 @@ function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
|
324
654
|
canonicalKey: record.canonicalKey,
|
|
325
655
|
},
|
|
326
656
|
})),
|
|
327
|
-
visibleInBootstrap:
|
|
328
|
-
visibleInRecall:
|
|
657
|
+
visibleInBootstrap: false,
|
|
658
|
+
visibleInRecall: false,
|
|
329
659
|
});
|
|
330
660
|
}
|
|
331
661
|
|
|
@@ -335,6 +665,7 @@ function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
|
335
665
|
function buildTimerSynthesisInput(normalized, statusUpdates, candidates, opts) {
|
|
336
666
|
const { cadence, periodStart, periodEnd } = opts;
|
|
337
667
|
if (!aggregateCandidateCadence(cadence)) return null;
|
|
668
|
+
const temporalPolicy = temporalPolicyForCadence(cadence);
|
|
338
669
|
|
|
339
670
|
const staleIds = new Set(statusUpdates.map(update => String(update.memoryId)));
|
|
340
671
|
const staleKeys = new Set(statusUpdates.map(update => String(update.canonicalKey || '')));
|
|
@@ -373,8 +704,10 @@ function buildTimerSynthesisInput(normalized, statusUpdates, candidates, opts) {
|
|
|
373
704
|
periodEnd: windowEnd,
|
|
374
705
|
promotion: {
|
|
375
706
|
default: 'candidate_only',
|
|
376
|
-
requires: 'apply=true
|
|
707
|
+
requires: 'apply=true, promoteCandidates=true, and reviewed synthesis summary',
|
|
708
|
+
aggregateCandidatePromotion: 'blocked_by_default',
|
|
377
709
|
},
|
|
710
|
+
temporalPolicy,
|
|
378
711
|
guards: {
|
|
379
712
|
rawTranscriptExcluded: true,
|
|
380
713
|
sessionSummariesExcluded: true,
|
|
@@ -419,14 +752,26 @@ function buildTimerSynthesisPrompt(plan = {}, opts = {}) {
|
|
|
419
752
|
throw new Error('memory.consolidation.timer_synthesis_prompt requires a timer synthesisInput');
|
|
420
753
|
}
|
|
421
754
|
const maxFacts = opts.maxFacts || 12;
|
|
755
|
+
const temporalPolicy = synthesisInput.temporalPolicy || temporalPolicyForCadence(synthesisInput.cadence) || {};
|
|
756
|
+
const policyGuidance = Array.isArray(temporalPolicy.promptGuidance)
|
|
757
|
+
? temporalPolicy.promptGuidance
|
|
758
|
+
: [];
|
|
422
759
|
return [
|
|
423
760
|
'You are producing an Aquifer timer current-memory synthesis proposal.',
|
|
424
761
|
'Use only the <timer_synthesis_input> block. Do not read raw transcripts, session summaries, tool output, or debug material.',
|
|
425
762
|
'This is a producer proposal, not an active memory commit. Promotion still requires the normal operator promotion gate.',
|
|
426
|
-
|
|
427
|
-
|
|
763
|
+
`Temporal tier: ${temporalPolicy.tier || synthesisInput.cadence || 'unknown'}.`,
|
|
764
|
+
`Temporal semantics: ${temporalPolicy.outputSemantics || 'Keep only memory that remains useful after the closed timer window.'}`,
|
|
765
|
+
...policyGuidance.map(line => `Policy: ${line}`),
|
|
766
|
+
'Return compact JSON with this shape. Every item must include mergeKey, scopeClass, durability, promotionTarget, and sourceCanonicalKeys; temporal runtime state also needs staleAfter or validTo:',
|
|
767
|
+
'{"summaryText":"...","structuredSummary":{"assistant_shaping":[{"guidance":"...","shapingKind":"memory_policy","servingImpact":"changes_memory_promotion","userRelevance":"...","temporalSupport":{"observationCount":2},"abstraction":{"languageLevel":"user_behavior","appliesBeyondSource":true,"sourceBound":false,"principle":"..."},"mergeKey":"...","scopeClass":"assistant_behavior","durability":"durable","promotionTarget":"assistant_behavior_memory","sourceCanonicalKeys":["memory:canonical:key"]}],"facts":[{"statement":"...","subject":"...","aspect":"...","mergeKey":"...","scopeClass":"product_memory","durability":"durable","promotionTarget":"project_current_memory","sourceCanonicalKeys":["memory:canonical:key"]}],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
|
|
428
768
|
`Keep facts/states/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
|
|
429
769
|
'Mark uncertain, resolved, superseded, revoked, or stale items explicitly in payload fields when applicable.',
|
|
770
|
+
'Use promotionTarget="project_current_memory" only for product-scoped memory that should be eligible for current-memory promotion.',
|
|
771
|
+
'Use assistant_shaping with promotionTarget="assistant_behavior_memory" only for durable assistant behavior that changes future responses, retrieval, tool routing, or memory promotion for the user.',
|
|
772
|
+
'For assistant_behavior_memory, put project-specific evidence only in sourceCanonicalKeys and abstraction context; guidance must state the generalized assistant behavior, and abstraction must set languageLevel="user_behavior", appliesBeyondSource=true, sourceBound=false, and principle.',
|
|
773
|
+
'Use scopeClass="workspace_policy" or promotionTarget="workspace_policy" for workspace/operator rules; those will stay out of project current memory.',
|
|
774
|
+
'Use the same mergeKey for duplicate claims across memory types so the deterministic gate can keep one candidate.',
|
|
430
775
|
'Do not copy sourceCurrentMemory unchanged unless the timer window confirms it still carries forward.',
|
|
431
776
|
'',
|
|
432
777
|
'<timer_synthesis_input>',
|
|
@@ -504,6 +849,93 @@ function sourceLineageFromSynthesisInput(synthesisInput = {}) {
|
|
|
504
849
|
};
|
|
505
850
|
}
|
|
506
851
|
|
|
852
|
+
function sourceLookupFromSynthesisInput(synthesisInput = {}) {
|
|
853
|
+
const rows = Array.isArray(synthesisInput.sourceCurrentMemory) ? synthesisInput.sourceCurrentMemory : [];
|
|
854
|
+
const byKey = new Map();
|
|
855
|
+
const byId = new Map();
|
|
856
|
+
for (const row of rows) {
|
|
857
|
+
const id = Number(row.memoryId);
|
|
858
|
+
const key = String(row.canonicalKey || '').trim();
|
|
859
|
+
if (!Number.isSafeInteger(id) || id <= 0 || !key) continue;
|
|
860
|
+
const pair = { id, key };
|
|
861
|
+
byKey.set(key, pair);
|
|
862
|
+
byId.set(id, pair);
|
|
863
|
+
}
|
|
864
|
+
return { byKey, byId };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function assistantBehaviorPromotionTarget(payload = {}) {
|
|
868
|
+
return normalizeClassifierValue(payloadValue(payload, 'promotionTarget', 'promotion_target'))
|
|
869
|
+
|| normalizeClassifierValue(payloadObject(payload, 'distillation').promotionTarget || payloadObject(payload, 'distillation').promotion_target);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function normalizeAssistantBehaviorServingText(candidate = {}) {
|
|
873
|
+
if ((candidate.memoryType || candidate.memory_type) !== 'assistant_shaping') return candidate;
|
|
874
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
875
|
+
if (assistantBehaviorPromotionTarget(payload) !== 'assistant_behavior_memory') return candidate;
|
|
876
|
+
const abstraction = assistantBehaviorAbstraction(payload);
|
|
877
|
+
if (!abstraction.eligible) return candidate;
|
|
878
|
+
const sourceGuidance = String(payload.guidance || '').trim();
|
|
879
|
+
const nextPayload = {
|
|
880
|
+
...payload,
|
|
881
|
+
guidance: abstraction.principle,
|
|
882
|
+
};
|
|
883
|
+
if (sourceGuidance && sourceGuidance !== abstraction.principle) {
|
|
884
|
+
nextPayload.sourceGuidance = sourceGuidance;
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
...candidate,
|
|
888
|
+
title: abstraction.principle.slice(0, 120),
|
|
889
|
+
summary: abstraction.principle,
|
|
890
|
+
payload: nextPayload,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function candidatePayloadWithoutLineage(payload = {}) {
|
|
895
|
+
const rest = { ...payload };
|
|
896
|
+
delete rest.sourceMemoryIds;
|
|
897
|
+
delete rest.source_memory_ids;
|
|
898
|
+
delete rest.sourceCanonicalKeys;
|
|
899
|
+
delete rest.source_canonical_keys;
|
|
900
|
+
delete rest.sources;
|
|
901
|
+
delete rest.sourceKeys;
|
|
902
|
+
delete rest.source_keys;
|
|
903
|
+
return rest;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function resolveCandidateSourceLineage(payload = {}, lookup = {}) {
|
|
907
|
+
const keys = normalizeStringList(
|
|
908
|
+
payload.sourceCanonicalKeys || payload.source_canonical_keys || payload.sourceKeys || payload.source_keys,
|
|
909
|
+
);
|
|
910
|
+
const ids = normalizeStringList(payload.sourceMemoryIds || payload.source_memory_ids)
|
|
911
|
+
.map(value => Number(value))
|
|
912
|
+
.filter(value => Number.isSafeInteger(value) && value > 0);
|
|
913
|
+
const pairs = new Map();
|
|
914
|
+
for (const key of keys) {
|
|
915
|
+
const pair = lookup.byKey?.get(key);
|
|
916
|
+
if (!pair) {
|
|
917
|
+
return { sourceMemoryIds: [], sourceCanonicalKeys: [], error: 'invalid_source_lineage' };
|
|
918
|
+
}
|
|
919
|
+
pairs.set(pair.key, pair);
|
|
920
|
+
}
|
|
921
|
+
for (const id of ids) {
|
|
922
|
+
const pair = lookup.byId?.get(id);
|
|
923
|
+
if (!pair) {
|
|
924
|
+
return { sourceMemoryIds: [], sourceCanonicalKeys: [], error: 'invalid_source_lineage' };
|
|
925
|
+
}
|
|
926
|
+
pairs.set(pair.key, pair);
|
|
927
|
+
}
|
|
928
|
+
const sorted = [...pairs.values()].sort((a, b) => {
|
|
929
|
+
if (a.key !== b.key) return a.key.localeCompare(b.key);
|
|
930
|
+
return a.id - b.id;
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
sourceMemoryIds: sorted.map(pair => pair.id),
|
|
934
|
+
sourceCanonicalKeys: sorted.map(pair => pair.key),
|
|
935
|
+
error: null,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
507
939
|
function buildTimerSynthesisCandidates(plan = {}, synthesisSummary = {}, opts = {}) {
|
|
508
940
|
const synthesisInput = plan.synthesisInput || plan.meta?.synthesisInput || null;
|
|
509
941
|
if (!synthesisInput) {
|
|
@@ -513,6 +945,8 @@ function buildTimerSynthesisCandidates(plan = {}, synthesisSummary = {}, opts =
|
|
|
513
945
|
const structuredSummary = summary.structuredSummary || {};
|
|
514
946
|
const scope = inferTimerSynthesisScope(synthesisInput, opts);
|
|
515
947
|
const lineage = sourceLineageFromSynthesisInput(synthesisInput);
|
|
948
|
+
const sourceLookup = sourceLookupFromSynthesisInput(synthesisInput);
|
|
949
|
+
const sourceRecordCount = lineage.sourceMemoryIds.length;
|
|
516
950
|
const synthesisHash = hashSnapshot({
|
|
517
951
|
summary,
|
|
518
952
|
lineage,
|
|
@@ -530,7 +964,7 @@ function buildTimerSynthesisCandidates(plan = {}, synthesisSummary = {}, opts =
|
|
|
530
964
|
periodStart: canonicalInstant(plan.periodStart),
|
|
531
965
|
periodEnd: canonicalInstant(plan.periodEnd),
|
|
532
966
|
policyVersion: plan.policyVersion || 'v1',
|
|
533
|
-
|
|
967
|
+
sourceRecordCount,
|
|
534
968
|
},
|
|
535
969
|
}];
|
|
536
970
|
const extracted = extractCandidatesFromStructuredSummary({
|
|
@@ -543,36 +977,54 @@ function buildTimerSynthesisCandidates(plan = {}, synthesisSummary = {}, opts =
|
|
|
543
977
|
authority: opts.authority || 'verified_summary',
|
|
544
978
|
evidenceRefs,
|
|
545
979
|
});
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
980
|
+
const prepared = extracted.map((candidate, index) => {
|
|
981
|
+
const normalizedCandidate = normalizeAssistantBehaviorServingText(candidate);
|
|
982
|
+
const itemPayload = normalizedCandidate.payload && typeof normalizedCandidate.payload === 'object'
|
|
983
|
+
? normalizedCandidate.payload
|
|
984
|
+
: {};
|
|
985
|
+
const itemLineage = resolveCandidateSourceLineage(itemPayload, sourceLookup);
|
|
986
|
+
const candidateHash = hashSnapshot({
|
|
549
987
|
synthesisHash,
|
|
550
988
|
index,
|
|
551
|
-
canonicalKey:
|
|
552
|
-
summary:
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
...
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
989
|
+
canonicalKey: normalizedCandidate.canonicalKey,
|
|
990
|
+
summary: normalizedCandidate.summary,
|
|
991
|
+
});
|
|
992
|
+
return {
|
|
993
|
+
...normalizedCandidate,
|
|
994
|
+
candidateHash,
|
|
995
|
+
sourceMemoryIds: itemLineage.sourceMemoryIds,
|
|
996
|
+
sourceCanonicalKeys: itemLineage.sourceCanonicalKeys,
|
|
997
|
+
sourceLineageError: itemLineage.error,
|
|
998
|
+
payload: {
|
|
999
|
+
...candidatePayloadWithoutLineage(itemPayload),
|
|
1000
|
+
kind: 'timer_synthesis',
|
|
1001
|
+
synthesisKind: synthesisInput.kind || 'timer_current_memory_synthesis_v1',
|
|
1002
|
+
currentMemoryRole: `${plan.cadence || 'manual'}_timer_synthesis_candidate`,
|
|
1003
|
+
promotionGate: 'operator_required',
|
|
1004
|
+
cadence: plan.cadence,
|
|
1005
|
+
policyVersion: plan.policyVersion || 'v1',
|
|
1006
|
+
periodStart: canonicalInstant(plan.periodStart),
|
|
1007
|
+
periodEnd: canonicalInstant(plan.periodEnd),
|
|
1008
|
+
synthesisHash,
|
|
1009
|
+
sourceLineageRef: `timer_synthesis:${synthesisHash}:candidate:${candidateHash}`,
|
|
1010
|
+
sourceRecordCount: itemLineage.sourceMemoryIds.length,
|
|
1011
|
+
safetyGateSummary: summarizeSafetyGate(safetyGate),
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
const standardized = applyTemporalDistillationStandard(prepared, {
|
|
1016
|
+
cadence: plan.cadence,
|
|
1017
|
+
periodStart: plan.periodStart,
|
|
1018
|
+
periodEnd: plan.periodEnd,
|
|
1019
|
+
requireSourceLineage: true,
|
|
1020
|
+
});
|
|
570
1021
|
|
|
571
1022
|
return {
|
|
572
1023
|
summary,
|
|
573
1024
|
safetyGate,
|
|
574
1025
|
synthesisHash,
|
|
575
|
-
candidates,
|
|
1026
|
+
candidates: standardized.candidates,
|
|
1027
|
+
rejectedCandidates: standardized.rejected,
|
|
576
1028
|
sourceMemoryIds: lineage.sourceMemoryIds,
|
|
577
1029
|
sourceCanonicalKeys: lineage.sourceCanonicalKeys,
|
|
578
1030
|
};
|
|
@@ -603,6 +1055,23 @@ function attachTimerSynthesis(plan = {}, synthesisSummary = {}, opts = {}) {
|
|
|
603
1055
|
safetyGate: synthesis.safetyGate,
|
|
604
1056
|
synthesisHash: synthesis.synthesisHash,
|
|
605
1057
|
candidateCount: synthesis.candidates.length,
|
|
1058
|
+
candidateTrace: synthesis.candidates.map(candidate => ({
|
|
1059
|
+
candidateHash: candidate.candidateHash,
|
|
1060
|
+
memoryType: candidate.memoryType,
|
|
1061
|
+
canonicalKey: candidate.canonicalKey,
|
|
1062
|
+
sourceMemoryIds: candidate.sourceMemoryIds || [],
|
|
1063
|
+
sourceCanonicalKeys: candidate.sourceCanonicalKeys || [],
|
|
1064
|
+
})),
|
|
1065
|
+
rejectedCandidateCount: synthesis.rejectedCandidates.length,
|
|
1066
|
+
rejectedCandidates: synthesis.rejectedCandidates.map(item => ({
|
|
1067
|
+
memoryType: item.candidate.memoryType,
|
|
1068
|
+
canonicalKey: item.candidate.canonicalKey,
|
|
1069
|
+
summary: compactSummary(item.candidate),
|
|
1070
|
+
reasons: item.reasons,
|
|
1071
|
+
distillation: item.candidate.payload?.distillation || null,
|
|
1072
|
+
sourceMemoryIds: item.candidate.sourceMemoryIds || [],
|
|
1073
|
+
sourceCanonicalKeys: item.candidate.sourceCanonicalKeys || [],
|
|
1074
|
+
})),
|
|
606
1075
|
sourceMemoryIds: synthesis.sourceMemoryIds,
|
|
607
1076
|
sourceCanonicalKeys: synthesis.sourceCanonicalKeys,
|
|
608
1077
|
},
|
|
@@ -613,6 +1082,7 @@ function attachTimerSynthesis(plan = {}, synthesisSummary = {}, opts = {}) {
|
|
|
613
1082
|
synthesisResult: {
|
|
614
1083
|
synthesisHash: synthesis.synthesisHash,
|
|
615
1084
|
candidateCount: synthesis.candidates.length,
|
|
1085
|
+
rejectedCandidateCount: synthesis.rejectedCandidates.length,
|
|
616
1086
|
safetyGate: synthesis.safetyGate,
|
|
617
1087
|
},
|
|
618
1088
|
},
|
|
@@ -625,14 +1095,28 @@ function buildPromotionReview(input = {}, opts = {}) {
|
|
|
625
1095
|
const applyResult = input.applyResult || {};
|
|
626
1096
|
const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
|
|
627
1097
|
const statusUpdates = Array.isArray(plan.statusUpdates) ? plan.statusUpdates : [];
|
|
1098
|
+
const aggregateCandidateCount = candidates.filter(isAggregateRollupCandidate).length;
|
|
1099
|
+
const synthesisResult = plan.synthesisResult || plan.meta?.synthesisResult || null;
|
|
1100
|
+
const rejectedCandidateCount = synthesisResult?.rejectedCandidateCount || 0;
|
|
1101
|
+
const gate = input.promoteCandidates === true
|
|
1102
|
+
? (aggregateCandidateCount > 0
|
|
1103
|
+
? 'blocked unless unsafePromoteAggregateCandidates=true; attach reviewed synthesis summary for normal promotion'
|
|
1104
|
+
: 'operator promotion requested for reviewed synthesis candidates')
|
|
1105
|
+
: 'candidate-only unless promoteCandidates=true with reviewed synthesis summary';
|
|
628
1106
|
const candidateLines = candidates.slice(0, Math.max(0, Math.min(20, opts.limit || 8))).map(candidate => {
|
|
629
1107
|
const type = candidate.memoryType || candidate.memory_type || 'memory';
|
|
630
1108
|
const scope = candidate.scopeKey || candidate.scope_key || 'unspecified';
|
|
631
1109
|
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
632
|
-
const
|
|
633
|
-
?
|
|
1110
|
+
const candidateSourceKeys = Array.isArray(candidate.sourceCanonicalKeys) && candidate.sourceCanonicalKeys.length > 0
|
|
1111
|
+
? candidate.sourceCanonicalKeys
|
|
1112
|
+
: (Array.isArray(payload.sourceCanonicalKeys) ? payload.sourceCanonicalKeys : []);
|
|
1113
|
+
const sourceKeys = candidateSourceKeys.length > 0
|
|
1114
|
+
? ` | sources=${candidateSourceKeys.join(',')}`
|
|
634
1115
|
: '';
|
|
635
|
-
|
|
1116
|
+
const sourceLineage = !sourceKeys && payload.sourceLineageRef
|
|
1117
|
+
? ` | lineage=${payload.sourceLineageRef} sourceRecordCount=${payload.sourceRecordCount || 0}`
|
|
1118
|
+
: '';
|
|
1119
|
+
return `- candidate ${type} ${scope}: ${compactSummary(candidate)}${sourceKeys}${sourceLineage}`;
|
|
636
1120
|
});
|
|
637
1121
|
const staleLines = statusUpdates.slice(0, Math.max(0, Math.min(20, opts.limit || 8))).map(update => (
|
|
638
1122
|
`- ${update.status}: ${update.canonicalKey || update.memoryId || 'memory'}${update.reason ? ` (${update.reason})` : ''}`
|
|
@@ -642,9 +1126,10 @@ function buildPromotionReview(input = {}, opts = {}) {
|
|
|
642
1126
|
'Promotion review:',
|
|
643
1127
|
`window: ${plan.cadence || 'manual'} ${canonicalInstant(plan.periodStart)} -> ${canonicalInstant(plan.periodEnd)}`,
|
|
644
1128
|
`source: ${plan.synthesisInput?.sourceOfTruth || plan.meta?.synthesisInput?.sourceOfTruth || 'memory_records'}`,
|
|
645
|
-
`gate: ${
|
|
1129
|
+
`gate: ${gate}`,
|
|
1130
|
+
`temporal synthesis: ${synthesisResult ? `reviewed summary attached (${synthesisResult.candidateCount || 0} candidates, ${rejectedCandidateCount} rejected)` : 'not attached'}`,
|
|
646
1131
|
`status updates: planned=${statusUpdates.length} applied=${applyResult.applied || 0} skipped=${applyResult.skipped || 0}`,
|
|
647
|
-
`candidates: planned=${candidates.length} promoted=${promotionResult.promoted || 0} quarantined=${promotionResult.quarantined || 0} errored=${promotionResult.errored || 0}`,
|
|
1132
|
+
`candidates: planned=${candidates.length} aggregate=${aggregateCandidateCount} promoted=${promotionResult.promoted || 0} quarantined=${promotionResult.quarantined || 0} errored=${promotionResult.errored || 0}`,
|
|
648
1133
|
'candidate proposals:',
|
|
649
1134
|
linesOrNone(candidateLines),
|
|
650
1135
|
'status update proposals:',
|
|
@@ -720,12 +1205,37 @@ function summarizePromotionResults(results = []) {
|
|
|
720
1205
|
return summary;
|
|
721
1206
|
}
|
|
722
1207
|
|
|
723
|
-
function
|
|
1208
|
+
function planLineageForCandidate(candidate = {}, plan = {}) {
|
|
724
1209
|
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1210
|
+
const sourceLineageRef = String(payload.sourceLineageRef || '').trim();
|
|
1211
|
+
if (!sourceLineageRef) return null;
|
|
1212
|
+
const synthesisResult = plan.synthesisResult || plan.meta?.synthesisResult || null;
|
|
1213
|
+
if (!synthesisResult || !synthesisResult.synthesisHash) return null;
|
|
1214
|
+
const baseRef = `timer_synthesis:${synthesisResult.synthesisHash}`;
|
|
1215
|
+
if (sourceLineageRef !== baseRef && !sourceLineageRef.startsWith(`${baseRef}:candidate:`)) return null;
|
|
1216
|
+
return {
|
|
1217
|
+
sourceMemoryIds: Array.isArray(synthesisResult.sourceMemoryIds) ? synthesisResult.sourceMemoryIds : [],
|
|
1218
|
+
sourceCanonicalKeys: Array.isArray(synthesisResult.sourceCanonicalKeys) ? synthesisResult.sourceCanonicalKeys : [],
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function normalizeCandidateLineage(candidate = {}, opts = {}) {
|
|
1223
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
1224
|
+
const planLineage = planLineageForCandidate(candidate, opts.plan || {});
|
|
1225
|
+
const candidateCanonicalKeys = Array.isArray(candidate.sourceCanonicalKeys)
|
|
1226
|
+
? candidate.sourceCanonicalKeys
|
|
1227
|
+
: (Array.isArray(candidate.source_canonical_keys) ? candidate.source_canonical_keys : null);
|
|
1228
|
+
const candidateMemoryIds = Array.isArray(candidate.sourceMemoryIds)
|
|
1229
|
+
? candidate.sourceMemoryIds
|
|
1230
|
+
: (Array.isArray(candidate.source_memory_ids) ? candidate.source_memory_ids : null);
|
|
1231
|
+
const sourceCanonicalKeys = Array.isArray(candidateCanonicalKeys)
|
|
1232
|
+
? candidateCanonicalKeys.map(key => String(key || '')).filter(Boolean)
|
|
1233
|
+
: (Array.isArray(payload.sourceCanonicalKeys)
|
|
1234
|
+
? payload.sourceCanonicalKeys.map(key => String(key || '')).filter(Boolean)
|
|
1235
|
+
: (planLineage?.sourceCanonicalKeys || []).map(key => String(key || '')).filter(Boolean));
|
|
1236
|
+
const rawSourceMemoryIds = Array.isArray(candidateMemoryIds)
|
|
1237
|
+
? candidateMemoryIds
|
|
1238
|
+
: (Array.isArray(payload.sourceMemoryIds) ? payload.sourceMemoryIds : (planLineage?.sourceMemoryIds || []));
|
|
729
1239
|
const sourceMemoryIds = rawSourceMemoryIds
|
|
730
1240
|
.filter(id => id !== null && id !== undefined)
|
|
731
1241
|
.map(id => Number(id));
|
|
@@ -1086,7 +1596,9 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1086
1596
|
for (let i = 0; i < candidates.length; i++) {
|
|
1087
1597
|
const candidate = candidates[i] || {};
|
|
1088
1598
|
const result = results[i] || {};
|
|
1089
|
-
const { payload, sourceMemoryIds, sourceCanonicalKeys } = normalizeCandidateLineage(candidate
|
|
1599
|
+
const { payload, sourceMemoryIds, sourceCanonicalKeys } = normalizeCandidateLineage(candidate, {
|
|
1600
|
+
plan: input.plan,
|
|
1601
|
+
});
|
|
1090
1602
|
const candidateHash = candidate.candidateHash || payload.candidateHash || hashSnapshot(candidate);
|
|
1091
1603
|
const inserted = await queryable.query(
|
|
1092
1604
|
`INSERT INTO ${schema}.compaction_candidates (
|
|
@@ -1213,6 +1725,7 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1213
1725
|
tenantId,
|
|
1214
1726
|
run: claim,
|
|
1215
1727
|
candidates,
|
|
1728
|
+
plan,
|
|
1216
1729
|
results: candidates.map(candidate => ({
|
|
1217
1730
|
candidate,
|
|
1218
1731
|
action: 'planned',
|
|
@@ -1258,6 +1771,8 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1258
1771
|
const promoteCandidates = input.promoteCandidates === true;
|
|
1259
1772
|
const summary = createApplySummary(statusUpdates);
|
|
1260
1773
|
|
|
1774
|
+
assertPromotableTemporalCandidates(candidates, input);
|
|
1775
|
+
|
|
1261
1776
|
if (!records || typeof records.updateMemoryStatusIfCurrent !== 'function') {
|
|
1262
1777
|
throw new Error('memory.consolidation.executePlan requires records.updateMemoryStatusIfCurrent');
|
|
1263
1778
|
}
|
|
@@ -1344,6 +1859,7 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1344
1859
|
tenantId,
|
|
1345
1860
|
run: claim,
|
|
1346
1861
|
candidates,
|
|
1862
|
+
plan,
|
|
1347
1863
|
results: promotionResults,
|
|
1348
1864
|
});
|
|
1349
1865
|
const promotionResult = summarizePromotionResults(promotionResults);
|
|
@@ -1525,6 +2041,7 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1525
2041
|
applyToken: input.applyToken,
|
|
1526
2042
|
appliedAt: input.appliedAt,
|
|
1527
2043
|
promoteCandidates: true,
|
|
2044
|
+
unsafePromoteAggregateCandidates: input.unsafePromoteAggregateCandidates,
|
|
1528
2045
|
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
1529
2046
|
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
1530
2047
|
})
|
|
@@ -1579,6 +2096,11 @@ module.exports = {
|
|
|
1579
2096
|
canonicalInstant,
|
|
1580
2097
|
normalizeClaimLeaseSeconds,
|
|
1581
2098
|
resolveOperatorWindow,
|
|
2099
|
+
temporalPolicyForCadence,
|
|
2100
|
+
evaluateTemporalDistillationCandidate,
|
|
2101
|
+
applyTemporalDistillationStandard,
|
|
2102
|
+
isAggregateRollupCandidate,
|
|
2103
|
+
assertPromotableTemporalCandidates,
|
|
1582
2104
|
planCompaction,
|
|
1583
2105
|
buildTimerSynthesisInput,
|
|
1584
2106
|
buildTimerSynthesisPrompt,
|