@shadowforge0/aquifer-memory 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- package/scripts/codex-recovery.js +246 -1
package/core/mcp-manifest.js
CHANGED
|
@@ -18,9 +18,73 @@ const path = require('path');
|
|
|
18
18
|
const MCP_SERVER_NAME = 'aquifer-memory';
|
|
19
19
|
|
|
20
20
|
const MCP_TOOL_MANIFEST = Object.freeze([
|
|
21
|
+
{
|
|
22
|
+
name: 'memory_recall',
|
|
23
|
+
description: 'Explicit current-memory recall on the active curated memory corpus. Use this for current state and next-step lookup; use historical_recall for older session detail.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
query: { type: 'string', minLength: 1, description: 'Search query (keyword or natural language)' },
|
|
29
|
+
limit: { type: 'integer', minimum: 1, maximum: 20, description: 'Max results (default 5)' },
|
|
30
|
+
mode: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
enum: ['fts', 'hybrid', 'vector'],
|
|
33
|
+
description: 'Recall mode: "fts" (keyword only), "hybrid" (default, FTS + vector), "vector" (vector only)',
|
|
34
|
+
},
|
|
35
|
+
explain: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Include per-result score breakdown (diagnostic use only).',
|
|
38
|
+
},
|
|
39
|
+
activeScopeKey: { type: 'string', description: 'Active curated memory scope key, e.g. project:aquifer' },
|
|
40
|
+
activeScopePath: {
|
|
41
|
+
type: 'array',
|
|
42
|
+
items: { type: 'string' },
|
|
43
|
+
description: 'Ordered curated scope path from global to active scope',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['query'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'historical_recall',
|
|
51
|
+
description: 'Explicit historical/session recall over stored sessions and summaries. Use this for timeline, detail, and session context lookup; use evidence_recall only for audit/debug/provenance.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
properties: {
|
|
56
|
+
query: { type: 'string', minLength: 1, description: 'Search query (keyword or natural language)' },
|
|
57
|
+
limit: { type: 'integer', minimum: 1, maximum: 20, description: 'Max results (default 5)' },
|
|
58
|
+
agentId: { type: 'string', description: 'Filter by agent ID' },
|
|
59
|
+
source: { type: 'string', description: 'Filter by source (e.g., gateway, cc)' },
|
|
60
|
+
dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
|
|
61
|
+
dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
|
|
62
|
+
entities: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
description: 'Entity names to match',
|
|
66
|
+
},
|
|
67
|
+
entityMode: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
enum: ['any', 'all'],
|
|
70
|
+
description: '"any" (default, boost) or "all" (only sessions with every entity)',
|
|
71
|
+
},
|
|
72
|
+
mode: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
enum: ['fts', 'hybrid', 'vector'],
|
|
75
|
+
description: 'Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)',
|
|
76
|
+
},
|
|
77
|
+
explain: {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Include per-result score breakdown (rrf, timeDecay, entity, trust, rerank). Diagnostic use only.',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ['query'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
21
85
|
{
|
|
22
86
|
name: 'session_recall',
|
|
23
|
-
description: '
|
|
87
|
+
description: 'Compatibility recall surface. In curated serving mode this routes to current memory; in legacy serving mode it routes to historical/session recall. Prefer memory_recall for current state and historical_recall for timeline/detail lookup.',
|
|
24
88
|
inputSchema: {
|
|
25
89
|
type: 'object',
|
|
26
90
|
additionalProperties: false,
|
|
@@ -115,7 +179,7 @@ const MCP_TOOL_MANIFEST = Object.freeze([
|
|
|
115
179
|
},
|
|
116
180
|
{
|
|
117
181
|
name: 'memory_stats',
|
|
118
|
-
description: 'Return storage statistics for the Aquifer memory store
|
|
182
|
+
description: 'Return storage statistics for the Aquifer memory store, serving mode, current-memory record coverage, and session date range.',
|
|
119
183
|
inputSchema: {
|
|
120
184
|
type: 'object',
|
|
121
185
|
additionalProperties: false,
|
package/core/memory-bootstrap.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const TYPE_PRIORITY = {
|
|
4
4
|
constraint: 0,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
state: 1,
|
|
6
|
+
open_loop: 2,
|
|
7
|
+
decision: 3,
|
|
8
|
+
preference: 4,
|
|
9
9
|
fact: 5,
|
|
10
10
|
conclusion: 6,
|
|
11
11
|
entity_note: 7,
|
|
@@ -96,7 +96,13 @@ function resolveApplicableRecords(records = [], opts = {}) {
|
|
|
96
96
|
return [...winners.values(), ...additive];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function sortForBootstrap(a, b) {
|
|
99
|
+
function sortForBootstrap(a, b, opts = {}) {
|
|
100
|
+
const activeScopePath = opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : ['global']);
|
|
101
|
+
const position = new Map(activeScopePath.map((key, idx) => [key, idx]));
|
|
102
|
+
const aScope = position.get(scopeKey(a)) ?? -1;
|
|
103
|
+
const bScope = position.get(scopeKey(b)) ?? -1;
|
|
104
|
+
if (bScope !== aScope) return bScope - aScope;
|
|
105
|
+
|
|
100
106
|
const aType = TYPE_PRIORITY[a.memoryType || a.memory_type] ?? 99;
|
|
101
107
|
const bType = TYPE_PRIORITY[b.memoryType || b.memory_type] ?? 99;
|
|
102
108
|
if (aType !== bType) return aType - bType;
|
|
@@ -129,10 +135,11 @@ function buildText(records, meta) {
|
|
|
129
135
|
|
|
130
136
|
function buildMemoryBootstrap(records = [], opts = {}) {
|
|
131
137
|
const maxChars = Math.max(120, opts.maxChars || 4000);
|
|
138
|
+
const limit = Number.isFinite(opts.limit) ? Math.max(1, Math.min(100, Math.floor(opts.limit))) : null;
|
|
132
139
|
const active = resolveApplicableRecords(
|
|
133
140
|
records.filter(record => isActiveBootstrap(record, opts)),
|
|
134
141
|
opts,
|
|
135
|
-
).sort(sortForBootstrap);
|
|
142
|
+
).sort((a, b) => sortForBootstrap(a, b, opts));
|
|
136
143
|
|
|
137
144
|
const meta = {
|
|
138
145
|
overflow: false,
|
|
@@ -141,7 +148,11 @@ function buildMemoryBootstrap(records = [], opts = {}) {
|
|
|
141
148
|
count: active.length,
|
|
142
149
|
};
|
|
143
150
|
|
|
144
|
-
let selected = active.slice();
|
|
151
|
+
let selected = limit ? active.slice(0, limit) : active.slice();
|
|
152
|
+
if (limit && active.length > limit) {
|
|
153
|
+
meta.overflow = true;
|
|
154
|
+
meta.degraded = true;
|
|
155
|
+
}
|
|
145
156
|
let text = buildText(selected, meta);
|
|
146
157
|
while (text.length > maxChars && selected.length > 1) {
|
|
147
158
|
selected = selected.slice(0, -1);
|
|
@@ -167,13 +178,14 @@ function buildMemoryBootstrap(records = [], opts = {}) {
|
|
|
167
178
|
|
|
168
179
|
function createMemoryBootstrap({ records }) {
|
|
169
180
|
async function bootstrap(opts = {}) {
|
|
181
|
+
const requestedLimit = Number.isFinite(opts.limit) ? Math.max(1, Math.floor(opts.limit)) : 50;
|
|
170
182
|
const rows = await records.listActive({
|
|
171
183
|
tenantId: opts.tenantId,
|
|
172
184
|
scopeId: opts.scopeId,
|
|
173
185
|
scopeKeys: opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : undefined),
|
|
174
186
|
visibleInBootstrap: true,
|
|
175
187
|
asOf: opts.asOf,
|
|
176
|
-
limit:
|
|
188
|
+
limit: Math.max(50, Math.min(200, requestedLimit * 4)),
|
|
177
189
|
});
|
|
178
190
|
return buildMemoryBootstrap(rows, opts);
|
|
179
191
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { extractCandidatesFromStructuredSummary, createMemoryPromotion } = require('./memory-promotion');
|
|
5
5
|
const { createMemoryRecords } = require('./memory-records');
|
|
6
|
+
const { sanitizeSummaryResult } = require('./memory-safety-gate');
|
|
6
7
|
|
|
7
8
|
const ALLOWED_CADENCES = new Set(['session', 'daily', 'weekly', 'monthly', 'manual']);
|
|
8
9
|
const OPERATOR_CADENCES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
@@ -296,6 +297,9 @@ function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
|
296
297
|
candidateHash,
|
|
297
298
|
payload: {
|
|
298
299
|
kind: 'compaction_rollup',
|
|
300
|
+
synthesisKind: 'timer_current_memory_synthesis_v1',
|
|
301
|
+
currentMemoryRole: `${cadence}_timer_synthesis_candidate`,
|
|
302
|
+
promotionGate: 'operator_required',
|
|
299
303
|
cadence,
|
|
300
304
|
policyVersion,
|
|
301
305
|
periodStart: windowStart,
|
|
@@ -328,6 +332,326 @@ function buildAggregateCandidates(normalized, statusUpdates, opts) {
|
|
|
328
332
|
return candidates;
|
|
329
333
|
}
|
|
330
334
|
|
|
335
|
+
function buildTimerSynthesisInput(normalized, statusUpdates, candidates, opts) {
|
|
336
|
+
const { cadence, periodStart, periodEnd } = opts;
|
|
337
|
+
if (!aggregateCandidateCadence(cadence)) return null;
|
|
338
|
+
|
|
339
|
+
const staleIds = new Set(statusUpdates.map(update => String(update.memoryId)));
|
|
340
|
+
const staleKeys = new Set(statusUpdates.map(update => String(update.canonicalKey || '')));
|
|
341
|
+
const sourceCurrentMemory = normalized
|
|
342
|
+
.filter(record => record.status === 'active')
|
|
343
|
+
.filter(record => record.id === null || !staleIds.has(String(record.id)))
|
|
344
|
+
.filter(record => !staleKeys.has(record.canonicalKey))
|
|
345
|
+
.filter(record => Boolean(record.canonicalKey))
|
|
346
|
+
.map(record => ({
|
|
347
|
+
memoryId: record.id,
|
|
348
|
+
memoryType: record.memoryType,
|
|
349
|
+
canonicalKey: record.canonicalKey,
|
|
350
|
+
scopeKind: record.scopeKind,
|
|
351
|
+
scopeKey: record.scopeKey,
|
|
352
|
+
contextKey: record.contextKey,
|
|
353
|
+
topicKey: record.topicKey,
|
|
354
|
+
summary: compactSummary(record),
|
|
355
|
+
acceptedAt: record.acceptedAt,
|
|
356
|
+
validFrom: record.validFrom,
|
|
357
|
+
validTo: record.validTo,
|
|
358
|
+
staleAfter: record.staleAfter,
|
|
359
|
+
}))
|
|
360
|
+
.sort((a, b) => {
|
|
361
|
+
if (a.canonicalKey !== b.canonicalKey) return a.canonicalKey.localeCompare(b.canonicalKey);
|
|
362
|
+
return String(a.memoryId).localeCompare(String(b.memoryId));
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const windowStart = canonicalInstant(periodStart);
|
|
366
|
+
const windowEnd = canonicalInstant(periodEnd);
|
|
367
|
+
return {
|
|
368
|
+
kind: 'timer_current_memory_synthesis_v1',
|
|
369
|
+
sourceOfTruth: 'memory_records',
|
|
370
|
+
cadence,
|
|
371
|
+
policyVersion: opts.policyVersion || 'v1',
|
|
372
|
+
periodStart: windowStart,
|
|
373
|
+
periodEnd: windowEnd,
|
|
374
|
+
promotion: {
|
|
375
|
+
default: 'candidate_only',
|
|
376
|
+
requires: 'apply=true and promoteCandidates=true',
|
|
377
|
+
},
|
|
378
|
+
guards: {
|
|
379
|
+
rawTranscriptExcluded: true,
|
|
380
|
+
sessionSummariesExcluded: true,
|
|
381
|
+
nonActiveMemoryExcluded: true,
|
|
382
|
+
stalePlannedMemoryExcluded: true,
|
|
383
|
+
},
|
|
384
|
+
sourceCurrentMemory,
|
|
385
|
+
statusUpdates: statusUpdates
|
|
386
|
+
.map(update => ({
|
|
387
|
+
memoryId: update.memoryId,
|
|
388
|
+
canonicalKey: update.canonicalKey,
|
|
389
|
+
status: update.status,
|
|
390
|
+
reason: update.reason,
|
|
391
|
+
}))
|
|
392
|
+
.sort((a, b) => {
|
|
393
|
+
if (a.canonicalKey !== b.canonicalKey) return String(a.canonicalKey).localeCompare(String(b.canonicalKey));
|
|
394
|
+
return String(a.memoryId).localeCompare(String(b.memoryId));
|
|
395
|
+
}),
|
|
396
|
+
candidateProposals: candidates
|
|
397
|
+
.map(candidate => ({
|
|
398
|
+
candidateHash: candidate.candidateHash,
|
|
399
|
+
memoryType: candidate.memoryType,
|
|
400
|
+
canonicalKey: candidate.canonicalKey,
|
|
401
|
+
scopeKind: candidate.scopeKind,
|
|
402
|
+
scopeKey: candidate.scopeKey,
|
|
403
|
+
contextKey: candidate.contextKey,
|
|
404
|
+
topicKey: candidate.topicKey,
|
|
405
|
+
summary: compactSummary(candidate),
|
|
406
|
+
sourceMemoryIds: candidate.payload?.sourceMemoryIds || [],
|
|
407
|
+
sourceCanonicalKeys: candidate.payload?.sourceCanonicalKeys || [],
|
|
408
|
+
}))
|
|
409
|
+
.sort((a, b) => {
|
|
410
|
+
if (a.canonicalKey !== b.canonicalKey) return String(a.canonicalKey).localeCompare(String(b.canonicalKey));
|
|
411
|
+
return String(a.candidateHash).localeCompare(String(b.candidateHash));
|
|
412
|
+
}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function buildTimerSynthesisPrompt(plan = {}, opts = {}) {
|
|
417
|
+
const synthesisInput = plan.synthesisInput || plan.meta?.synthesisInput || null;
|
|
418
|
+
if (!synthesisInput) {
|
|
419
|
+
throw new Error('memory.consolidation.timer_synthesis_prompt requires a timer synthesisInput');
|
|
420
|
+
}
|
|
421
|
+
const maxFacts = opts.maxFacts || 12;
|
|
422
|
+
return [
|
|
423
|
+
'You are producing an Aquifer timer current-memory synthesis proposal.',
|
|
424
|
+
'Use only the <timer_synthesis_input> block. Do not read raw transcripts, session summaries, tool output, or debug material.',
|
|
425
|
+
'This is a producer proposal, not an active memory commit. Promotion still requires the normal operator promotion gate.',
|
|
426
|
+
'Return compact JSON with this shape:',
|
|
427
|
+
'{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]}}',
|
|
428
|
+
`Keep facts/states/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
|
|
429
|
+
'Mark uncertain, resolved, superseded, revoked, or stale items explicitly in payload fields when applicable.',
|
|
430
|
+
'Do not copy sourceCurrentMemory unchanged unless the timer window confirms it still carries forward.',
|
|
431
|
+
'',
|
|
432
|
+
'<timer_synthesis_input>',
|
|
433
|
+
stableJson(synthesisInput),
|
|
434
|
+
'</timer_synthesis_input>',
|
|
435
|
+
].join('\n');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeTimerSynthesisSummary(input = {}) {
|
|
439
|
+
const raw = input && typeof input === 'object'
|
|
440
|
+
? {
|
|
441
|
+
summaryText: input.summaryText || input.summary || '',
|
|
442
|
+
structuredSummary: input.structuredSummary || input.structured_summary || {},
|
|
443
|
+
}
|
|
444
|
+
: {
|
|
445
|
+
summaryText: '',
|
|
446
|
+
structuredSummary: {},
|
|
447
|
+
};
|
|
448
|
+
const sanitized = sanitizeSummaryResult(raw);
|
|
449
|
+
return {
|
|
450
|
+
summary: sanitized.summaryResult || raw,
|
|
451
|
+
safetyGate: sanitized.meta || {},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function inferTimerSynthesisScope(synthesisInput = {}, opts = {}) {
|
|
456
|
+
if (opts.scopeKind && opts.scopeKey) {
|
|
457
|
+
return {
|
|
458
|
+
scopeKind: opts.scopeKind,
|
|
459
|
+
scopeKey: opts.scopeKey,
|
|
460
|
+
contextKey: opts.contextKey || null,
|
|
461
|
+
topicKey: opts.topicKey || null,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const scoped = new Map();
|
|
465
|
+
for (const row of synthesisInput.sourceCurrentMemory || []) {
|
|
466
|
+
const scopeKind = row.scopeKind || 'unspecified';
|
|
467
|
+
const scopeKey = row.scopeKey || 'unspecified';
|
|
468
|
+
const contextKey = row.contextKey || null;
|
|
469
|
+
const topicKey = row.topicKey || null;
|
|
470
|
+
scoped.set(stableJson({ scopeKind, scopeKey, contextKey, topicKey }), {
|
|
471
|
+
scopeKind,
|
|
472
|
+
scopeKey,
|
|
473
|
+
contextKey,
|
|
474
|
+
topicKey,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (scoped.size === 1) return [...scoped.values()][0];
|
|
478
|
+
if (scoped.size === 0 && opts.activeScopeKey) {
|
|
479
|
+
return {
|
|
480
|
+
scopeKind: opts.activeScopeKind || 'project',
|
|
481
|
+
scopeKey: opts.activeScopeKey,
|
|
482
|
+
contextKey: opts.contextKey || null,
|
|
483
|
+
topicKey: opts.topicKey || null,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
throw new Error('memory.consolidation.timer_synthesis requires scopeKind/scopeKey for multi-scope synthesis');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function sourceLineageFromSynthesisInput(synthesisInput = {}) {
|
|
490
|
+
const rows = Array.isArray(synthesisInput.sourceCurrentMemory) ? synthesisInput.sourceCurrentMemory : [];
|
|
491
|
+
const pairs = rows
|
|
492
|
+
.map(row => ({
|
|
493
|
+
id: Number(row.memoryId),
|
|
494
|
+
key: String(row.canonicalKey || '').trim(),
|
|
495
|
+
}))
|
|
496
|
+
.filter(row => Number.isSafeInteger(row.id) && row.id > 0 && row.key);
|
|
497
|
+
pairs.sort((a, b) => {
|
|
498
|
+
if (a.key !== b.key) return a.key.localeCompare(b.key);
|
|
499
|
+
return a.id - b.id;
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
sourceMemoryIds: pairs.map(row => row.id),
|
|
503
|
+
sourceCanonicalKeys: pairs.map(row => row.key),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildTimerSynthesisCandidates(plan = {}, synthesisSummary = {}, opts = {}) {
|
|
508
|
+
const synthesisInput = plan.synthesisInput || plan.meta?.synthesisInput || null;
|
|
509
|
+
if (!synthesisInput) {
|
|
510
|
+
throw new Error('memory.consolidation.timer_synthesis requires a timer synthesisInput');
|
|
511
|
+
}
|
|
512
|
+
const { summary, safetyGate } = normalizeTimerSynthesisSummary(synthesisSummary);
|
|
513
|
+
const structuredSummary = summary.structuredSummary || {};
|
|
514
|
+
const scope = inferTimerSynthesisScope(synthesisInput, opts);
|
|
515
|
+
const lineage = sourceLineageFromSynthesisInput(synthesisInput);
|
|
516
|
+
const synthesisHash = hashSnapshot({
|
|
517
|
+
summary,
|
|
518
|
+
lineage,
|
|
519
|
+
cadence: plan.cadence,
|
|
520
|
+
periodStart: canonicalInstant(plan.periodStart),
|
|
521
|
+
periodEnd: canonicalInstant(plan.periodEnd),
|
|
522
|
+
policyVersion: plan.policyVersion || 'v1',
|
|
523
|
+
});
|
|
524
|
+
const evidenceRefs = [{
|
|
525
|
+
sourceKind: 'external',
|
|
526
|
+
sourceRef: `timer_synthesis:${synthesisHash}`,
|
|
527
|
+
relationKind: 'derived_from',
|
|
528
|
+
metadata: {
|
|
529
|
+
cadence: plan.cadence,
|
|
530
|
+
periodStart: canonicalInstant(plan.periodStart),
|
|
531
|
+
periodEnd: canonicalInstant(plan.periodEnd),
|
|
532
|
+
policyVersion: plan.policyVersion || 'v1',
|
|
533
|
+
sourceCanonicalKeys: lineage.sourceCanonicalKeys,
|
|
534
|
+
},
|
|
535
|
+
}];
|
|
536
|
+
const extracted = extractCandidatesFromStructuredSummary({
|
|
537
|
+
structuredSummary,
|
|
538
|
+
scopeKind: scope.scopeKind,
|
|
539
|
+
scopeKey: scope.scopeKey,
|
|
540
|
+
contextKey: scope.contextKey,
|
|
541
|
+
topicKey: scope.topicKey,
|
|
542
|
+
subject: opts.subject || `timer:${plan.cadence || 'manual'}`,
|
|
543
|
+
authority: opts.authority || 'verified_summary',
|
|
544
|
+
evidenceRefs,
|
|
545
|
+
});
|
|
546
|
+
const candidates = extracted.map((candidate, index) => ({
|
|
547
|
+
...candidate,
|
|
548
|
+
candidateHash: hashSnapshot({
|
|
549
|
+
synthesisHash,
|
|
550
|
+
index,
|
|
551
|
+
canonicalKey: candidate.canonicalKey,
|
|
552
|
+
summary: candidate.summary,
|
|
553
|
+
}),
|
|
554
|
+
payload: {
|
|
555
|
+
...(candidate.payload || {}),
|
|
556
|
+
kind: 'timer_synthesis',
|
|
557
|
+
synthesisKind: synthesisInput.kind || 'timer_current_memory_synthesis_v1',
|
|
558
|
+
currentMemoryRole: `${plan.cadence || 'manual'}_timer_synthesis_candidate`,
|
|
559
|
+
promotionGate: 'operator_required',
|
|
560
|
+
cadence: plan.cadence,
|
|
561
|
+
policyVersion: plan.policyVersion || 'v1',
|
|
562
|
+
periodStart: canonicalInstant(plan.periodStart),
|
|
563
|
+
periodEnd: canonicalInstant(plan.periodEnd),
|
|
564
|
+
synthesisHash,
|
|
565
|
+
sourceMemoryIds: lineage.sourceMemoryIds,
|
|
566
|
+
sourceCanonicalKeys: lineage.sourceCanonicalKeys,
|
|
567
|
+
safetyGate,
|
|
568
|
+
},
|
|
569
|
+
}));
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
summary,
|
|
573
|
+
safetyGate,
|
|
574
|
+
synthesisHash,
|
|
575
|
+
candidates,
|
|
576
|
+
sourceMemoryIds: lineage.sourceMemoryIds,
|
|
577
|
+
sourceCanonicalKeys: lineage.sourceCanonicalKeys,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function attachTimerSynthesis(plan = {}, synthesisSummary = {}, opts = {}) {
|
|
582
|
+
const synthesis = buildTimerSynthesisCandidates(plan, synthesisSummary, opts);
|
|
583
|
+
const includeAggregateCandidates = opts.includeAggregateCandidates === true;
|
|
584
|
+
const candidates = includeAggregateCandidates
|
|
585
|
+
? [...(Array.isArray(plan.candidates) ? plan.candidates : []), ...synthesis.candidates]
|
|
586
|
+
: synthesis.candidates;
|
|
587
|
+
const outputCoverage = {
|
|
588
|
+
...(plan.outputCoverage || plan.meta?.outputCoverage || {}),
|
|
589
|
+
candidateCount: candidates.length,
|
|
590
|
+
synthesizedCandidateCount: synthesis.candidates.length,
|
|
591
|
+
};
|
|
592
|
+
const inputHash = hashSnapshot({
|
|
593
|
+
baseInputHash: plan.inputHash,
|
|
594
|
+
synthesisHash: synthesis.synthesisHash,
|
|
595
|
+
candidateHashes: candidates.map(candidate => candidate.candidateHash || hashSnapshot(candidate)),
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
...plan,
|
|
599
|
+
inputHash,
|
|
600
|
+
candidates,
|
|
601
|
+
synthesisResult: {
|
|
602
|
+
summary: synthesis.summary,
|
|
603
|
+
safetyGate: synthesis.safetyGate,
|
|
604
|
+
synthesisHash: synthesis.synthesisHash,
|
|
605
|
+
candidateCount: synthesis.candidates.length,
|
|
606
|
+
sourceMemoryIds: synthesis.sourceMemoryIds,
|
|
607
|
+
sourceCanonicalKeys: synthesis.sourceCanonicalKeys,
|
|
608
|
+
},
|
|
609
|
+
outputCoverage,
|
|
610
|
+
meta: {
|
|
611
|
+
...(plan.meta || {}),
|
|
612
|
+
outputCoverage,
|
|
613
|
+
synthesisResult: {
|
|
614
|
+
synthesisHash: synthesis.synthesisHash,
|
|
615
|
+
candidateCount: synthesis.candidates.length,
|
|
616
|
+
safetyGate: synthesis.safetyGate,
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function buildPromotionReview(input = {}, opts = {}) {
|
|
623
|
+
const plan = input.plan || input;
|
|
624
|
+
const promotionResult = input.promotionResult || {};
|
|
625
|
+
const applyResult = input.applyResult || {};
|
|
626
|
+
const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
|
|
627
|
+
const statusUpdates = Array.isArray(plan.statusUpdates) ? plan.statusUpdates : [];
|
|
628
|
+
const candidateLines = candidates.slice(0, Math.max(0, Math.min(20, opts.limit || 8))).map(candidate => {
|
|
629
|
+
const type = candidate.memoryType || candidate.memory_type || 'memory';
|
|
630
|
+
const scope = candidate.scopeKey || candidate.scope_key || 'unspecified';
|
|
631
|
+
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
|
|
632
|
+
const sourceKeys = Array.isArray(payload.sourceCanonicalKeys) && payload.sourceCanonicalKeys.length > 0
|
|
633
|
+
? ` | sources=${payload.sourceCanonicalKeys.join(',')}`
|
|
634
|
+
: '';
|
|
635
|
+
return `- candidate ${type} ${scope}: ${compactSummary(candidate)}${sourceKeys}`;
|
|
636
|
+
});
|
|
637
|
+
const staleLines = statusUpdates.slice(0, Math.max(0, Math.min(20, opts.limit || 8))).map(update => (
|
|
638
|
+
`- ${update.status}: ${update.canonicalKey || update.memoryId || 'memory'}${update.reason ? ` (${update.reason})` : ''}`
|
|
639
|
+
));
|
|
640
|
+
const linesOrNone = lines => (lines.length > 0 ? lines.join('\n') : '- none');
|
|
641
|
+
return [
|
|
642
|
+
'Promotion review:',
|
|
643
|
+
`window: ${plan.cadence || 'manual'} ${canonicalInstant(plan.periodStart)} -> ${canonicalInstant(plan.periodEnd)}`,
|
|
644
|
+
`source: ${plan.synthesisInput?.sourceOfTruth || plan.meta?.synthesisInput?.sourceOfTruth || 'memory_records'}`,
|
|
645
|
+
`gate: ${input.promoteCandidates === true ? 'operator promotion requested' : 'candidate-only unless promoteCandidates=true'}`,
|
|
646
|
+
`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}`,
|
|
648
|
+
'candidate proposals:',
|
|
649
|
+
linesOrNone(candidateLines),
|
|
650
|
+
'status update proposals:',
|
|
651
|
+
linesOrNone(staleLines),
|
|
652
|
+
].join('\n');
|
|
653
|
+
}
|
|
654
|
+
|
|
331
655
|
function buildCoverage(normalized, statusUpdates, candidates) {
|
|
332
656
|
const active = normalized.filter(record => record.status === 'active');
|
|
333
657
|
const activeOpenLoops = active.filter(record => record.memoryType === 'open_loop');
|
|
@@ -460,6 +784,12 @@ function planCompaction(records = [], opts = {}) {
|
|
|
460
784
|
periodStart,
|
|
461
785
|
periodEnd,
|
|
462
786
|
});
|
|
787
|
+
const synthesisInput = buildTimerSynthesisInput(normalized, statusUpdates, candidates, {
|
|
788
|
+
...opts,
|
|
789
|
+
cadence,
|
|
790
|
+
periodStart,
|
|
791
|
+
periodEnd,
|
|
792
|
+
});
|
|
463
793
|
const coverage = buildCoverage(normalized, statusUpdates, candidates);
|
|
464
794
|
|
|
465
795
|
return {
|
|
@@ -469,6 +799,7 @@ function planCompaction(records = [], opts = {}) {
|
|
|
469
799
|
policyVersion: opts.policyVersion || 'v1',
|
|
470
800
|
inputHash,
|
|
471
801
|
candidates,
|
|
802
|
+
synthesisInput,
|
|
472
803
|
statusUpdates,
|
|
473
804
|
sourceCoverage: coverage.sourceCoverage,
|
|
474
805
|
outputCoverage: coverage.outputCoverage,
|
|
@@ -476,6 +807,7 @@ function planCompaction(records = [], opts = {}) {
|
|
|
476
807
|
activeConflictRate: 0,
|
|
477
808
|
deterministic: true,
|
|
478
809
|
recordCount: normalized.length,
|
|
810
|
+
synthesisInput,
|
|
479
811
|
sourceCoverage: coverage.sourceCoverage,
|
|
480
812
|
outputCoverage: coverage.outputCoverage,
|
|
481
813
|
},
|
|
@@ -1156,16 +1488,27 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1156
1488
|
periodEnd: window.periodEnd,
|
|
1157
1489
|
policyVersion: input.policyVersion || 'v1',
|
|
1158
1490
|
});
|
|
1491
|
+
const synthesisSummary = input.synthesisSummary || input.timerSynthesisSummary || null;
|
|
1492
|
+
const effectivePlan = synthesisSummary
|
|
1493
|
+
? attachTimerSynthesis(plan, synthesisSummary, {
|
|
1494
|
+
...input,
|
|
1495
|
+
tenantId,
|
|
1496
|
+
})
|
|
1497
|
+
: plan;
|
|
1159
1498
|
|
|
1160
1499
|
if (input.apply !== true) {
|
|
1161
1500
|
return {
|
|
1162
1501
|
job,
|
|
1163
1502
|
status: 'planned',
|
|
1164
1503
|
dryRun: true,
|
|
1165
|
-
plan,
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1504
|
+
plan: effectivePlan,
|
|
1505
|
+
synthesisPrompt: input.includeSynthesisPrompt === true && effectivePlan.synthesisInput
|
|
1506
|
+
? buildTimerSynthesisPrompt(effectivePlan, input)
|
|
1507
|
+
: undefined,
|
|
1508
|
+
promotionReview: buildPromotionReview({ plan: effectivePlan }),
|
|
1509
|
+
cadence: effectivePlan.cadence,
|
|
1510
|
+
periodStart: effectivePlan.periodStart,
|
|
1511
|
+
periodEnd: effectivePlan.periodEnd,
|
|
1169
1512
|
snapshotCount: snapshot.rows.length,
|
|
1170
1513
|
snapshotLimit: snapshot.snapshotLimit,
|
|
1171
1514
|
snapshotTruncated: snapshot.snapshotTruncated,
|
|
@@ -1176,7 +1519,7 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1176
1519
|
|
|
1177
1520
|
const result = input.promoteCandidates === true
|
|
1178
1521
|
? await executePlan({
|
|
1179
|
-
plan,
|
|
1522
|
+
plan: effectivePlan,
|
|
1180
1523
|
tenantId,
|
|
1181
1524
|
workerId: input.workerId,
|
|
1182
1525
|
applyToken: input.applyToken,
|
|
@@ -1186,7 +1529,7 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1186
1529
|
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
1187
1530
|
})
|
|
1188
1531
|
: await applyPlan({
|
|
1189
|
-
plan,
|
|
1532
|
+
plan: effectivePlan,
|
|
1190
1533
|
tenantId,
|
|
1191
1534
|
workerId: input.workerId,
|
|
1192
1535
|
applyToken: input.applyToken,
|
|
@@ -1194,21 +1537,27 @@ function createMemoryConsolidation({ pool, schema, defaultTenantId, records = nu
|
|
|
1194
1537
|
claimLeaseSeconds: input.claimLeaseSeconds,
|
|
1195
1538
|
reclaimStaleClaims: input.reclaimStaleClaims,
|
|
1196
1539
|
});
|
|
1197
|
-
const existingRun = result.run || await findExistingRun({ tenantId, plan });
|
|
1540
|
+
const existingRun = result.run || await findExistingRun({ tenantId, plan: effectivePlan });
|
|
1198
1541
|
return {
|
|
1199
1542
|
...result,
|
|
1200
1543
|
job,
|
|
1201
1544
|
dryRun: false,
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1545
|
+
promotionReview: buildPromotionReview({
|
|
1546
|
+
plan: effectivePlan,
|
|
1547
|
+
promotionResult: result.promotionResult,
|
|
1548
|
+
applyResult: result.applyResult,
|
|
1549
|
+
promoteCandidates: input.promoteCandidates === true,
|
|
1550
|
+
}),
|
|
1551
|
+
cadence: effectivePlan.cadence,
|
|
1552
|
+
periodStart: effectivePlan.periodStart,
|
|
1553
|
+
periodEnd: effectivePlan.periodEnd,
|
|
1205
1554
|
snapshotCount: snapshot.rows.length,
|
|
1206
1555
|
snapshotLimit: snapshot.snapshotLimit,
|
|
1207
1556
|
snapshotTruncated: snapshot.snapshotTruncated,
|
|
1208
1557
|
snapshotAsOf: snapshot.snapshotAsOf,
|
|
1209
1558
|
scopeKeys: snapshot.scopeKeys,
|
|
1210
1559
|
existingRun,
|
|
1211
|
-
skipReason: result.run ? null : classifySkippedRun(existingRun,
|
|
1560
|
+
skipReason: result.run ? null : classifySkippedRun(existingRun, effectivePlan),
|
|
1212
1561
|
};
|
|
1213
1562
|
}
|
|
1214
1563
|
|
|
@@ -1231,6 +1580,11 @@ module.exports = {
|
|
|
1231
1580
|
normalizeClaimLeaseSeconds,
|
|
1232
1581
|
resolveOperatorWindow,
|
|
1233
1582
|
planCompaction,
|
|
1583
|
+
buildTimerSynthesisInput,
|
|
1584
|
+
buildTimerSynthesisPrompt,
|
|
1585
|
+
buildTimerSynthesisCandidates,
|
|
1586
|
+
attachTimerSynthesis,
|
|
1587
|
+
buildPromotionReview,
|
|
1234
1588
|
distillArchiveSnapshot,
|
|
1235
1589
|
createMemoryConsolidation,
|
|
1236
1590
|
};
|