@shadowforge0/aquifer-memory 1.8.0 → 1.9.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 +1 -0
- package/README.md +49 -22
- package/README_CN.md +24 -22
- package/README_TW.md +20 -22
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +560 -4
- package/consumers/codex.js +1 -1
- package/consumers/mcp.js +3 -0
- package/consumers/openclaw-ext/index.js +64 -6
- 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 +39 -1
- package/consumers/shared/config.js +2 -0
- package/core/aquifer.js +180 -33
- package/core/backends/local.js +109 -0
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/memory-explain.js +624 -0
- package/core/memory-recall.js +49 -23
- package/core/memory-records.js +16 -5
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +13 -0
- package/core/session-finalization.js +76 -1
- package/core/storage.js +124 -8
- package/docs/getting-started.md +34 -1
- package/docs/setup.md +102 -22
- package/package.json +5 -4
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
package/core/memory-serving.js
CHANGED
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function splitScopePath(value) {
|
|
4
|
-
if (Array.isArray(value))
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
const parts = value.map(v => String(v).trim()).filter(Boolean);
|
|
6
|
+
return parts.length > 0 ? parts : null;
|
|
7
|
+
}
|
|
5
8
|
if (typeof value !== 'string') return null;
|
|
6
9
|
const parts = value.split(',').map(v => v.trim()).filter(Boolean);
|
|
7
10
|
return parts.length > 0 ? parts : null;
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
function uniqueList(values = []) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const value of values) {
|
|
17
|
+
const key = String(value || '').trim();
|
|
18
|
+
if (!key || seen.has(key)) continue;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push(key);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeScopeList(value) {
|
|
26
|
+
return uniqueList(splitScopePath(value) || []);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requestedScopeKeys(opts = {}) {
|
|
30
|
+
const fromPath = normalizeScopeList(opts.activeScopePath);
|
|
31
|
+
const keys = [];
|
|
32
|
+
if (fromPath.length > 0) keys.push(...fromPath);
|
|
33
|
+
if (opts.activeScopeKey) keys.push(opts.activeScopeKey);
|
|
34
|
+
if (opts.scopeKey) keys.push(opts.scopeKey);
|
|
35
|
+
if (opts.resolvedScopeKey) keys.push(opts.resolvedScopeKey);
|
|
36
|
+
if (opts.scopeKeys) keys.push(...normalizeScopeList(opts.scopeKeys));
|
|
37
|
+
return uniqueList(keys);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertAllowedScopeRequest(opts = {}) {
|
|
41
|
+
const allowed = normalizeScopeList(opts.allowedScopeKeys);
|
|
42
|
+
if (allowed.length === 0) return;
|
|
43
|
+
const allowedSet = new Set(allowed);
|
|
44
|
+
const requested = requestedScopeKeys(opts);
|
|
45
|
+
const denied = requested.filter(key => !allowedSet.has(key));
|
|
46
|
+
if (denied.length > 0) {
|
|
47
|
+
throw new Error(`Requested memory scope is outside allowedScopeKeys: ${denied.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
10
51
|
function hasEvidenceBoundary(opts = {}) {
|
|
11
52
|
return Boolean(
|
|
12
53
|
opts.agentId
|
|
@@ -89,7 +130,13 @@ function normalizeCuratedRecallRow(row = {}) {
|
|
|
89
130
|
function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
|
|
90
131
|
const servingMode = memoryCfg.servingMode || env.AQUIFER_MEMORY_SERVING_MODE || 'legacy';
|
|
91
132
|
const defaultActiveScopeKey = memoryCfg.activeScopeKey || null;
|
|
92
|
-
const
|
|
133
|
+
const configuredActiveScopePath = splitScopePath(memoryCfg.activeScopePath || null);
|
|
134
|
+
const defaultActiveScopePath = configuredActiveScopePath
|
|
135
|
+
|| uniqueList(['global', defaultActiveScopeKey].filter(Boolean));
|
|
136
|
+
const configuredAllowedScopeKeys = normalizeScopeList(memoryCfg.allowedScopeKeys || env.AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS || null);
|
|
137
|
+
const defaultAllowedScopeKeys = configuredAllowedScopeKeys.length > 0
|
|
138
|
+
? configuredAllowedScopeKeys
|
|
139
|
+
: defaultActiveScopePath;
|
|
93
140
|
|
|
94
141
|
function resolveMode(opts = {}) {
|
|
95
142
|
const mode = opts.memoryMode || opts.servingMode || servingMode;
|
|
@@ -100,12 +147,18 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
|
|
|
100
147
|
|
|
101
148
|
function withDefaultScope(opts = {}) {
|
|
102
149
|
const next = { ...opts };
|
|
103
|
-
|
|
150
|
+
const scopeIdOnly = next.scopeId && !next.activeScopePath && !next.activeScopeKey;
|
|
151
|
+
if (!next.allowedScopeKeys && defaultAllowedScopeKeys) {
|
|
152
|
+
next.allowedScopeKeys = defaultAllowedScopeKeys;
|
|
153
|
+
}
|
|
154
|
+
if (!scopeIdOnly && !next.activeScopePath && defaultActiveScopePath) next.activeScopePath = defaultActiveScopePath;
|
|
104
155
|
if (Array.isArray(next.activeScopePath) && next.activeScopePath.length > 0) {
|
|
105
156
|
if (!next.activeScopeKey) next.activeScopeKey = next.activeScopePath[next.activeScopePath.length - 1];
|
|
157
|
+
assertAllowedScopeRequest(next);
|
|
106
158
|
return next;
|
|
107
159
|
}
|
|
108
|
-
if (!next.activeScopeKey && defaultActiveScopeKey) next.activeScopeKey = defaultActiveScopeKey;
|
|
160
|
+
if (!scopeIdOnly && !next.activeScopeKey && defaultActiveScopeKey) next.activeScopeKey = defaultActiveScopeKey;
|
|
161
|
+
assertAllowedScopeRequest(next);
|
|
109
162
|
return next;
|
|
110
163
|
}
|
|
111
164
|
|
|
@@ -114,6 +167,7 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
|
|
|
114
167
|
assertCuratedRecallOpts,
|
|
115
168
|
defaultActiveScopeKey,
|
|
116
169
|
defaultActiveScopePath,
|
|
170
|
+
defaultAllowedScopeKeys,
|
|
117
171
|
hasEvidenceBoundary,
|
|
118
172
|
normalizeCuratedRecallRow,
|
|
119
173
|
resolveMode,
|
|
@@ -123,10 +177,13 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
|
|
|
123
177
|
}
|
|
124
178
|
|
|
125
179
|
module.exports = {
|
|
180
|
+
assertAllowedScopeRequest,
|
|
126
181
|
assertCuratedBootstrapOpts,
|
|
127
182
|
assertCuratedRecallOpts,
|
|
128
183
|
createMemoryServingRuntime,
|
|
129
184
|
hasEvidenceBoundary,
|
|
130
185
|
normalizeCuratedRecallRow,
|
|
186
|
+
normalizeScopeList,
|
|
187
|
+
requestedScopeKeys,
|
|
131
188
|
splitScopePath,
|
|
132
189
|
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function parseJsonObject(value, fallback = {}) {
|
|
4
|
+
if (value === null || value === undefined) return fallback;
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(value);
|
|
8
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rowTime(value) {
|
|
17
|
+
if (!value) return null;
|
|
18
|
+
const parsed = new Date(value);
|
|
19
|
+
return isNaN(parsed.getTime()) ? String(value) : parsed.toISOString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summarizeCompactionRun(row = {}) {
|
|
23
|
+
const output = parseJsonObject(row.output, {});
|
|
24
|
+
const plan = output.plan || output;
|
|
25
|
+
const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
|
|
26
|
+
const statusUpdates = Array.isArray(plan.statusUpdates || plan.status_updates)
|
|
27
|
+
? (plan.statusUpdates || plan.status_updates)
|
|
28
|
+
: [];
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
kind: 'compaction',
|
|
32
|
+
status: row.status,
|
|
33
|
+
cadence: row.cadence,
|
|
34
|
+
periodStart: row.period_start,
|
|
35
|
+
periodEnd: row.period_end,
|
|
36
|
+
policyVersion: row.policy_version || null,
|
|
37
|
+
workerId: row.worker_id || null,
|
|
38
|
+
claimedAt: rowTime(row.claimed_at),
|
|
39
|
+
leaseExpiresAt: rowTime(row.lease_expires_at),
|
|
40
|
+
appliedAt: rowTime(row.applied_at),
|
|
41
|
+
reclaimedAt: rowTime(row.reclaimed_at),
|
|
42
|
+
error: row.error || null,
|
|
43
|
+
sourceCoverage: parseJsonObject(row.source_coverage, {}),
|
|
44
|
+
outputCoverage: parseJsonObject(row.output_coverage, {}),
|
|
45
|
+
candidateCount: candidates.length,
|
|
46
|
+
statusUpdateCount: statusUpdates.length,
|
|
47
|
+
createdAt: rowTime(row.created_at),
|
|
48
|
+
updatedAt: rowTime(row.updated_at),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summarizeCheckpointRun(row = {}, sourceCount = 0) {
|
|
53
|
+
const payload = parseJsonObject(row.checkpoint_payload, {});
|
|
54
|
+
return {
|
|
55
|
+
id: row.id,
|
|
56
|
+
kind: 'checkpoint',
|
|
57
|
+
status: row.status,
|
|
58
|
+
scopeId: row.scope_id,
|
|
59
|
+
checkpointKey: row.checkpoint_key,
|
|
60
|
+
fromFinalizationIdExclusive: row.from_finalization_id_exclusive,
|
|
61
|
+
toFinalizationIdInclusive: row.to_finalization_id_inclusive,
|
|
62
|
+
windowStart: rowTime(row.window_start),
|
|
63
|
+
windowEnd: rowTime(row.window_end),
|
|
64
|
+
claimedAt: rowTime(row.claimed_at),
|
|
65
|
+
finalizedAt: rowTime(row.finalized_at),
|
|
66
|
+
error: row.error || null,
|
|
67
|
+
sourceCount,
|
|
68
|
+
checkpointPayloadKeys: Object.keys(payload).sort(),
|
|
69
|
+
createdAt: rowTime(row.created_at),
|
|
70
|
+
updatedAt: rowTime(row.updated_at),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sourceValue(row = {}, key) {
|
|
75
|
+
const metadata = parseJsonObject(row.metadata, {});
|
|
76
|
+
return row[key] ?? metadata[key] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tableMissingError(error) {
|
|
80
|
+
return error && error.code === '42P01';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createOperatorObservability({ pool, schema, defaultTenantId = 'default' }) {
|
|
84
|
+
async function compactionStatus(input = {}) {
|
|
85
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
86
|
+
const limit = Math.max(1, Math.min(50, input.limit || 10));
|
|
87
|
+
try {
|
|
88
|
+
const [latest, stale, counts] = await Promise.all([
|
|
89
|
+
pool.query(
|
|
90
|
+
`SELECT *
|
|
91
|
+
FROM ${schema}.compaction_runs
|
|
92
|
+
WHERE tenant_id = $1
|
|
93
|
+
ORDER BY COALESCE(applied_at, created_at) DESC, id DESC
|
|
94
|
+
LIMIT $2`,
|
|
95
|
+
[tenantId, limit],
|
|
96
|
+
),
|
|
97
|
+
pool.query(
|
|
98
|
+
`SELECT *
|
|
99
|
+
FROM ${schema}.compaction_runs
|
|
100
|
+
WHERE tenant_id = $1
|
|
101
|
+
AND status = 'applying'
|
|
102
|
+
AND lease_expires_at IS NOT NULL
|
|
103
|
+
AND lease_expires_at < transaction_timestamp()
|
|
104
|
+
ORDER BY lease_expires_at ASC, id ASC
|
|
105
|
+
LIMIT $2`,
|
|
106
|
+
[tenantId, limit],
|
|
107
|
+
),
|
|
108
|
+
pool.query(
|
|
109
|
+
`SELECT status, COUNT(*)::int AS count
|
|
110
|
+
FROM ${schema}.compaction_runs
|
|
111
|
+
WHERE tenant_id = $1
|
|
112
|
+
GROUP BY status`,
|
|
113
|
+
[tenantId],
|
|
114
|
+
),
|
|
115
|
+
]);
|
|
116
|
+
return {
|
|
117
|
+
available: true,
|
|
118
|
+
latest: latest.rows.map(summarizeCompactionRun),
|
|
119
|
+
staleClaims: stale.rows.map(summarizeCompactionRun),
|
|
120
|
+
statusCounts: Object.fromEntries(counts.rows.map(row => [row.status, row.count])),
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (tableMissingError(error)) {
|
|
124
|
+
return { available: false, latest: [], staleClaims: [], statusCounts: {}, error: 'compaction_runs table is missing' };
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function checkpointStatus(input = {}) {
|
|
131
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
132
|
+
const limit = Math.max(1, Math.min(50, input.limit || 10));
|
|
133
|
+
try {
|
|
134
|
+
const [latest, counts] = await Promise.all([
|
|
135
|
+
pool.query(
|
|
136
|
+
`SELECT c.*, COUNT(s.id)::int AS source_count
|
|
137
|
+
FROM ${schema}.checkpoint_runs c
|
|
138
|
+
LEFT JOIN ${schema}.checkpoint_run_sources s
|
|
139
|
+
ON s.tenant_id = c.tenant_id
|
|
140
|
+
AND s.checkpoint_run_id = c.id
|
|
141
|
+
WHERE c.tenant_id = $1
|
|
142
|
+
GROUP BY c.id
|
|
143
|
+
ORDER BY c.updated_at DESC, c.id DESC
|
|
144
|
+
LIMIT $2`,
|
|
145
|
+
[tenantId, limit],
|
|
146
|
+
),
|
|
147
|
+
pool.query(
|
|
148
|
+
`SELECT status, COUNT(*)::int AS count
|
|
149
|
+
FROM ${schema}.checkpoint_runs
|
|
150
|
+
WHERE tenant_id = $1
|
|
151
|
+
GROUP BY status`,
|
|
152
|
+
[tenantId],
|
|
153
|
+
),
|
|
154
|
+
]);
|
|
155
|
+
return {
|
|
156
|
+
available: true,
|
|
157
|
+
latest: latest.rows.map(row => summarizeCheckpointRun(row, row.source_count || 0)),
|
|
158
|
+
statusCounts: Object.fromEntries(counts.rows.map(row => [row.status, row.count])),
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (tableMissingError(error)) {
|
|
162
|
+
return { available: false, latest: [], statusCounts: {}, error: 'checkpoint_runs table is missing' };
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function status(input = {}) {
|
|
169
|
+
const [compaction, checkpoint] = await Promise.all([
|
|
170
|
+
compactionStatus(input),
|
|
171
|
+
checkpointStatus(input),
|
|
172
|
+
]);
|
|
173
|
+
return {
|
|
174
|
+
readOnly: true,
|
|
175
|
+
compaction,
|
|
176
|
+
checkpoint,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function inspect(input = {}) {
|
|
181
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
182
|
+
const runId = input.runId || input.id;
|
|
183
|
+
if (!runId) throw new Error('runId is required');
|
|
184
|
+
const kind = input.kind || 'compaction';
|
|
185
|
+
if (kind === 'compaction') {
|
|
186
|
+
const result = await pool.query(
|
|
187
|
+
`SELECT *
|
|
188
|
+
FROM ${schema}.compaction_runs
|
|
189
|
+
WHERE tenant_id = $1
|
|
190
|
+
AND id = $2
|
|
191
|
+
LIMIT 1`,
|
|
192
|
+
[tenantId, runId],
|
|
193
|
+
);
|
|
194
|
+
if (!result.rows[0]) throw new Error('Compaction run not found');
|
|
195
|
+
return {
|
|
196
|
+
readOnly: true,
|
|
197
|
+
run: summarizeCompactionRun(result.rows[0]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (kind === 'checkpoint') {
|
|
201
|
+
const [run, sources] = await Promise.all([
|
|
202
|
+
pool.query(
|
|
203
|
+
`SELECT *
|
|
204
|
+
FROM ${schema}.checkpoint_runs
|
|
205
|
+
WHERE tenant_id = $1
|
|
206
|
+
AND id = $2
|
|
207
|
+
LIMIT 1`,
|
|
208
|
+
[tenantId, runId],
|
|
209
|
+
),
|
|
210
|
+
pool.query(
|
|
211
|
+
`SELECT id, finalization_id, source_index, session_id, transcript_hash, finalized_at, metadata
|
|
212
|
+
FROM ${schema}.checkpoint_run_sources
|
|
213
|
+
WHERE tenant_id = $1
|
|
214
|
+
AND checkpoint_run_id = $2
|
|
215
|
+
ORDER BY source_index ASC, id ASC`,
|
|
216
|
+
[tenantId, runId],
|
|
217
|
+
),
|
|
218
|
+
]);
|
|
219
|
+
if (!run.rows[0]) throw new Error('Checkpoint run not found');
|
|
220
|
+
return {
|
|
221
|
+
readOnly: true,
|
|
222
|
+
run: summarizeCheckpointRun(run.rows[0], sources.rows.length),
|
|
223
|
+
sources: sources.rows.map(row => ({
|
|
224
|
+
id: row.id,
|
|
225
|
+
finalizationId: row.finalization_id,
|
|
226
|
+
sourceIndex: row.source_index,
|
|
227
|
+
sessionId: row.session_id,
|
|
228
|
+
agentId: sourceValue(row, 'agentId') || sourceValue(row, 'agent_id'),
|
|
229
|
+
source: sourceValue(row, 'source'),
|
|
230
|
+
status: sourceValue(row, 'status'),
|
|
231
|
+
transcriptHashPrefix: row.transcript_hash ? String(row.transcript_hash).slice(0, 12) : null,
|
|
232
|
+
finalizedAt: rowTime(row.finalized_at),
|
|
233
|
+
})),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
throw new Error('kind must be compaction or checkpoint');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
status,
|
|
241
|
+
inspect,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
createOperatorObservability,
|
|
247
|
+
summarizeCompactionRun,
|
|
248
|
+
summarizeCheckpointRun,
|
|
249
|
+
};
|
|
@@ -81,6 +81,18 @@ const MIGRATION_PLAN = [
|
|
|
81
81
|
{ table: 'finalization_candidates', column: 'candidate_hash' },
|
|
82
82
|
],
|
|
83
83
|
},
|
|
84
|
+
{
|
|
85
|
+
id: '019-v1-memory-review-resolutions',
|
|
86
|
+
file: '019-v1-memory-review-resolutions.sql',
|
|
87
|
+
always: true,
|
|
88
|
+
signature: [
|
|
89
|
+
'memory_review_resolutions',
|
|
90
|
+
{ index: 'idx_memory_review_resolutions_memory_latest' },
|
|
91
|
+
{ index: 'idx_memory_review_resolutions_canonical_latest' },
|
|
92
|
+
{ index: 'idx_memory_review_resolutions_defer_until' },
|
|
93
|
+
{ index: 'idx_feedback_memory_review_latest' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
84
96
|
];
|
|
85
97
|
|
|
86
98
|
function createPostgresMigrationRuntime(opts = {}) {
|
|
@@ -296,6 +308,7 @@ function createPostgresMigrationRuntime(opts = {}) {
|
|
|
296
308
|
['016-v1-evidence-ref-multi-item.sql', '016-v1-evidence-ref-multi-item'],
|
|
297
309
|
['017-v1-memory-record-embeddings.sql', '017-v1-memory-record-embeddings'],
|
|
298
310
|
['018-v1-finalization-candidate-envelope.sql', '018-v1-finalization-candidate-envelope'],
|
|
311
|
+
['019-v1-memory-review-resolutions.sql', '019-v1-memory-review-resolutions'],
|
|
299
312
|
]) {
|
|
300
313
|
await client.query(loadSql(migration[0], schema));
|
|
301
314
|
ddlExecuted.push(migration[1]);
|
|
@@ -6,6 +6,7 @@ const { createMemoryRecords } = require('./memory-records');
|
|
|
6
6
|
const { createMemoryPromotion } = require('./memory-promotion');
|
|
7
7
|
const { sanitizeSummaryResult } = require('./memory-safety-gate');
|
|
8
8
|
const { buildFinalizationReview, buildSessionStartContext } = require('./finalization-review');
|
|
9
|
+
const { buildFinalizationInspection } = require('./finalization-inspector');
|
|
9
10
|
|
|
10
11
|
function qi(identifier) { return `"${identifier}"`; }
|
|
11
12
|
|
|
@@ -15,6 +16,13 @@ function requireField(obj, field) {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function finalizationReadError(error) {
|
|
20
|
+
if (error && error.code === '42P01') {
|
|
21
|
+
return new Error('Finalization observability tables are not available. Run `aquifer migrate`, then retry this read-only command.');
|
|
22
|
+
}
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
function hasStructuredContent(value) {
|
|
19
27
|
return value && typeof value === 'object' && Object.keys(value).length > 0;
|
|
20
28
|
}
|
|
@@ -190,7 +198,73 @@ function createSessionFinalization({
|
|
|
190
198
|
|
|
191
199
|
async function list(input = {}) {
|
|
192
200
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
193
|
-
|
|
201
|
+
try {
|
|
202
|
+
return await storage.listSessionFinalizations(pool, input, { schema, tenantId });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw finalizationReadError(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function inspect(input = {}) {
|
|
209
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
210
|
+
let row = null;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
if (input.id) {
|
|
214
|
+
row = await storage.getSessionFinalizationById(pool, {
|
|
215
|
+
tenantId,
|
|
216
|
+
id: input.id,
|
|
217
|
+
}, { schema, tenantId });
|
|
218
|
+
} else {
|
|
219
|
+
requireField(input, 'sessionId');
|
|
220
|
+
requireField(input, 'agentId');
|
|
221
|
+
requireField(input, 'source');
|
|
222
|
+
const phase = input.phase || 'curated_memory_v1';
|
|
223
|
+
if (input.transcriptHash) {
|
|
224
|
+
row = await storage.getSessionFinalization(pool, {
|
|
225
|
+
tenantId,
|
|
226
|
+
sessionId: input.sessionId,
|
|
227
|
+
agentId: input.agentId,
|
|
228
|
+
source: input.source,
|
|
229
|
+
transcriptHash: input.transcriptHash,
|
|
230
|
+
phase,
|
|
231
|
+
}, { schema, tenantId });
|
|
232
|
+
} else {
|
|
233
|
+
const rows = await storage.listSessionFinalizations(pool, {
|
|
234
|
+
tenantId,
|
|
235
|
+
sessionId: input.sessionId,
|
|
236
|
+
agentId: input.agentId,
|
|
237
|
+
source: input.source,
|
|
238
|
+
phase,
|
|
239
|
+
limit: 2,
|
|
240
|
+
}, { schema, tenantId });
|
|
241
|
+
if (rows.length > 1) {
|
|
242
|
+
const matches = rows.map(match => {
|
|
243
|
+
const hash = match.transcript_hash ? String(match.transcript_hash).slice(0, 12) : '?';
|
|
244
|
+
const updatedAt = match.updated_at || '?';
|
|
245
|
+
return `#${match.id} status=${match.status} phase=${match.phase} hash=${hash} updated=${updatedAt}`;
|
|
246
|
+
}).join('; ');
|
|
247
|
+
throw new Error(`Multiple finalizations matched. Add --transcript-hash or --id to inspect one row. Matches: ${matches}`);
|
|
248
|
+
}
|
|
249
|
+
row = rows[0] || null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!row) throw new Error('Finalization not found');
|
|
254
|
+
const [candidates, lineage] = await Promise.all([
|
|
255
|
+
storage.listFinalizationCandidates(pool, {
|
|
256
|
+
tenantId,
|
|
257
|
+
finalizationId: row.id,
|
|
258
|
+
}, { schema, tenantId }),
|
|
259
|
+
storage.getFinalizationLineageSummary(pool, {
|
|
260
|
+
tenantId,
|
|
261
|
+
finalizationId: row.id,
|
|
262
|
+
}, { schema, tenantId }),
|
|
263
|
+
]);
|
|
264
|
+
return buildFinalizationInspection(row, candidates, lineage);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw finalizationReadError(error);
|
|
267
|
+
}
|
|
194
268
|
}
|
|
195
269
|
|
|
196
270
|
async function updateStatus(input = {}) {
|
|
@@ -436,6 +510,7 @@ function createSessionFinalization({
|
|
|
436
510
|
createTask,
|
|
437
511
|
get,
|
|
438
512
|
list,
|
|
513
|
+
inspect,
|
|
439
514
|
updateStatus,
|
|
440
515
|
finalizeSession,
|
|
441
516
|
};
|
package/core/storage.js
CHANGED
|
@@ -614,6 +614,20 @@ async function getSessionFinalization(pool, input = {}, { schema, tenantId: defa
|
|
|
614
614
|
return result.rows[0] || null;
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
+
async function getSessionFinalizationById(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
618
|
+
requireField(input, 'id');
|
|
619
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
620
|
+
const result = await pool.query(
|
|
621
|
+
`SELECT *
|
|
622
|
+
FROM ${qi(schema)}.session_finalizations
|
|
623
|
+
WHERE tenant_id = $1
|
|
624
|
+
AND id = $2
|
|
625
|
+
LIMIT 1`,
|
|
626
|
+
[tenantId, input.id]
|
|
627
|
+
);
|
|
628
|
+
return result.rows[0] || null;
|
|
629
|
+
}
|
|
630
|
+
|
|
617
631
|
async function updateSessionFinalizationStatus(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
618
632
|
const status = normalizeFinalizationStatus(input.status);
|
|
619
633
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
@@ -660,38 +674,137 @@ async function updateSessionFinalizationStatus(pool, input = {}, { schema, tenan
|
|
|
660
674
|
|
|
661
675
|
async function listSessionFinalizations(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
662
676
|
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
677
|
+
const table = `${qi(schema)}.session_finalizations`;
|
|
663
678
|
const params = [tenantId];
|
|
664
|
-
const where = [
|
|
679
|
+
const where = [`${table}.tenant_id = $1`];
|
|
665
680
|
if (input.host) {
|
|
666
681
|
params.push(input.host);
|
|
667
|
-
where.push(
|
|
682
|
+
where.push(`${table}.host = $${params.length}`);
|
|
668
683
|
}
|
|
669
684
|
if (input.status) {
|
|
670
685
|
const statuses = Array.isArray(input.status) ? input.status : [input.status];
|
|
671
686
|
for (const status of statuses) normalizeFinalizationStatus(status);
|
|
672
687
|
params.push(statuses);
|
|
673
|
-
where.push(
|
|
688
|
+
where.push(`${table}.status = ANY($${params.length}::text[])`);
|
|
674
689
|
}
|
|
675
690
|
if (input.agentId) {
|
|
676
691
|
params.push(input.agentId);
|
|
677
|
-
where.push(
|
|
692
|
+
where.push(`${table}.agent_id = $${params.length}`);
|
|
678
693
|
}
|
|
679
694
|
if (input.source) {
|
|
680
695
|
params.push(input.source);
|
|
681
|
-
where.push(
|
|
696
|
+
where.push(`${table}.source = $${params.length}`);
|
|
697
|
+
}
|
|
698
|
+
if (input.sessionId) {
|
|
699
|
+
params.push(input.sessionId);
|
|
700
|
+
where.push(`${table}.session_id = $${params.length}`);
|
|
701
|
+
}
|
|
702
|
+
if (input.transcriptHash) {
|
|
703
|
+
params.push(input.transcriptHash);
|
|
704
|
+
where.push(`${table}.transcript_hash = $${params.length}`);
|
|
705
|
+
}
|
|
706
|
+
if (input.phase) {
|
|
707
|
+
params.push(input.phase);
|
|
708
|
+
where.push(`${table}.phase = $${params.length}`);
|
|
709
|
+
}
|
|
710
|
+
if (input.mode) {
|
|
711
|
+
params.push(normalizeFinalizationMode(input.mode));
|
|
712
|
+
where.push(`${table}.mode = $${params.length}`);
|
|
682
713
|
}
|
|
683
714
|
params.push(Math.max(1, Math.min(200, input.limit || 50)));
|
|
684
715
|
const result = await pool.query(
|
|
685
|
-
`SELECT
|
|
686
|
-
|
|
716
|
+
`SELECT
|
|
717
|
+
${table}.id,
|
|
718
|
+
${table}.tenant_id,
|
|
719
|
+
${table}.source,
|
|
720
|
+
${table}.host,
|
|
721
|
+
${table}.agent_id,
|
|
722
|
+
${table}.session_id,
|
|
723
|
+
${table}.transcript_hash,
|
|
724
|
+
${table}.phase,
|
|
725
|
+
${table}.mode,
|
|
726
|
+
${table}.status,
|
|
727
|
+
${table}.finalizer_model,
|
|
728
|
+
${table}.scope_kind,
|
|
729
|
+
${table}.scope_key,
|
|
730
|
+
${table}.context_key,
|
|
731
|
+
${table}.topic_key,
|
|
732
|
+
${table}.candidate_envelope_hash,
|
|
733
|
+
${table}.candidate_envelope_version,
|
|
734
|
+
${table}.error,
|
|
735
|
+
${table}.claimed_at,
|
|
736
|
+
${table}.finalized_at,
|
|
737
|
+
${table}.created_at,
|
|
738
|
+
${table}.updated_at,
|
|
739
|
+
(
|
|
740
|
+
SELECT COUNT(*)::int
|
|
741
|
+
FROM ${qi(schema)}.finalization_candidates fc
|
|
742
|
+
WHERE fc.tenant_id = ${table}.tenant_id
|
|
743
|
+
AND fc.finalization_id = ${table}.id
|
|
744
|
+
) AS candidate_count
|
|
745
|
+
FROM ${table}
|
|
687
746
|
WHERE ${where.join(' AND ')}
|
|
688
|
-
ORDER BY updated_at DESC, id DESC
|
|
747
|
+
ORDER BY ${table}.updated_at DESC, ${table}.id DESC
|
|
689
748
|
LIMIT $${params.length}`,
|
|
690
749
|
params
|
|
691
750
|
);
|
|
692
751
|
return result.rows;
|
|
693
752
|
}
|
|
694
753
|
|
|
754
|
+
async function getFinalizationLineageSummary(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
755
|
+
requireField(input, 'finalizationId');
|
|
756
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
757
|
+
const result = await pool.query(
|
|
758
|
+
`SELECT
|
|
759
|
+
COALESCE((
|
|
760
|
+
SELECT array_agg(id ORDER BY id)
|
|
761
|
+
FROM ${qi(schema)}.memory_records
|
|
762
|
+
WHERE tenant_id = $1
|
|
763
|
+
AND created_by_finalization_id = $2
|
|
764
|
+
), ARRAY[]::bigint[]) AS memory_record_ids,
|
|
765
|
+
COALESCE((
|
|
766
|
+
SELECT array_agg(id ORDER BY id)
|
|
767
|
+
FROM ${qi(schema)}.fact_assertions_v1
|
|
768
|
+
WHERE tenant_id = $1
|
|
769
|
+
AND created_by_finalization_id = $2
|
|
770
|
+
), ARRAY[]::bigint[]) AS fact_assertion_ids,
|
|
771
|
+
COALESCE((
|
|
772
|
+
SELECT COUNT(*)::int
|
|
773
|
+
FROM ${qi(schema)}.evidence_refs
|
|
774
|
+
WHERE tenant_id = $1
|
|
775
|
+
AND created_by_finalization_id = $2
|
|
776
|
+
), 0) AS evidence_ref_count,
|
|
777
|
+
COALESCE((
|
|
778
|
+
SELECT COUNT(*)::int
|
|
779
|
+
FROM ${qi(schema)}.evidence_items
|
|
780
|
+
WHERE tenant_id = $1
|
|
781
|
+
AND created_by_finalization_id = $2
|
|
782
|
+
), 0) AS evidence_item_count`,
|
|
783
|
+
[tenantId, input.finalizationId]
|
|
784
|
+
);
|
|
785
|
+
const row = result.rows[0] || {};
|
|
786
|
+
return {
|
|
787
|
+
memoryRecordIds: row.memory_record_ids || [],
|
|
788
|
+
factAssertionIds: row.fact_assertion_ids || [],
|
|
789
|
+
evidenceRefCount: row.evidence_ref_count || 0,
|
|
790
|
+
evidenceItemCount: row.evidence_item_count || 0,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function listFinalizationCandidates(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
|
|
795
|
+
requireField(input, 'finalizationId');
|
|
796
|
+
const tenantId = input.tenantId || defaultTenantId || 'default';
|
|
797
|
+
const result = await pool.query(
|
|
798
|
+
`SELECT *
|
|
799
|
+
FROM ${qi(schema)}.finalization_candidates
|
|
800
|
+
WHERE tenant_id = $1
|
|
801
|
+
AND finalization_id = $2
|
|
802
|
+
ORDER BY candidate_index ASC, id ASC`,
|
|
803
|
+
[tenantId, input.finalizationId]
|
|
804
|
+
);
|
|
805
|
+
return result.rows;
|
|
806
|
+
}
|
|
807
|
+
|
|
695
808
|
function candidateText(candidate = {}) {
|
|
696
809
|
if (typeof candidate === 'string') return candidate.trim();
|
|
697
810
|
const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : null;
|
|
@@ -1236,8 +1349,11 @@ module.exports = {
|
|
|
1236
1349
|
recordAccess,
|
|
1237
1350
|
upsertSessionFinalization,
|
|
1238
1351
|
getSessionFinalization,
|
|
1352
|
+
getSessionFinalizationById,
|
|
1239
1353
|
updateSessionFinalizationStatus,
|
|
1240
1354
|
listSessionFinalizations,
|
|
1355
|
+
getFinalizationLineageSummary,
|
|
1356
|
+
listFinalizationCandidates,
|
|
1241
1357
|
upsertCheckpointRun,
|
|
1242
1358
|
updateCheckpointRunStatus,
|
|
1243
1359
|
listCheckpointRuns,
|