@shadowforge0/aquifer-memory 1.5.12 → 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 +78 -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 +353 -52
- 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 +96 -5
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +1 -1
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +27 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +372 -18
- package/core/finalization-review.js +319 -0
- package/core/mcp-manifest.js +52 -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 +385 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +25 -11
- 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/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -120
- 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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const REDACTION = '[REDACTED_SECRET]';
|
|
4
|
+
|
|
5
|
+
const SECRET_PATTERNS = [
|
|
6
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
7
|
+
/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
|
|
8
|
+
/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g,
|
|
9
|
+
/\b(AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN|DATABASE_URL)\s*=\s*[^\s]+/gi,
|
|
10
|
+
/\bAuthorization:\s*Bearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
11
|
+
/\b(cookie|set-cookie)\s*:\s*[^\n]+/gi,
|
|
12
|
+
/\bpostgres(?:ql)?:\/\/[^:\s/]+:[^@\s/]+@[^\s]+/gi,
|
|
13
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SESSION_INJECTION_RE = [
|
|
17
|
+
/^\s*\[AQUIFER CONTEXT\]/i,
|
|
18
|
+
/<session-bootstrap\b/i,
|
|
19
|
+
/<memory-bootstrap\b/i,
|
|
20
|
+
/^# AGENTS\.md instructions/i,
|
|
21
|
+
/<environment_context>/i,
|
|
22
|
+
/<developer_context>/i,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const TOOL_OUTPUT_RE = [
|
|
26
|
+
/^\s*(tool|command|shell|exec)_?output\s*:/i,
|
|
27
|
+
/^\s*Exit code:\s*\d+/im,
|
|
28
|
+
/^\s*Wall time:\s*[\d.]+/im,
|
|
29
|
+
/^\s*Output:\s*$/im,
|
|
30
|
+
/\bstdout\b[\s\S]*\bstderr\b/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const STACK_TRACE_RE = [
|
|
34
|
+
/Traceback \(most recent call last\):/,
|
|
35
|
+
/^\s+at .+\(.+:\d+:\d+\)$/m,
|
|
36
|
+
/^\s+at .+:\d+:\d+$/m,
|
|
37
|
+
/\bSQLSTATE\s+[A-Z0-9]{5}\b/i,
|
|
38
|
+
/\bduplicate key value violates unique constraint\b/i,
|
|
39
|
+
/\bsyntax error at or near\b/i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const COMMENTARY_RE = [
|
|
43
|
+
/^(I will|I'll|I'm going to|I’m going to|I will now)\b/i,
|
|
44
|
+
/^(我先|我會|接下來我|現在我會|我現在)\b/,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function extractText(message) {
|
|
48
|
+
if (!message || typeof message !== 'object') return '';
|
|
49
|
+
if (typeof message.content === 'string') return message.content;
|
|
50
|
+
if (Array.isArray(message.content)) {
|
|
51
|
+
return message.content
|
|
52
|
+
.filter(part => part && part.type === 'text' && typeof part.text === 'string')
|
|
53
|
+
.map(part => part.text)
|
|
54
|
+
.join('\n');
|
|
55
|
+
}
|
|
56
|
+
if (typeof message.text === 'string') return message.text;
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function replaceText(message, text) {
|
|
61
|
+
const next = { ...message };
|
|
62
|
+
if (typeof next.content === 'string') {
|
|
63
|
+
next.content = text;
|
|
64
|
+
} else if (Array.isArray(next.content)) {
|
|
65
|
+
let replaced = false;
|
|
66
|
+
next.content = next.content.map(part => {
|
|
67
|
+
if (!part || part.type !== 'text' || typeof part.text !== 'string') return part;
|
|
68
|
+
if (replaced) return { ...part, text: '' };
|
|
69
|
+
replaced = true;
|
|
70
|
+
return { ...part, text };
|
|
71
|
+
});
|
|
72
|
+
if (!replaced) next.content = text;
|
|
73
|
+
} else if (typeof next.text === 'string') {
|
|
74
|
+
next.text = text;
|
|
75
|
+
} else {
|
|
76
|
+
next.content = text;
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function redactSecrets(text) {
|
|
82
|
+
let redacted = String(text || '');
|
|
83
|
+
for (const re of SECRET_PATTERNS) {
|
|
84
|
+
redacted = redacted.replace(re, REDACTION);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
text: redacted,
|
|
88
|
+
redacted: redacted !== String(text || ''),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isEnvDump(text) {
|
|
93
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
94
|
+
let envLines = 0;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (/^[A-Z_][A-Z0-9_]{2,}=/.test(line.trim())) envLines++;
|
|
97
|
+
}
|
|
98
|
+
return envLines >= 3;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hasAny(patterns, text) {
|
|
102
|
+
return patterns.some(re => re.test(text));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assessTextForEnrich(text, role) {
|
|
106
|
+
const raw = String(text || '').trim();
|
|
107
|
+
const tags = [];
|
|
108
|
+
if (!raw) return { action: 'drop', reason: 'empty', tags: ['empty'], text: '' };
|
|
109
|
+
|
|
110
|
+
if (hasAny(SESSION_INJECTION_RE, raw)) tags.push('session_injected_context');
|
|
111
|
+
if (role === 'tool' || hasAny(TOOL_OUTPUT_RE, raw)) tags.push('tool_output');
|
|
112
|
+
if (hasAny(STACK_TRACE_RE, raw)) tags.push('stack_trace');
|
|
113
|
+
if (isEnvDump(raw)) tags.push('env_dump');
|
|
114
|
+
if (role === 'assistant' && hasAny(COMMENTARY_RE, raw)) tags.push('commentary');
|
|
115
|
+
|
|
116
|
+
const secretResult = redactSecrets(raw);
|
|
117
|
+
if (secretResult.redacted) tags.push('secret_risk');
|
|
118
|
+
|
|
119
|
+
const dropTags = new Set([
|
|
120
|
+
'session_injected_context',
|
|
121
|
+
'tool_output',
|
|
122
|
+
'stack_trace',
|
|
123
|
+
'env_dump',
|
|
124
|
+
'commentary',
|
|
125
|
+
]);
|
|
126
|
+
const dropReason = tags.find(tag => dropTags.has(tag));
|
|
127
|
+
if (dropReason) {
|
|
128
|
+
return { action: 'drop', reason: dropReason, tags, text: '' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
action: 'keep',
|
|
133
|
+
reason: secretResult.redacted ? 'redacted' : 'clean',
|
|
134
|
+
tags,
|
|
135
|
+
text: secretResult.text,
|
|
136
|
+
redacted: secretResult.redacted,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyEnrichSafetyGate(messages = []) {
|
|
141
|
+
const input = Array.isArray(messages) ? messages : [];
|
|
142
|
+
const safe = [];
|
|
143
|
+
const quarantined = [];
|
|
144
|
+
const stats = {
|
|
145
|
+
total: input.length,
|
|
146
|
+
kept: 0,
|
|
147
|
+
dropped: 0,
|
|
148
|
+
redacted: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < input.length; i++) {
|
|
152
|
+
const message = input[i];
|
|
153
|
+
const role = message && message.role ? String(message.role) : 'unknown';
|
|
154
|
+
const assessment = assessTextForEnrich(extractText(message), role);
|
|
155
|
+
if (assessment.action === 'drop') {
|
|
156
|
+
stats.dropped++;
|
|
157
|
+
quarantined.push({ index: i, role, reason: assessment.reason, tags: assessment.tags });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (assessment.redacted) stats.redacted++;
|
|
161
|
+
stats.kept++;
|
|
162
|
+
safe.push(replaceText(message, assessment.text));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
messages: safe,
|
|
167
|
+
meta: {
|
|
168
|
+
stats,
|
|
169
|
+
quarantined,
|
|
170
|
+
redacted: stats.redacted,
|
|
171
|
+
dropped: stats.dropped,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sanitizeStructuredValue(value, meta, role = 'assistant') {
|
|
177
|
+
if (typeof value === 'string') {
|
|
178
|
+
const assessment = assessTextForEnrich(value, role);
|
|
179
|
+
if (assessment.action === 'drop') {
|
|
180
|
+
meta.dropped++;
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
if (assessment.redacted) meta.redacted++;
|
|
184
|
+
return assessment.text;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
return value
|
|
188
|
+
.map(item => sanitizeStructuredValue(item, meta, role))
|
|
189
|
+
.filter(item => item !== undefined);
|
|
190
|
+
}
|
|
191
|
+
if (value && typeof value === 'object') {
|
|
192
|
+
const out = {};
|
|
193
|
+
for (const [key, child] of Object.entries(value)) {
|
|
194
|
+
const sanitized = sanitizeStructuredValue(child, meta, role);
|
|
195
|
+
if (sanitized !== undefined) out[key] = sanitized;
|
|
196
|
+
}
|
|
197
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sanitizeSummaryResult(summaryResult) {
|
|
203
|
+
if (!summaryResult) return { summaryResult: null, meta: { redacted: 0, dropped: 0 } };
|
|
204
|
+
const meta = { redacted: 0, dropped: 0 };
|
|
205
|
+
const next = { ...summaryResult };
|
|
206
|
+
if (typeof next.summaryText === 'string') {
|
|
207
|
+
const assessment = assessTextForEnrich(next.summaryText, 'assistant');
|
|
208
|
+
next.summaryText = assessment.action === 'drop' ? '' : assessment.text;
|
|
209
|
+
if (assessment.action === 'drop') meta.dropped++;
|
|
210
|
+
if (assessment.redacted) meta.redacted++;
|
|
211
|
+
}
|
|
212
|
+
if (next.structuredSummary) {
|
|
213
|
+
next.structuredSummary = sanitizeStructuredValue(next.structuredSummary, meta, 'assistant');
|
|
214
|
+
}
|
|
215
|
+
return { summaryResult: next, meta };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
REDACTION,
|
|
220
|
+
redactSecrets,
|
|
221
|
+
assessTextForEnrich,
|
|
222
|
+
applyEnrichSafetyGate,
|
|
223
|
+
sanitizeSummaryResult,
|
|
224
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const storage = require('./storage');
|
|
4
|
+
const { createMemoryRecords } = require('./memory-records');
|
|
5
|
+
const { createMemoryPromotion } = require('./memory-promotion');
|
|
6
|
+
const { sanitizeSummaryResult } = require('./memory-safety-gate');
|
|
7
|
+
const { buildFinalizationReview, buildSessionStartContext } = require('./finalization-review');
|
|
8
|
+
|
|
9
|
+
function qi(identifier) { return `"${identifier}"`; }
|
|
10
|
+
|
|
11
|
+
function requireField(obj, field) {
|
|
12
|
+
if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
|
|
13
|
+
throw new Error(`${field} is required`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasStructuredContent(value) {
|
|
18
|
+
return value && typeof value === 'object' && Object.keys(value).length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TERMINAL_SUPPRESSION_STATUSES = new Set(['skipped', 'declined', 'deferred']);
|
|
22
|
+
|
|
23
|
+
function countByReason(results) {
|
|
24
|
+
const reasons = {};
|
|
25
|
+
for (const result of results || []) {
|
|
26
|
+
const reason = result && result.reason ? result.reason : 'unknown';
|
|
27
|
+
reasons[reason] = (reasons[reason] || 0) + 1;
|
|
28
|
+
}
|
|
29
|
+
return reasons;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function summarizeMemoryResults(results = [], extra = {}) {
|
|
33
|
+
return {
|
|
34
|
+
candidates: results.length,
|
|
35
|
+
promoted: results.filter(result => result.action === 'promote').length,
|
|
36
|
+
quarantined: results.filter(result => result.action === 'quarantine').length,
|
|
37
|
+
skipped: results.filter(result => result.action && !['promote', 'quarantine'].includes(result.action)).length,
|
|
38
|
+
reasons: countByReason(results),
|
|
39
|
+
...extra,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeFinalizationInput(input = {}, defaults = {}) {
|
|
44
|
+
const tenantId = input.tenantId || defaults.defaultTenantId || 'default';
|
|
45
|
+
return {
|
|
46
|
+
tenantId,
|
|
47
|
+
sessionId: input.sessionId,
|
|
48
|
+
agentId: input.agentId || 'main',
|
|
49
|
+
source: input.source || 'codex',
|
|
50
|
+
host: input.host || 'codex',
|
|
51
|
+
transcriptHash: input.transcriptHash,
|
|
52
|
+
phase: input.phase || 'curated_memory_v1',
|
|
53
|
+
mode: input.mode || 'handoff',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSessionFinalization({
|
|
58
|
+
pool,
|
|
59
|
+
schema,
|
|
60
|
+
recordsSchema,
|
|
61
|
+
defaultTenantId = 'default',
|
|
62
|
+
}) {
|
|
63
|
+
const memorySchema = recordsSchema || qi(schema);
|
|
64
|
+
|
|
65
|
+
async function createTask(input = {}) {
|
|
66
|
+
const base = normalizeFinalizationInput(input, { defaultTenantId });
|
|
67
|
+
requireField(base, 'sessionId');
|
|
68
|
+
requireField(base, 'agentId');
|
|
69
|
+
requireField(base, 'source');
|
|
70
|
+
requireField(base, 'transcriptHash');
|
|
71
|
+
|
|
72
|
+
let sessionRowId = input.sessionRowId || null;
|
|
73
|
+
if (!sessionRowId) {
|
|
74
|
+
const session = await storage.getSession(
|
|
75
|
+
pool,
|
|
76
|
+
base.sessionId,
|
|
77
|
+
base.agentId,
|
|
78
|
+
{ tenantId: base.tenantId, source: base.source },
|
|
79
|
+
{ schema, tenantId: base.tenantId },
|
|
80
|
+
);
|
|
81
|
+
if (!session) {
|
|
82
|
+
throw new Error(`Session not found: ${base.sessionId} (agentId=${base.agentId}, source=${base.source})`);
|
|
83
|
+
}
|
|
84
|
+
sessionRowId = session.id;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return storage.upsertSessionFinalization(pool, {
|
|
88
|
+
...base,
|
|
89
|
+
sessionRowId,
|
|
90
|
+
status: input.status || 'pending',
|
|
91
|
+
finalizerModel: input.finalizerModel || null,
|
|
92
|
+
scopeKind: input.scopeKind || null,
|
|
93
|
+
scopeKey: input.scopeKey || null,
|
|
94
|
+
contextKey: input.contextKey || null,
|
|
95
|
+
topicKey: input.topicKey || null,
|
|
96
|
+
memoryResult: input.memoryResult || {},
|
|
97
|
+
error: input.error || null,
|
|
98
|
+
metadata: input.metadata || {},
|
|
99
|
+
claimedAt: input.claimedAt || null,
|
|
100
|
+
finalizedAt: input.finalizedAt || null,
|
|
101
|
+
}, { schema, tenantId: base.tenantId });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function get(input = {}) {
|
|
105
|
+
const base = normalizeFinalizationInput(input, { defaultTenantId });
|
|
106
|
+
return storage.getSessionFinalization(pool, base, { schema, tenantId: base.tenantId });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function list(input = {}) {
|
|
110
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
111
|
+
return storage.listSessionFinalizations(pool, input, { schema, tenantId });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function updateStatus(input = {}) {
|
|
115
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
116
|
+
return storage.updateSessionFinalizationStatus(pool, input, { schema, tenantId });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function finalizeSession(input = {}) {
|
|
120
|
+
const base = normalizeFinalizationInput(input, { defaultTenantId });
|
|
121
|
+
requireField(base, 'sessionId');
|
|
122
|
+
requireField(base, 'agentId');
|
|
123
|
+
requireField(base, 'source');
|
|
124
|
+
requireField(base, 'transcriptHash');
|
|
125
|
+
|
|
126
|
+
const summaryText = String(input.summaryText || '').trim();
|
|
127
|
+
const structuredSummary = input.structuredSummary || {};
|
|
128
|
+
if (!summaryText && !hasStructuredContent(structuredSummary)) {
|
|
129
|
+
throw new Error('summaryText or structuredSummary is required');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const client = await pool.connect();
|
|
133
|
+
let failureSession = null;
|
|
134
|
+
let processingTask = null;
|
|
135
|
+
try {
|
|
136
|
+
await client.query('BEGIN');
|
|
137
|
+
|
|
138
|
+
const sessionResult = await client.query(
|
|
139
|
+
`SELECT *
|
|
140
|
+
FROM ${qi(schema)}.sessions
|
|
141
|
+
WHERE tenant_id = $1
|
|
142
|
+
AND agent_id = $2
|
|
143
|
+
AND session_id = $3
|
|
144
|
+
AND source = $4
|
|
145
|
+
FOR UPDATE`,
|
|
146
|
+
[base.tenantId, base.agentId, base.sessionId, base.source],
|
|
147
|
+
);
|
|
148
|
+
const session = sessionResult.rows[0] || null;
|
|
149
|
+
if (!session) {
|
|
150
|
+
throw new Error(`Session not found: ${base.sessionId} (agentId=${base.agentId}, source=${base.source})`);
|
|
151
|
+
}
|
|
152
|
+
failureSession = session;
|
|
153
|
+
|
|
154
|
+
const existing = await storage.getSessionFinalization(client, base, {
|
|
155
|
+
schema,
|
|
156
|
+
tenantId: base.tenantId,
|
|
157
|
+
});
|
|
158
|
+
if (existing && existing.status === 'finalized') {
|
|
159
|
+
await client.query('COMMIT');
|
|
160
|
+
return {
|
|
161
|
+
status: 'already_finalized',
|
|
162
|
+
finalization: existing,
|
|
163
|
+
memoryResult: existing.memory_result || {},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (existing && TERMINAL_SUPPRESSION_STATUSES.has(existing.status)) {
|
|
167
|
+
await client.query('COMMIT');
|
|
168
|
+
return {
|
|
169
|
+
status: 'suppressed',
|
|
170
|
+
finalizationStatus: existing.status,
|
|
171
|
+
finalization: existing,
|
|
172
|
+
memoryResult: existing.memory_result || {},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
processingTask = await storage.upsertSessionFinalization(client, {
|
|
177
|
+
...base,
|
|
178
|
+
sessionRowId: session.id,
|
|
179
|
+
status: 'processing',
|
|
180
|
+
finalizerModel: input.finalizerModel || input.model || null,
|
|
181
|
+
scopeKind: input.scopeKind || null,
|
|
182
|
+
scopeKey: input.scopeKey || null,
|
|
183
|
+
contextKey: input.contextKey || null,
|
|
184
|
+
topicKey: input.topicKey || null,
|
|
185
|
+
metadata: input.metadata || {},
|
|
186
|
+
claimedAt: input.claimedAt || new Date().toISOString(),
|
|
187
|
+
}, { schema, tenantId: base.tenantId });
|
|
188
|
+
|
|
189
|
+
const sanitized = sanitizeSummaryResult({ summaryText, structuredSummary });
|
|
190
|
+
const safeSummary = sanitized.summaryResult || {};
|
|
191
|
+
const safeStructuredSummary = safeSummary.structuredSummary || {};
|
|
192
|
+
const safeSummaryText = safeSummary.summaryText || summaryText;
|
|
193
|
+
const finalizerModel = input.finalizerModel || input.model || session.model || null;
|
|
194
|
+
|
|
195
|
+
const summaryRow = await storage.upsertSummary(client, session.id, {
|
|
196
|
+
schema,
|
|
197
|
+
tenantId: base.tenantId,
|
|
198
|
+
agentId: base.agentId,
|
|
199
|
+
sessionId: base.sessionId,
|
|
200
|
+
summaryText: safeSummaryText,
|
|
201
|
+
structuredSummary: safeStructuredSummary,
|
|
202
|
+
model: finalizerModel,
|
|
203
|
+
sourceHash: base.transcriptHash,
|
|
204
|
+
msgCount: input.msgCount || input.messageCount || session.msg_count || 0,
|
|
205
|
+
userCount: input.userCount || session.user_count || 0,
|
|
206
|
+
assistantCount: input.assistantCount || session.assistant_count || 0,
|
|
207
|
+
startedAt: input.startedAt || session.started_at || null,
|
|
208
|
+
endedAt: input.endedAt || session.ended_at || session.last_message_at || null,
|
|
209
|
+
embedding: input.embedding || null,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const records = createMemoryRecords({
|
|
213
|
+
pool: client,
|
|
214
|
+
schema: memorySchema,
|
|
215
|
+
defaultTenantId: base.tenantId,
|
|
216
|
+
inTransaction: true,
|
|
217
|
+
});
|
|
218
|
+
const promotion = createMemoryPromotion({ records });
|
|
219
|
+
const evidenceRefs = [{
|
|
220
|
+
sourceKind: 'session_summary',
|
|
221
|
+
sourceRef: base.sessionId,
|
|
222
|
+
relationKind: 'primary',
|
|
223
|
+
metadata: {
|
|
224
|
+
transcriptHash: base.transcriptHash,
|
|
225
|
+
finalizationId: processingTask ? processingTask.id : null,
|
|
226
|
+
mode: base.mode,
|
|
227
|
+
phase: base.phase,
|
|
228
|
+
},
|
|
229
|
+
}];
|
|
230
|
+
const candidates = Array.isArray(input.candidates)
|
|
231
|
+
? input.candidates
|
|
232
|
+
: promotion.extractCandidates({
|
|
233
|
+
sessionId: base.sessionId,
|
|
234
|
+
structuredSummary: safeStructuredSummary,
|
|
235
|
+
scopeKind: input.scopeKind || null,
|
|
236
|
+
scopeKey: input.scopeKey || null,
|
|
237
|
+
contextKey: input.contextKey || null,
|
|
238
|
+
topicKey: input.topicKey || null,
|
|
239
|
+
authority: input.authority || 'verified_summary',
|
|
240
|
+
evidenceRefs,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const memoryResults = candidates.length > 0
|
|
244
|
+
? await promotion.promote(candidates, {
|
|
245
|
+
tenantId: base.tenantId,
|
|
246
|
+
acceptedAt: input.acceptedAt || new Date().toISOString(),
|
|
247
|
+
createdByFinalizationId: processingTask ? processingTask.id : null,
|
|
248
|
+
})
|
|
249
|
+
: [];
|
|
250
|
+
if (processingTask && memoryResults.length > 0) {
|
|
251
|
+
await storage.upsertFinalizationCandidates(client, memoryResults, {
|
|
252
|
+
tenantId: base.tenantId,
|
|
253
|
+
finalizationId: processingTask.id,
|
|
254
|
+
sessionId: base.sessionId,
|
|
255
|
+
}, { schema, tenantId: base.tenantId });
|
|
256
|
+
}
|
|
257
|
+
const memoryResult = summarizeMemoryResults(memoryResults, {
|
|
258
|
+
safetyGate: sanitized.meta || {},
|
|
259
|
+
});
|
|
260
|
+
const promotedMemories = memoryResults
|
|
261
|
+
.filter(result => result && result.action === 'promote' && result.memory)
|
|
262
|
+
.map(result => result.memory);
|
|
263
|
+
const humanReviewText = buildFinalizationReview({
|
|
264
|
+
summary: {
|
|
265
|
+
summaryText: safeSummaryText,
|
|
266
|
+
structuredSummary: safeStructuredSummary,
|
|
267
|
+
},
|
|
268
|
+
memoryResult,
|
|
269
|
+
memoryResults,
|
|
270
|
+
sessionId: base.sessionId,
|
|
271
|
+
transcriptHash: base.transcriptHash,
|
|
272
|
+
finalization: processingTask,
|
|
273
|
+
});
|
|
274
|
+
const sessionStartText = buildSessionStartContext(promotedMemories);
|
|
275
|
+
|
|
276
|
+
const finalization = await storage.upsertSessionFinalization(client, {
|
|
277
|
+
...base,
|
|
278
|
+
sessionRowId: session.id,
|
|
279
|
+
status: 'finalized',
|
|
280
|
+
finalizerModel,
|
|
281
|
+
scopeKind: input.scopeKind || null,
|
|
282
|
+
scopeKey: input.scopeKey || null,
|
|
283
|
+
contextKey: input.contextKey || null,
|
|
284
|
+
topicKey: input.topicKey || null,
|
|
285
|
+
summaryRowId: summaryRow ? summaryRow.session_row_id : session.id,
|
|
286
|
+
memoryResult,
|
|
287
|
+
summaryText: safeSummaryText,
|
|
288
|
+
structuredSummary: safeStructuredSummary,
|
|
289
|
+
humanReviewText,
|
|
290
|
+
sessionStartText,
|
|
291
|
+
metadata: {
|
|
292
|
+
...(input.metadata || {}),
|
|
293
|
+
safetyGate: sanitized.meta || {},
|
|
294
|
+
},
|
|
295
|
+
}, { schema, tenantId: base.tenantId });
|
|
296
|
+
|
|
297
|
+
await storage.markStatus(client, session.id, 'succeeded', null, { schema });
|
|
298
|
+
await client.query('COMMIT');
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
status: 'finalized',
|
|
302
|
+
finalization,
|
|
303
|
+
summary: {
|
|
304
|
+
summaryText: safeSummaryText,
|
|
305
|
+
structuredSummary: safeStructuredSummary,
|
|
306
|
+
},
|
|
307
|
+
memoryResult,
|
|
308
|
+
memoryResults,
|
|
309
|
+
humanReviewText,
|
|
310
|
+
sessionStartText,
|
|
311
|
+
};
|
|
312
|
+
} catch (error) {
|
|
313
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
314
|
+
if (failureSession) {
|
|
315
|
+
try {
|
|
316
|
+
await storage.upsertSessionFinalization(pool, {
|
|
317
|
+
...base,
|
|
318
|
+
sessionRowId: failureSession.id,
|
|
319
|
+
status: 'failed',
|
|
320
|
+
finalizerModel: input.finalizerModel || input.model || null,
|
|
321
|
+
scopeKind: input.scopeKind || null,
|
|
322
|
+
scopeKey: input.scopeKey || null,
|
|
323
|
+
contextKey: input.contextKey || null,
|
|
324
|
+
topicKey: input.topicKey || null,
|
|
325
|
+
metadata: input.metadata || {},
|
|
326
|
+
error: error.message,
|
|
327
|
+
}, { schema, tenantId: base.tenantId });
|
|
328
|
+
} catch {
|
|
329
|
+
// The original finalization failure is the useful error. A secondary
|
|
330
|
+
// ledger failure should not hide it.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
} finally {
|
|
335
|
+
client.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
createTask,
|
|
341
|
+
get,
|
|
342
|
+
list,
|
|
343
|
+
updateStatus,
|
|
344
|
+
finalizeSession,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
createSessionFinalization,
|
|
350
|
+
};
|