@shadowforge0/aquifer-memory 1.8.1 → 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 +535 -2
- 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
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveApplicableRecords } = require('./memory-bootstrap');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LIMIT = 20;
|
|
6
|
+
const MAX_LIMIT = 100;
|
|
7
|
+
const MAX_FETCH_LIMIT = 400;
|
|
8
|
+
const DEFAULT_VISIBILITY = 'either';
|
|
9
|
+
const DEFAULT_RESOLUTION_LIMIT = 10;
|
|
10
|
+
const RESOLUTION_ACTIONS = ['resolved', 'ignored', 'deferred'];
|
|
11
|
+
|
|
12
|
+
const ISSUE_FEEDBACK_TYPES = [
|
|
13
|
+
'incorrect',
|
|
14
|
+
'sensitive',
|
|
15
|
+
'scope_mismatch',
|
|
16
|
+
'authority_mismatch',
|
|
17
|
+
'stale',
|
|
18
|
+
'expired',
|
|
19
|
+
'conflict',
|
|
20
|
+
'superseded',
|
|
21
|
+
'archive',
|
|
22
|
+
'irrelevant',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const SEVERITY_SCORE = {
|
|
26
|
+
incorrect: 100,
|
|
27
|
+
sensitive: 95,
|
|
28
|
+
scope_mismatch: 85,
|
|
29
|
+
authority_mismatch: 80,
|
|
30
|
+
stale: 75,
|
|
31
|
+
expired: 70,
|
|
32
|
+
conflict: 65,
|
|
33
|
+
superseded: 60,
|
|
34
|
+
archive: 50,
|
|
35
|
+
irrelevant: 40,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function clampLimit(value, fallback = DEFAULT_LIMIT) {
|
|
39
|
+
const parsed = Number.parseInt(value, 10);
|
|
40
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
41
|
+
return Math.min(MAX_LIMIT, parsed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseCounts(value) {
|
|
45
|
+
if (!value || typeof value !== 'object') return {};
|
|
46
|
+
return Object.fromEntries(
|
|
47
|
+
Object.entries(value)
|
|
48
|
+
.map(([key, count]) => [key, Number.parseInt(count, 10) || 0])
|
|
49
|
+
.filter(([, count]) => count > 0)
|
|
50
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function severityCaseSql() {
|
|
55
|
+
const clauses = Object.entries(SEVERITY_SCORE)
|
|
56
|
+
.map(([type, score]) => `WHEN '${type}' THEN ${score}`)
|
|
57
|
+
.join(' ');
|
|
58
|
+
return `CASE feedback_type ${clauses} ELSE 0 END`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeFeedbackType(value) {
|
|
62
|
+
if (!value || value === true) return null;
|
|
63
|
+
const type = String(value).trim();
|
|
64
|
+
return type || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeList(value) {
|
|
68
|
+
if (Array.isArray(value)) return value.map(String).map(s => s.trim()).filter(Boolean);
|
|
69
|
+
if (!value || value === true) return [];
|
|
70
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeVisibility(value) {
|
|
74
|
+
if (!value || value === true) return DEFAULT_VISIBILITY;
|
|
75
|
+
const visibility = String(value).trim();
|
|
76
|
+
if (!['bootstrap', 'recall', 'either', 'both'].includes(visibility)) {
|
|
77
|
+
throw new Error('review visibility must be one of: bootstrap, recall, either, both');
|
|
78
|
+
}
|
|
79
|
+
return visibility;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeIsoOrNull(value) {
|
|
83
|
+
if (!value || value === true) return null;
|
|
84
|
+
const date = new Date(value);
|
|
85
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requireIso(value, label) {
|
|
89
|
+
const iso = normalizeIsoOrNull(value);
|
|
90
|
+
if (!iso) throw new Error(`${label} must be a valid ISO timestamp`);
|
|
91
|
+
return iso;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeReason(value) {
|
|
95
|
+
if (!value || value === true) return null;
|
|
96
|
+
const text = String(value).replace(/\s+/g, ' ').trim();
|
|
97
|
+
if (!text) return null;
|
|
98
|
+
return text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeActorKind(value) {
|
|
102
|
+
if (!value || value === true) return 'user';
|
|
103
|
+
const actorKind = String(value).trim();
|
|
104
|
+
if (!['user', 'agent', 'system', 'curator'].includes(actorKind)) {
|
|
105
|
+
throw new Error('review resolution actorKind must be one of: user, agent, system, curator');
|
|
106
|
+
}
|
|
107
|
+
return actorKind;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeResolution(value) {
|
|
111
|
+
if (!value || value === true) throw new Error('review.resolve requires resolution');
|
|
112
|
+
const resolution = String(value).trim();
|
|
113
|
+
if (!RESOLUTION_ACTIONS.includes(resolution)) {
|
|
114
|
+
throw new Error('review resolution must be one of: resolved, ignored, deferred');
|
|
115
|
+
}
|
|
116
|
+
return resolution;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scopePathFromOpts(opts = {}) {
|
|
120
|
+
const path = normalizeList(opts.activeScopePath);
|
|
121
|
+
if (path.length > 0) return path;
|
|
122
|
+
const key = opts.scopeKey || opts.activeScopeKey || 'global';
|
|
123
|
+
return [String(key)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scopeContextFromOpts(opts = {}) {
|
|
127
|
+
const activeScopePath = scopePathFromOpts(opts);
|
|
128
|
+
return {
|
|
129
|
+
activeScopeKey: opts.activeScopeKey || opts.scopeKey || activeScopePath[activeScopePath.length - 1] || 'global',
|
|
130
|
+
activeScopePath,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function issueTypes(input) {
|
|
135
|
+
return Array.isArray(input) && input.length > 0
|
|
136
|
+
? input.map(String)
|
|
137
|
+
: ISSUE_FEEDBACK_TYPES.slice();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function issueTypesForResolution(input = {}) {
|
|
141
|
+
return issueTypes(input.issueFeedbackTypes);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function severityLabel(score) {
|
|
145
|
+
const value = Number.parseInt(score, 10) || 0;
|
|
146
|
+
if (value >= 85) return 'high';
|
|
147
|
+
if (value >= 65) return 'medium';
|
|
148
|
+
return 'low';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeQueueRow(row = {}) {
|
|
152
|
+
const severityScore = Number.parseInt(row.severity_score, 10) || 0;
|
|
153
|
+
const resolution = normalizeResolutionRow({
|
|
154
|
+
id: row.resolution_id,
|
|
155
|
+
memory_id: row.resolution_memory_id || row.memory_id,
|
|
156
|
+
canonical_key: row.resolution_canonical_key || row.canonical_key,
|
|
157
|
+
scope_id: row.resolution_scope_id || row.scope_id,
|
|
158
|
+
resolution: row.resolution,
|
|
159
|
+
reason: row.resolution_reason,
|
|
160
|
+
actor_kind: row.resolution_actor_kind,
|
|
161
|
+
actor_id: row.resolution_actor_id,
|
|
162
|
+
issue_feedback_types: row.resolution_issue_feedback_types,
|
|
163
|
+
resolved_through_feedback_id: row.resolution_resolved_through_feedback_id,
|
|
164
|
+
resolved_through_feedback_at: row.resolution_resolved_through_feedback_at,
|
|
165
|
+
defer_until: row.resolution_defer_until,
|
|
166
|
+
resolved_at: row.resolved_at,
|
|
167
|
+
created_at: row.resolution_created_at,
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
memoryId: row.memory_id === null || row.memory_id === undefined ? null : String(row.memory_id),
|
|
171
|
+
canonicalKey: row.canonical_key,
|
|
172
|
+
memoryType: row.memory_type,
|
|
173
|
+
status: row.status,
|
|
174
|
+
visibility: {
|
|
175
|
+
bootstrap: row.visible_in_bootstrap === true,
|
|
176
|
+
recall: row.visible_in_recall === true,
|
|
177
|
+
},
|
|
178
|
+
authority: row.authority || null,
|
|
179
|
+
acceptedAt: row.accepted_at || null,
|
|
180
|
+
updatedAt: row.updated_at || null,
|
|
181
|
+
scope: {
|
|
182
|
+
id: row.scope_id === null || row.scope_id === undefined ? null : String(row.scope_id),
|
|
183
|
+
kind: row.scope_kind || null,
|
|
184
|
+
key: row.scope_key || null,
|
|
185
|
+
inheritanceMode: row.inheritance_mode || null,
|
|
186
|
+
},
|
|
187
|
+
feedbackCounts: parseCounts(row.feedback_counts),
|
|
188
|
+
feedbackCount: Number.parseInt(row.feedback_count, 10) || 0,
|
|
189
|
+
issueCount: Number.parseInt(row.issue_count, 10) || 0,
|
|
190
|
+
matchingFeedbackCount: Number.parseInt(row.matching_feedback_count, 10) || 0,
|
|
191
|
+
severity: severityLabel(severityScore),
|
|
192
|
+
severityScore,
|
|
193
|
+
firstFeedbackAt: row.first_feedback_at || null,
|
|
194
|
+
latestFeedbackAt: row.latest_feedback_at || null,
|
|
195
|
+
latestIssueFeedbackId: row.latest_issue_feedback_id === null || row.latest_issue_feedback_id === undefined
|
|
196
|
+
? null : String(row.latest_issue_feedback_id),
|
|
197
|
+
latestIssueFeedbackAt: row.latest_issue_feedback_at || null,
|
|
198
|
+
resolution,
|
|
199
|
+
queueState: row.queue_state || (resolution ? resolution.resolution : 'open'),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeFeedbackEvent(row = {}) {
|
|
204
|
+
return {
|
|
205
|
+
id: row.id === null || row.id === undefined ? null : String(row.id),
|
|
206
|
+
feedbackType: row.feedback_type,
|
|
207
|
+
actorKind: row.actor_kind || null,
|
|
208
|
+
actorId: row.actor_id || null,
|
|
209
|
+
createdAt: row.created_at || null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeResolutionRow(row = {}) {
|
|
214
|
+
if (!row || row.id === null || row.id === undefined) return null;
|
|
215
|
+
return {
|
|
216
|
+
id: String(row.id),
|
|
217
|
+
memoryId: row.memory_id === null || row.memory_id === undefined ? null : String(row.memory_id),
|
|
218
|
+
canonicalKey: row.canonical_key || null,
|
|
219
|
+
scopeId: row.scope_id === null || row.scope_id === undefined ? null : String(row.scope_id),
|
|
220
|
+
resolution: row.resolution,
|
|
221
|
+
reason: row.reason || null,
|
|
222
|
+
actorKind: row.actor_kind || null,
|
|
223
|
+
actorId: row.actor_id || null,
|
|
224
|
+
issueFeedbackTypes: Array.isArray(row.issue_feedback_types) ? row.issue_feedback_types : [],
|
|
225
|
+
resolvedThroughFeedbackId: row.resolved_through_feedback_id === null || row.resolved_through_feedback_id === undefined
|
|
226
|
+
? null : String(row.resolved_through_feedback_id),
|
|
227
|
+
resolvedThroughFeedbackAt: row.resolved_through_feedback_at || null,
|
|
228
|
+
deferUntil: row.defer_until || null,
|
|
229
|
+
resolvedAt: row.resolved_at || null,
|
|
230
|
+
createdAt: row.created_at || null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compareIso(left, right) {
|
|
235
|
+
if (!left || !right) return null;
|
|
236
|
+
const leftTime = new Date(left).getTime();
|
|
237
|
+
const rightTime = new Date(right).getTime();
|
|
238
|
+
if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) return null;
|
|
239
|
+
if (leftTime === rightTime) return 0;
|
|
240
|
+
return leftTime > rightTime ? 1 : -1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createMemoryReview({ pool, schema, defaultTenantId }) {
|
|
244
|
+
function assertPool() {
|
|
245
|
+
if (!pool || typeof pool.query !== 'function') {
|
|
246
|
+
throw new Error('memory.review requires a pool with query(sql, params)');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildQueueQuery(opts = {}) {
|
|
251
|
+
const params = [opts.tenantId || defaultTenantId, issueTypes(opts.issueFeedbackTypes)];
|
|
252
|
+
const filters = [];
|
|
253
|
+
const feedbackType = normalizeFeedbackType(opts.feedbackType);
|
|
254
|
+
const memoryTypes = normalizeList(opts.memoryType || opts.memoryTypes);
|
|
255
|
+
const visibility = normalizeVisibility(opts.visibility);
|
|
256
|
+
const scopeContext = scopeContextFromOpts(opts);
|
|
257
|
+
const severityExpr = severityCaseSql();
|
|
258
|
+
const includeResolved = opts.includeResolved === true;
|
|
259
|
+
const feedbackFilters = [];
|
|
260
|
+
let matchTypeExpr = `NULL::text`;
|
|
261
|
+
|
|
262
|
+
if (opts.dateFrom) {
|
|
263
|
+
params.push(String(opts.dateFrom));
|
|
264
|
+
feedbackFilters.push(`created_at >= $${params.length}`);
|
|
265
|
+
}
|
|
266
|
+
if (opts.dateTo) {
|
|
267
|
+
params.push(String(opts.dateTo));
|
|
268
|
+
feedbackFilters.push(`created_at < ($${params.length}::date + INTERVAL '1 day')`);
|
|
269
|
+
}
|
|
270
|
+
if (feedbackType && feedbackType !== 'all') {
|
|
271
|
+
params.push(feedbackType);
|
|
272
|
+
matchTypeExpr = `$${params.length}::text`;
|
|
273
|
+
filters.push(`r.matching_feedback_count > 0`);
|
|
274
|
+
} else if (feedbackType === 'all') {
|
|
275
|
+
filters.push(`r.feedback_count > 0`);
|
|
276
|
+
} else {
|
|
277
|
+
filters.push(`r.issue_count > 0`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
params.push(scopeContext.activeScopePath);
|
|
281
|
+
filters.push(`s.scope_key = ANY($${params.length}::text[])`);
|
|
282
|
+
filters.push(`m.status = 'active'`);
|
|
283
|
+
params.push(normalizeIsoOrNull(opts.asOf));
|
|
284
|
+
const asOfRef = `$${params.length}::timestamptz`;
|
|
285
|
+
filters.push(`(m.valid_from IS NULL OR m.valid_from <= COALESCE(${asOfRef}, now()))`);
|
|
286
|
+
filters.push(`(m.valid_to IS NULL OR m.valid_to > COALESCE(${asOfRef}, now()))`);
|
|
287
|
+
filters.push(`(m.stale_after IS NULL OR m.stale_after > COALESCE(${asOfRef}, now()))`);
|
|
288
|
+
if (opts.canonicalKey) {
|
|
289
|
+
params.push(String(opts.canonicalKey));
|
|
290
|
+
filters.push(`m.canonical_key = $${params.length}`);
|
|
291
|
+
}
|
|
292
|
+
if (memoryTypes.length > 0) {
|
|
293
|
+
params.push(memoryTypes);
|
|
294
|
+
filters.push(`m.memory_type = ANY($${params.length}::text[])`);
|
|
295
|
+
}
|
|
296
|
+
if (visibility === 'bootstrap') {
|
|
297
|
+
filters.push(`m.visible_in_bootstrap = true`);
|
|
298
|
+
} else if (visibility === 'recall') {
|
|
299
|
+
filters.push(`m.visible_in_recall = true`);
|
|
300
|
+
} else if (visibility === 'both') {
|
|
301
|
+
filters.push(`m.visible_in_bootstrap = true AND m.visible_in_recall = true`);
|
|
302
|
+
} else {
|
|
303
|
+
filters.push(`(m.visible_in_bootstrap = true OR m.visible_in_recall = true)`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const requestedLimit = clampLimit(opts.limit);
|
|
307
|
+
const fetchLimit = Math.min(MAX_FETCH_LIMIT, Math.max(requestedLimit * 4, requestedLimit));
|
|
308
|
+
params.push(fetchLimit);
|
|
309
|
+
const limitRef = `$${params.length}`;
|
|
310
|
+
return {
|
|
311
|
+
params,
|
|
312
|
+
sql: `
|
|
313
|
+
WITH feedback_counts AS (
|
|
314
|
+
SELECT
|
|
315
|
+
target_id,
|
|
316
|
+
feedback_type,
|
|
317
|
+
COUNT(*)::int AS count,
|
|
318
|
+
MIN(created_at) AS first_feedback_at,
|
|
319
|
+
MAX(created_at) AS latest_feedback_at,
|
|
320
|
+
MAX(id)::bigint AS latest_feedback_id,
|
|
321
|
+
MAX(${severityExpr})::int AS severity_score
|
|
322
|
+
FROM ${schema}.feedback
|
|
323
|
+
WHERE tenant_id = $1
|
|
324
|
+
AND target_kind = 'memory_record'
|
|
325
|
+
${feedbackFilters.length > 0 ? `AND ${feedbackFilters.join('\n AND ')}` : ''}
|
|
326
|
+
GROUP BY target_id, feedback_type
|
|
327
|
+
),
|
|
328
|
+
feedback_rollup AS (
|
|
329
|
+
SELECT
|
|
330
|
+
target_id,
|
|
331
|
+
jsonb_object_agg(feedback_type, count ORDER BY feedback_type) AS feedback_counts,
|
|
332
|
+
SUM(count)::int AS feedback_count,
|
|
333
|
+
SUM(CASE WHEN feedback_type = ANY($2::text[]) THEN count ELSE 0 END)::int AS issue_count,
|
|
334
|
+
SUM(CASE WHEN ${matchTypeExpr} IS NOT NULL AND feedback_type = ${matchTypeExpr} THEN count ELSE 0 END)::int AS matching_feedback_count,
|
|
335
|
+
MIN(first_feedback_at) AS first_feedback_at,
|
|
336
|
+
MAX(latest_feedback_at) AS latest_feedback_at,
|
|
337
|
+
MAX(CASE WHEN feedback_type = ANY($2::text[]) THEN latest_feedback_id ELSE NULL END)::bigint AS latest_issue_feedback_id,
|
|
338
|
+
MAX(CASE WHEN feedback_type = ANY($2::text[]) THEN latest_feedback_at ELSE NULL END) AS latest_issue_feedback_at,
|
|
339
|
+
MAX(severity_score)::int AS severity_score
|
|
340
|
+
FROM feedback_counts
|
|
341
|
+
GROUP BY target_id
|
|
342
|
+
),
|
|
343
|
+
latest_resolution AS (
|
|
344
|
+
SELECT DISTINCT ON (memory_id)
|
|
345
|
+
id,
|
|
346
|
+
memory_id,
|
|
347
|
+
canonical_key,
|
|
348
|
+
scope_id,
|
|
349
|
+
resolution,
|
|
350
|
+
reason,
|
|
351
|
+
actor_kind,
|
|
352
|
+
actor_id,
|
|
353
|
+
issue_feedback_types,
|
|
354
|
+
resolved_through_feedback_id,
|
|
355
|
+
resolved_through_feedback_at,
|
|
356
|
+
defer_until,
|
|
357
|
+
resolved_at,
|
|
358
|
+
created_at
|
|
359
|
+
FROM ${schema}.memory_review_resolutions
|
|
360
|
+
WHERE tenant_id = $1
|
|
361
|
+
ORDER BY memory_id, resolved_at DESC, id DESC
|
|
362
|
+
)
|
|
363
|
+
SELECT
|
|
364
|
+
m.id AS memory_id,
|
|
365
|
+
m.canonical_key,
|
|
366
|
+
m.memory_type,
|
|
367
|
+
m.status,
|
|
368
|
+
m.visible_in_bootstrap,
|
|
369
|
+
m.visible_in_recall,
|
|
370
|
+
m.authority,
|
|
371
|
+
m.accepted_at,
|
|
372
|
+
m.updated_at,
|
|
373
|
+
m.scope_id,
|
|
374
|
+
s.scope_kind,
|
|
375
|
+
s.scope_key,
|
|
376
|
+
s.inheritance_mode,
|
|
377
|
+
r.feedback_counts,
|
|
378
|
+
r.feedback_count,
|
|
379
|
+
r.issue_count,
|
|
380
|
+
r.matching_feedback_count,
|
|
381
|
+
r.severity_score,
|
|
382
|
+
r.first_feedback_at,
|
|
383
|
+
r.latest_feedback_at,
|
|
384
|
+
r.latest_issue_feedback_id,
|
|
385
|
+
r.latest_issue_feedback_at,
|
|
386
|
+
lr.id AS resolution_id,
|
|
387
|
+
lr.memory_id AS resolution_memory_id,
|
|
388
|
+
lr.canonical_key AS resolution_canonical_key,
|
|
389
|
+
lr.scope_id AS resolution_scope_id,
|
|
390
|
+
lr.resolution,
|
|
391
|
+
lr.reason AS resolution_reason,
|
|
392
|
+
lr.actor_kind AS resolution_actor_kind,
|
|
393
|
+
lr.actor_id AS resolution_actor_id,
|
|
394
|
+
lr.issue_feedback_types AS resolution_issue_feedback_types,
|
|
395
|
+
lr.resolved_through_feedback_id AS resolution_resolved_through_feedback_id,
|
|
396
|
+
lr.resolved_through_feedback_at AS resolution_resolved_through_feedback_at,
|
|
397
|
+
lr.defer_until AS resolution_defer_until,
|
|
398
|
+
lr.resolved_at,
|
|
399
|
+
lr.created_at AS resolution_created_at,
|
|
400
|
+
CASE
|
|
401
|
+
WHEN lr.id IS NULL THEN 'open'
|
|
402
|
+
WHEN lr.resolved_through_feedback_id IS NULL THEN 'open'
|
|
403
|
+
WHEN lr.resolved_through_feedback_id < r.latest_issue_feedback_id THEN 'open'
|
|
404
|
+
WHEN lr.resolution = 'deferred'
|
|
405
|
+
AND lr.defer_until IS NOT NULL
|
|
406
|
+
AND lr.defer_until > COALESCE(${asOfRef}, now()) THEN 'deferred'
|
|
407
|
+
WHEN lr.resolution IN ('resolved','ignored') THEN lr.resolution
|
|
408
|
+
ELSE 'open'
|
|
409
|
+
END AS queue_state
|
|
410
|
+
FROM feedback_rollup r
|
|
411
|
+
JOIN ${schema}.memory_records m
|
|
412
|
+
ON m.tenant_id = $1
|
|
413
|
+
AND r.target_id = m.id::text
|
|
414
|
+
JOIN ${schema}.scopes s
|
|
415
|
+
ON s.id = m.scope_id
|
|
416
|
+
AND s.tenant_id = m.tenant_id
|
|
417
|
+
LEFT JOIN latest_resolution lr
|
|
418
|
+
ON lr.memory_id = m.id
|
|
419
|
+
WHERE ${filters.join('\n AND ')}
|
|
420
|
+
${includeResolved ? '' : `AND (
|
|
421
|
+
lr.id IS NULL
|
|
422
|
+
OR lr.resolved_through_feedback_id IS NULL
|
|
423
|
+
OR lr.resolved_through_feedback_id < r.latest_issue_feedback_id
|
|
424
|
+
OR (
|
|
425
|
+
lr.resolution = 'deferred'
|
|
426
|
+
AND (
|
|
427
|
+
lr.defer_until IS NULL
|
|
428
|
+
OR lr.defer_until <= COALESCE(${asOfRef}, now())
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
)`}
|
|
432
|
+
ORDER BY
|
|
433
|
+
r.severity_score DESC,
|
|
434
|
+
r.issue_count DESC,
|
|
435
|
+
r.latest_feedback_at DESC NULLS LAST,
|
|
436
|
+
m.id DESC
|
|
437
|
+
LIMIT ${limitRef}`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function queue(opts = {}) {
|
|
442
|
+
assertPool();
|
|
443
|
+
const query = buildQueueQuery(opts);
|
|
444
|
+
const result = await pool.query(query.sql, query.params);
|
|
445
|
+
const feedbackType = normalizeFeedbackType(opts.feedbackType);
|
|
446
|
+
const memoryTypes = normalizeList(opts.memoryType || opts.memoryTypes);
|
|
447
|
+
const visibility = normalizeVisibility(opts.visibility);
|
|
448
|
+
const scopeContext = scopeContextFromOpts(opts);
|
|
449
|
+
const requestedLimit = clampLimit(opts.limit);
|
|
450
|
+
const applicableRows = resolveApplicableRecords(result.rows || [], scopeContext);
|
|
451
|
+
const items = applicableRows.slice(0, requestedLimit).map(normalizeQueueRow);
|
|
452
|
+
const feedbackTypeCounts = {};
|
|
453
|
+
const severityCounts = {};
|
|
454
|
+
const queueStateCounts = {};
|
|
455
|
+
const latestResolutionCounts = {};
|
|
456
|
+
for (const item of items) {
|
|
457
|
+
severityCounts[item.severity] = (severityCounts[item.severity] || 0) + 1;
|
|
458
|
+
const queueState = item.queueState || 'open';
|
|
459
|
+
queueStateCounts[queueState] = (queueStateCounts[queueState] || 0) + 1;
|
|
460
|
+
if (item.resolution?.resolution) {
|
|
461
|
+
latestResolutionCounts[item.resolution.resolution] = (latestResolutionCounts[item.resolution.resolution] || 0) + 1;
|
|
462
|
+
}
|
|
463
|
+
for (const [type, count] of Object.entries(item.feedbackCounts)) {
|
|
464
|
+
feedbackTypeCounts[type] = (feedbackTypeCounts[type] || 0) + count;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
readOnly: true,
|
|
469
|
+
derived: true,
|
|
470
|
+
available: true,
|
|
471
|
+
items,
|
|
472
|
+
reviewQueue: items,
|
|
473
|
+
scope: {
|
|
474
|
+
activeScopeKey: scopeContext.activeScopeKey,
|
|
475
|
+
activeScopePath: scopeContext.activeScopePath,
|
|
476
|
+
},
|
|
477
|
+
filters: {
|
|
478
|
+
feedbackTypes: feedbackType ? [feedbackType] : issueTypes(opts.issueFeedbackTypes),
|
|
479
|
+
memoryTypes,
|
|
480
|
+
visibility,
|
|
481
|
+
dateFrom: opts.dateFrom || null,
|
|
482
|
+
dateTo: opts.dateTo || null,
|
|
483
|
+
asOf: opts.asOf || null,
|
|
484
|
+
scopeKey: scopeContext.activeScopeKey,
|
|
485
|
+
canonicalKey: opts.canonicalKey || null,
|
|
486
|
+
includeResolved: opts.includeResolved === true,
|
|
487
|
+
limit: requestedLimit,
|
|
488
|
+
},
|
|
489
|
+
summary: {
|
|
490
|
+
scanned: applicableRows.length,
|
|
491
|
+
queued: items.length,
|
|
492
|
+
truncated: applicableRows.length > requestedLimit,
|
|
493
|
+
severityCounts,
|
|
494
|
+
feedbackTypeCounts,
|
|
495
|
+
queueStateCounts,
|
|
496
|
+
resolutionCounts: queueStateCounts,
|
|
497
|
+
latestResolutionCounts,
|
|
498
|
+
},
|
|
499
|
+
totals: {
|
|
500
|
+
items: items.length,
|
|
501
|
+
issueFeedback: items.reduce((sum, item) => sum + item.issueCount, 0),
|
|
502
|
+
allFeedback: items.reduce((sum, item) => sum + item.feedbackCount, 0),
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function findMemory(input = {}) {
|
|
508
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
509
|
+
const visibility = normalizeVisibility(input.visibility);
|
|
510
|
+
const scopeContext = scopeContextFromOpts(input);
|
|
511
|
+
const memoryId = input.memoryId || input.id || null;
|
|
512
|
+
let canonicalKey = input.canonicalKey || null;
|
|
513
|
+
if (!memoryId && !canonicalKey) {
|
|
514
|
+
throw new Error('review.inspect requires memoryId or canonicalKey');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function addCurrentFilters(params, filters) {
|
|
518
|
+
params.push(scopeContext.activeScopePath);
|
|
519
|
+
filters.push(`s.scope_key = ANY($${params.length}::text[])`);
|
|
520
|
+
filters.push(`m.status = 'active'`);
|
|
521
|
+
params.push(normalizeIsoOrNull(input.asOf));
|
|
522
|
+
const asOfRef = `$${params.length}::timestamptz`;
|
|
523
|
+
filters.push(`(m.valid_from IS NULL OR m.valid_from <= COALESCE(${asOfRef}, now()))`);
|
|
524
|
+
filters.push(`(m.valid_to IS NULL OR m.valid_to > COALESCE(${asOfRef}, now()))`);
|
|
525
|
+
filters.push(`(m.stale_after IS NULL OR m.stale_after > COALESCE(${asOfRef}, now()))`);
|
|
526
|
+
if (visibility === 'bootstrap') {
|
|
527
|
+
filters.push(`m.visible_in_bootstrap = true`);
|
|
528
|
+
} else if (visibility === 'recall') {
|
|
529
|
+
filters.push(`m.visible_in_recall = true`);
|
|
530
|
+
} else if (visibility === 'both') {
|
|
531
|
+
filters.push(`m.visible_in_bootstrap = true AND m.visible_in_recall = true`);
|
|
532
|
+
} else {
|
|
533
|
+
filters.push(`(m.visible_in_bootstrap = true OR m.visible_in_recall = true)`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (memoryId && !canonicalKey) {
|
|
538
|
+
const targetParams = [tenantId, String(memoryId)];
|
|
539
|
+
const targetFilters = [`m.tenant_id = $1`, `m.id::text = $2`];
|
|
540
|
+
addCurrentFilters(targetParams, targetFilters);
|
|
541
|
+
const target = await pool.query(
|
|
542
|
+
`SELECT m.canonical_key
|
|
543
|
+
FROM ${schema}.memory_records m
|
|
544
|
+
JOIN ${schema}.scopes s
|
|
545
|
+
ON s.id = m.scope_id
|
|
546
|
+
AND s.tenant_id = m.tenant_id
|
|
547
|
+
WHERE ${targetFilters.join(' AND ')}
|
|
548
|
+
LIMIT 1`,
|
|
549
|
+
targetParams,
|
|
550
|
+
);
|
|
551
|
+
canonicalKey = target.rows?.[0]?.canonical_key || null;
|
|
552
|
+
if (!canonicalKey) return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const params = [tenantId, String(canonicalKey)];
|
|
556
|
+
const filters = [`m.tenant_id = $1`, `m.canonical_key = $2`];
|
|
557
|
+
addCurrentFilters(params, filters);
|
|
558
|
+
|
|
559
|
+
const result = await pool.query(
|
|
560
|
+
`SELECT
|
|
561
|
+
m.id AS memory_id,
|
|
562
|
+
m.canonical_key,
|
|
563
|
+
m.memory_type,
|
|
564
|
+
m.status,
|
|
565
|
+
m.visible_in_bootstrap,
|
|
566
|
+
m.visible_in_recall,
|
|
567
|
+
m.authority,
|
|
568
|
+
m.accepted_at,
|
|
569
|
+
m.updated_at,
|
|
570
|
+
m.scope_id,
|
|
571
|
+
s.scope_kind,
|
|
572
|
+
s.scope_key,
|
|
573
|
+
s.inheritance_mode
|
|
574
|
+
FROM ${schema}.memory_records m
|
|
575
|
+
JOIN ${schema}.scopes s
|
|
576
|
+
ON s.id = m.scope_id
|
|
577
|
+
AND s.tenant_id = m.tenant_id
|
|
578
|
+
WHERE ${filters.join(' AND ')}
|
|
579
|
+
ORDER BY CASE WHEN m.status = 'active' THEN 0 ELSE 1 END, m.updated_at DESC NULLS LAST, m.id DESC
|
|
580
|
+
LIMIT 20`,
|
|
581
|
+
params,
|
|
582
|
+
);
|
|
583
|
+
const rows = resolveApplicableRecords(result.rows || [], scopeContext);
|
|
584
|
+
if (memoryId) {
|
|
585
|
+
return rows.find(row => String(row.memory_id) === String(memoryId)) || null;
|
|
586
|
+
}
|
|
587
|
+
if (rows.length > 1) {
|
|
588
|
+
throw new Error('Multiple memory records matched. Use --memory-id to inspect one row.');
|
|
589
|
+
}
|
|
590
|
+
return rows[0] || null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function loadFeedback(targetId, opts = {}) {
|
|
594
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
595
|
+
const limit = clampLimit(opts.limit, 20);
|
|
596
|
+
const params = [tenantId, String(targetId)];
|
|
597
|
+
const filters = [];
|
|
598
|
+
const feedbackType = normalizeFeedbackType(opts.feedbackType);
|
|
599
|
+
if (feedbackType && feedbackType !== 'all') {
|
|
600
|
+
params.push(feedbackType);
|
|
601
|
+
filters.push(`feedback_type = $${params.length}`);
|
|
602
|
+
}
|
|
603
|
+
if (opts.dateFrom) {
|
|
604
|
+
params.push(String(opts.dateFrom));
|
|
605
|
+
filters.push(`created_at >= $${params.length}`);
|
|
606
|
+
}
|
|
607
|
+
if (opts.dateTo) {
|
|
608
|
+
params.push(String(opts.dateTo));
|
|
609
|
+
filters.push(`created_at < ($${params.length}::date + INTERVAL '1 day')`);
|
|
610
|
+
}
|
|
611
|
+
const where = filters.length > 0 ? `\n AND ${filters.join('\n AND ')}` : '';
|
|
612
|
+
const eventParams = params.concat([limit]);
|
|
613
|
+
const [events, counts] = await Promise.all([
|
|
614
|
+
pool.query(
|
|
615
|
+
`SELECT id, feedback_type, actor_kind, actor_id, created_at
|
|
616
|
+
FROM ${schema}.feedback
|
|
617
|
+
WHERE tenant_id = $1
|
|
618
|
+
AND target_kind = 'memory_record'
|
|
619
|
+
AND target_id = $2
|
|
620
|
+
${where}
|
|
621
|
+
ORDER BY created_at DESC, id DESC
|
|
622
|
+
LIMIT $${eventParams.length}`,
|
|
623
|
+
eventParams,
|
|
624
|
+
),
|
|
625
|
+
pool.query(
|
|
626
|
+
`SELECT feedback_type, COUNT(*)::int AS count
|
|
627
|
+
FROM ${schema}.feedback
|
|
628
|
+
WHERE tenant_id = $1
|
|
629
|
+
AND target_kind = 'memory_record'
|
|
630
|
+
AND target_id = $2
|
|
631
|
+
${where}
|
|
632
|
+
GROUP BY feedback_type
|
|
633
|
+
ORDER BY feedback_type`,
|
|
634
|
+
params,
|
|
635
|
+
),
|
|
636
|
+
]);
|
|
637
|
+
return {
|
|
638
|
+
recentFeedback: (events.rows || []).map(normalizeFeedbackEvent),
|
|
639
|
+
feedbackCounts: parseCounts(Object.fromEntries((counts.rows || []).map(row => [row.feedback_type, row.count]))),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function loadResolutions(targetId, opts = {}) {
|
|
644
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
645
|
+
const limit = clampLimit(opts.resolutionLimit || opts.limit, DEFAULT_RESOLUTION_LIMIT);
|
|
646
|
+
const result = await pool.query(
|
|
647
|
+
`SELECT
|
|
648
|
+
id,
|
|
649
|
+
memory_id,
|
|
650
|
+
canonical_key,
|
|
651
|
+
scope_id,
|
|
652
|
+
resolution,
|
|
653
|
+
reason,
|
|
654
|
+
actor_kind,
|
|
655
|
+
actor_id,
|
|
656
|
+
issue_feedback_types,
|
|
657
|
+
resolved_through_feedback_id,
|
|
658
|
+
resolved_through_feedback_at,
|
|
659
|
+
defer_until,
|
|
660
|
+
resolved_at,
|
|
661
|
+
created_at
|
|
662
|
+
FROM ${schema}.memory_review_resolutions
|
|
663
|
+
WHERE tenant_id = $1
|
|
664
|
+
AND memory_id::text = $2
|
|
665
|
+
ORDER BY resolved_at DESC, id DESC
|
|
666
|
+
LIMIT $3`,
|
|
667
|
+
[tenantId, String(targetId), limit],
|
|
668
|
+
);
|
|
669
|
+
const resolutions = (result.rows || []).map(normalizeResolutionRow).filter(Boolean);
|
|
670
|
+
return {
|
|
671
|
+
latestResolution: resolutions[0] || null,
|
|
672
|
+
recentResolutions: resolutions,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function loadIssueFeedbackWatermark(targetId, opts = {}) {
|
|
677
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
678
|
+
const types = issueTypesForResolution(opts);
|
|
679
|
+
const result = await pool.query(
|
|
680
|
+
`SELECT
|
|
681
|
+
feedback_type,
|
|
682
|
+
COUNT(*)::int AS count,
|
|
683
|
+
MAX(id)::bigint AS latest_feedback_id,
|
|
684
|
+
MAX(created_at) AS latest_feedback_at
|
|
685
|
+
FROM ${schema}.feedback
|
|
686
|
+
WHERE tenant_id = $1
|
|
687
|
+
AND target_kind = 'memory_record'
|
|
688
|
+
AND target_id = $2
|
|
689
|
+
AND feedback_type = ANY($3::text[])
|
|
690
|
+
GROUP BY feedback_type
|
|
691
|
+
ORDER BY feedback_type`,
|
|
692
|
+
[tenantId, String(targetId), types],
|
|
693
|
+
);
|
|
694
|
+
const counts = parseCounts(Object.fromEntries((result.rows || []).map(row => [row.feedback_type, row.count])));
|
|
695
|
+
const latest = (result.rows || [])
|
|
696
|
+
.map(row => row.latest_feedback_at)
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
.reduce((max, value) => {
|
|
699
|
+
const time = new Date(value).getTime();
|
|
700
|
+
if (!Number.isFinite(time)) return max;
|
|
701
|
+
if (!max || time > max.time) return { time, value };
|
|
702
|
+
return max;
|
|
703
|
+
}, null)?.value || null;
|
|
704
|
+
const latestId = (result.rows || [])
|
|
705
|
+
.map(row => Number.parseInt(row.latest_feedback_id, 10))
|
|
706
|
+
.filter(Number.isFinite)
|
|
707
|
+
.reduce((max, value) => Math.max(max, value), 0);
|
|
708
|
+
return {
|
|
709
|
+
issueFeedbackTypes: types,
|
|
710
|
+
issueFeedbackCounts: counts,
|
|
711
|
+
latestIssueFeedbackId: latestId > 0 ? String(latestId) : null,
|
|
712
|
+
issueFeedbackLatestAt: latest,
|
|
713
|
+
issueFeedbackCount: Object.values(counts).reduce((sum, count) => sum + count, 0),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function inspect(input = {}) {
|
|
718
|
+
assertPool();
|
|
719
|
+
const memory = await findMemory(input);
|
|
720
|
+
if (!memory) throw new Error('Memory record not found');
|
|
721
|
+
const normalized = normalizeQueueRow({
|
|
722
|
+
...memory,
|
|
723
|
+
feedback_counts: {},
|
|
724
|
+
feedback_count: 0,
|
|
725
|
+
issue_count: 0,
|
|
726
|
+
matching_feedback_count: 0,
|
|
727
|
+
severity_score: 0,
|
|
728
|
+
});
|
|
729
|
+
const feedback = await loadFeedback(normalized.memoryId, input);
|
|
730
|
+
const resolutions = await loadResolutions(normalized.memoryId, input);
|
|
731
|
+
const issueSet = new Set(issueTypes(input.issueFeedbackTypes));
|
|
732
|
+
const issueCount = Object.entries(feedback.feedbackCounts)
|
|
733
|
+
.filter(([type]) => issueSet.has(type))
|
|
734
|
+
.reduce((sum, [, count]) => sum + count, 0);
|
|
735
|
+
const issueWatermark = await loadIssueFeedbackWatermark(normalized.memoryId, input);
|
|
736
|
+
return {
|
|
737
|
+
readOnly: true,
|
|
738
|
+
derived: true,
|
|
739
|
+
available: true,
|
|
740
|
+
memory: normalized,
|
|
741
|
+
review: {
|
|
742
|
+
severity: severityLabel(Math.max(...Object.keys(feedback.feedbackCounts).map(type => SEVERITY_SCORE[type] || 0), 0)),
|
|
743
|
+
feedbackCounts: feedback.feedbackCounts,
|
|
744
|
+
issueCount,
|
|
745
|
+
feedbackCount: Object.values(feedback.feedbackCounts).reduce((sum, count) => sum + count, 0),
|
|
746
|
+
latestIssueFeedbackId: issueWatermark.latestIssueFeedbackId,
|
|
747
|
+
latestIssueFeedbackAt: issueWatermark.issueFeedbackLatestAt,
|
|
748
|
+
recentFeedback: feedback.recentFeedback,
|
|
749
|
+
latestResolution: resolutions.latestResolution,
|
|
750
|
+
recentResolutions: resolutions.recentResolutions,
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function resolve(input = {}) {
|
|
756
|
+
assertPool();
|
|
757
|
+
const memory = await findMemory(input);
|
|
758
|
+
if (!memory) throw new Error('Memory record not found');
|
|
759
|
+
const resolution = normalizeResolution(input.resolution || input.action);
|
|
760
|
+
const deferUntil = input.deferUntil || input.defer_until || null;
|
|
761
|
+
if (resolution === 'deferred') {
|
|
762
|
+
requireIso(deferUntil, 'review.resolve deferUntil');
|
|
763
|
+
} else if (deferUntil) {
|
|
764
|
+
throw new Error('review.resolve deferUntil is only valid for deferred resolution');
|
|
765
|
+
}
|
|
766
|
+
const reason = normalizeReason(input.reason || input.note);
|
|
767
|
+
if ((resolution === 'ignored' || resolution === 'deferred') && !reason) {
|
|
768
|
+
throw new Error(`review.resolve reason is required for ${resolution}`);
|
|
769
|
+
}
|
|
770
|
+
const expectedIssueFeedbackId = input.expectedLatestIssueFeedbackId || input.expected_latest_issue_feedback_id || null;
|
|
771
|
+
const expectedIssueFeedbackAt = input.expectedLatestIssueFeedbackAt || input.expected_latest_issue_feedback_at || null;
|
|
772
|
+
if (!expectedIssueFeedbackId && !expectedIssueFeedbackAt) {
|
|
773
|
+
throw new Error('review.resolve requires expectedLatestIssueFeedbackId or expectedLatestIssueFeedbackAt');
|
|
774
|
+
}
|
|
775
|
+
const issueFeedback = await loadIssueFeedbackWatermark(memory.memory_id, input);
|
|
776
|
+
if (issueFeedback.issueFeedbackCount <= 0 || !issueFeedback.latestIssueFeedbackId) {
|
|
777
|
+
throw new Error('review.resolve requires current issue feedback on the memory record');
|
|
778
|
+
}
|
|
779
|
+
if (expectedIssueFeedbackId && String(expectedIssueFeedbackId) !== String(issueFeedback.latestIssueFeedbackId)) {
|
|
780
|
+
throw new Error(`review.resolve stale issue feedback snapshot: expected latest issue feedback id ${expectedIssueFeedbackId}, current ${issueFeedback.latestIssueFeedbackId}`);
|
|
781
|
+
}
|
|
782
|
+
if (expectedIssueFeedbackAt) {
|
|
783
|
+
const expectedIso = requireIso(expectedIssueFeedbackAt, 'review.resolve expectedLatestIssueFeedbackAt');
|
|
784
|
+
const currentIso = requireIso(issueFeedback.issueFeedbackLatestAt, 'review.resolve current issueFeedbackLatestAt');
|
|
785
|
+
if (compareIso(expectedIso, currentIso) !== 0) {
|
|
786
|
+
throw new Error(`review.resolve stale issue feedback snapshot: expected latest issue feedback at ${expectedIso}, current ${currentIso}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
790
|
+
const params = [
|
|
791
|
+
tenantId,
|
|
792
|
+
memory.memory_id,
|
|
793
|
+
memory.canonical_key,
|
|
794
|
+
memory.scope_id,
|
|
795
|
+
resolution,
|
|
796
|
+
reason,
|
|
797
|
+
normalizeActorKind(input.actorKind || input.actor_kind),
|
|
798
|
+
input.actorId || input.actor_id || input.agentId || null,
|
|
799
|
+
issueFeedback.issueFeedbackTypes,
|
|
800
|
+
issueFeedback.latestIssueFeedbackId,
|
|
801
|
+
issueFeedback.issueFeedbackLatestAt,
|
|
802
|
+
resolution === 'deferred' ? requireIso(deferUntil, 'review.resolve deferUntil') : null,
|
|
803
|
+
{
|
|
804
|
+
publicSurface: 'memoryReviewResolve',
|
|
805
|
+
issueFeedbackCounts: issueFeedback.issueFeedbackCounts,
|
|
806
|
+
issueFeedbackCount: issueFeedback.issueFeedbackCount,
|
|
807
|
+
memoryStatus: memory.status || null,
|
|
808
|
+
},
|
|
809
|
+
];
|
|
810
|
+
const inserted = await pool.query(
|
|
811
|
+
`INSERT INTO ${schema}.memory_review_resolutions (
|
|
812
|
+
tenant_id,
|
|
813
|
+
memory_id,
|
|
814
|
+
canonical_key,
|
|
815
|
+
scope_id,
|
|
816
|
+
resolution,
|
|
817
|
+
reason,
|
|
818
|
+
actor_kind,
|
|
819
|
+
actor_id,
|
|
820
|
+
issue_feedback_types,
|
|
821
|
+
resolved_through_feedback_id,
|
|
822
|
+
resolved_through_feedback_at,
|
|
823
|
+
defer_until,
|
|
824
|
+
metadata
|
|
825
|
+
)
|
|
826
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::text[],$10,$11,$12,$13::jsonb)
|
|
827
|
+
RETURNING
|
|
828
|
+
id,
|
|
829
|
+
memory_id,
|
|
830
|
+
canonical_key,
|
|
831
|
+
scope_id,
|
|
832
|
+
resolution,
|
|
833
|
+
reason,
|
|
834
|
+
actor_kind,
|
|
835
|
+
actor_id,
|
|
836
|
+
issue_feedback_types,
|
|
837
|
+
resolved_through_feedback_id,
|
|
838
|
+
resolved_through_feedback_at,
|
|
839
|
+
defer_until,
|
|
840
|
+
resolved_at,
|
|
841
|
+
created_at`,
|
|
842
|
+
params,
|
|
843
|
+
);
|
|
844
|
+
const normalizedMemory = normalizeQueueRow({
|
|
845
|
+
...memory,
|
|
846
|
+
feedback_counts: issueFeedback.issueFeedbackCounts,
|
|
847
|
+
feedback_count: issueFeedback.issueFeedbackCount,
|
|
848
|
+
issue_count: issueFeedback.issueFeedbackCount,
|
|
849
|
+
matching_feedback_count: issueFeedback.issueFeedbackCount,
|
|
850
|
+
severity_score: Math.max(...issueFeedback.issueFeedbackTypes.map(type => SEVERITY_SCORE[type] || 0), 0),
|
|
851
|
+
latest_issue_feedback_id: issueFeedback.latestIssueFeedbackId,
|
|
852
|
+
latest_issue_feedback_at: issueFeedback.issueFeedbackLatestAt,
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
appendedOnly: true,
|
|
856
|
+
appendOnly: true,
|
|
857
|
+
memoryMutated: false,
|
|
858
|
+
feedbackMutated: false,
|
|
859
|
+
available: true,
|
|
860
|
+
memory: normalizedMemory,
|
|
861
|
+
resolution: normalizeResolutionRow(inserted.rows?.[0]),
|
|
862
|
+
review: {
|
|
863
|
+
issueFeedbackTypes: issueFeedback.issueFeedbackTypes,
|
|
864
|
+
issueFeedbackCounts: issueFeedback.issueFeedbackCounts,
|
|
865
|
+
issueFeedbackCount: issueFeedback.issueFeedbackCount,
|
|
866
|
+
latestIssueFeedbackId: issueFeedback.latestIssueFeedbackId,
|
|
867
|
+
issueFeedbackLatestAt: issueFeedback.issueFeedbackLatestAt,
|
|
868
|
+
},
|
|
869
|
+
queue: {
|
|
870
|
+
wasQueued: true,
|
|
871
|
+
nowQueued: false,
|
|
872
|
+
suppressedBy: resolution,
|
|
873
|
+
reopensAt: resolution === 'deferred' ? requireIso(deferUntil, 'review.resolve deferUntil') : null,
|
|
874
|
+
reopensOnNewIssueFeedback: true,
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
queue,
|
|
881
|
+
inspect,
|
|
882
|
+
resolve,
|
|
883
|
+
ISSUE_FEEDBACK_TYPES: ISSUE_FEEDBACK_TYPES.slice(),
|
|
884
|
+
RESOLUTION_ACTIONS: RESOLUTION_ACTIONS.slice(),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
module.exports = {
|
|
889
|
+
ISSUE_FEEDBACK_TYPES,
|
|
890
|
+
createMemoryReview,
|
|
891
|
+
};
|