@shadowforge0/aquifer-memory 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +66 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +192 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +442 -3
- package/consumers/codex.js +164 -107
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +351 -840
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +128 -8
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +82 -1
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/setup.md +22 -0
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- package/scripts/codex-recovery.js +105 -0
package/core/memory-recall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { resolveApplicableRecords } = require('./memory-bootstrap');
|
|
4
|
+
const { hybridRank } = require('./hybrid-rank');
|
|
4
5
|
|
|
5
6
|
const TYPE_RANK = {
|
|
6
7
|
constraint: 80,
|
|
@@ -22,6 +23,13 @@ const FEEDBACK_WEIGHT = {
|
|
|
22
23
|
incorrect: -0.50,
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
const RETRIEVAL_TYPE_BOOST = 0.05;
|
|
27
|
+
const SIGNAL_PRIORITY = {
|
|
28
|
+
linked_summary: 1,
|
|
29
|
+
evidence_item: 2,
|
|
30
|
+
memory_row: 3,
|
|
31
|
+
};
|
|
32
|
+
|
|
25
33
|
const TYPE_RANK_SQL = `
|
|
26
34
|
CASE m.memory_type
|
|
27
35
|
WHEN 'constraint' THEN 0.80
|
|
@@ -35,6 +43,8 @@ const TYPE_RANK_SQL = `
|
|
|
35
43
|
ELSE 0
|
|
36
44
|
END`;
|
|
37
45
|
|
|
46
|
+
const TYPE_BOOST_SQL = `(${TYPE_RANK_SQL}) * ${RETRIEVAL_TYPE_BOOST}`;
|
|
47
|
+
|
|
38
48
|
function feedbackScoreSql(schema) {
|
|
39
49
|
return `
|
|
40
50
|
COALESCE((
|
|
@@ -109,6 +119,10 @@ function rankValue(record, key) {
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
function sortRecallRows(a, b) {
|
|
122
|
+
const aSignalPriority = rankValue(a, 'signal_priority');
|
|
123
|
+
const bSignalPriority = rankValue(b, 'signal_priority');
|
|
124
|
+
if (bSignalPriority !== aSignalPriority) return bSignalPriority - aSignalPriority;
|
|
125
|
+
|
|
112
126
|
const aTitleMatch = a.title_match === true ? 1 : 0;
|
|
113
127
|
const bTitleMatch = b.title_match === true ? 1 : 0;
|
|
114
128
|
if (bTitleMatch !== aTitleMatch) return bTitleMatch - aTitleMatch;
|
|
@@ -124,6 +138,82 @@ function sortRecallRows(a, b) {
|
|
|
124
138
|
return getId(a).localeCompare(getId(b));
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
function memoryRecallKey(row) {
|
|
142
|
+
return String(row && (row.id || row.memory_id || row.memoryId || row.canonical_key || row.canonicalKey || ''));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rankHybridMemoryRows(lexicalRows = [], embeddingRows = [], opts = {}) {
|
|
146
|
+
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
147
|
+
const rowsById = new Map();
|
|
148
|
+
function remember(row, signal) {
|
|
149
|
+
const id = memoryRecallKey(row);
|
|
150
|
+
if (!id) return;
|
|
151
|
+
const existing = rowsById.get(id);
|
|
152
|
+
const next = existing ? { ...existing, ...row } : { ...row };
|
|
153
|
+
const signals = new Set(existing && Array.isArray(existing._matchSignals) ? existing._matchSignals : []);
|
|
154
|
+
signals.add(signal);
|
|
155
|
+
next._matchSignals = [...signals];
|
|
156
|
+
next.match_signal = signals.size > 1 ? 'memory_row_hybrid' : 'memory_row';
|
|
157
|
+
delete next.signal_priority;
|
|
158
|
+
rowsById.set(id, next);
|
|
159
|
+
}
|
|
160
|
+
for (const row of lexicalRows || []) remember(row, 'lexical');
|
|
161
|
+
for (const row of embeddingRows || []) remember(row, 'semantic');
|
|
162
|
+
|
|
163
|
+
function adapt(row) {
|
|
164
|
+
const id = memoryRecallKey(row);
|
|
165
|
+
return {
|
|
166
|
+
...row,
|
|
167
|
+
session_id: id,
|
|
168
|
+
started_at: row.accepted_at || row.observed_at || row.updated_at || row.created_at || row.started_at,
|
|
169
|
+
trust_score: row.trust_score ?? 0.5,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fused = hybridRank(
|
|
174
|
+
(lexicalRows || []).map(adapt),
|
|
175
|
+
(embeddingRows || []).map(adapt),
|
|
176
|
+
[],
|
|
177
|
+
{
|
|
178
|
+
limit: Math.max(limit, rowsById.size || limit),
|
|
179
|
+
weights: { rrf: 0.82, timeDecay: 0.12, access: 0.06, entityBoost: 0, openLoop: 0 },
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const scored = fused.map(fusedRow => {
|
|
184
|
+
const id = memoryRecallKey(fusedRow);
|
|
185
|
+
const row = rowsById.get(id) || fusedRow;
|
|
186
|
+
const rowScore = rankValue(row, 'recall_score') || rankValue(row, 'score') || rankValue(row, 'semantic_score') || rankValue(row, 'lexical_rank');
|
|
187
|
+
const typeScore = rankValue(row, 'type_rank');
|
|
188
|
+
const feedback = rankValue(row, 'feedback_score');
|
|
189
|
+
const score = (0.82 * rankValue(fusedRow, '_score')) + (0.14 * Math.min(1, Math.max(0, rowScore))) + (0.02 * typeScore) + (0.02 * feedback);
|
|
190
|
+
const ranked = {
|
|
191
|
+
...row,
|
|
192
|
+
recall_score: score,
|
|
193
|
+
score,
|
|
194
|
+
_score: score,
|
|
195
|
+
_rrf: fusedRow._rrf,
|
|
196
|
+
_timeDecay: fusedRow._timeDecay,
|
|
197
|
+
_access: fusedRow._access,
|
|
198
|
+
};
|
|
199
|
+
delete ranked.session_id;
|
|
200
|
+
delete ranked.signal_priority;
|
|
201
|
+
return ranked;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
scored.sort((a, b) => {
|
|
205
|
+
const aScore = rankValue(a, '_score');
|
|
206
|
+
const bScore = rankValue(b, '_score');
|
|
207
|
+
if (bScore !== aScore) return bScore - aScore;
|
|
208
|
+
const aAccepted = Date.parse(a.accepted_at || a.acceptedAt || '') || 0;
|
|
209
|
+
const bAccepted = Date.parse(b.accepted_at || b.acceptedAt || '') || 0;
|
|
210
|
+
if (bAccepted !== aAccepted) return bAccepted - aAccepted;
|
|
211
|
+
return memoryRecallKey(a).localeCompare(memoryRecallKey(b));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return scored.slice(0, limit);
|
|
215
|
+
}
|
|
216
|
+
|
|
127
217
|
function feedbackScore(record, feedbackEvents = []) {
|
|
128
218
|
const id = getId(record);
|
|
129
219
|
let score = 0;
|
|
@@ -144,6 +234,14 @@ function lexicalScore(haystack, query) {
|
|
|
144
234
|
return hits / tokens.length;
|
|
145
235
|
}
|
|
146
236
|
|
|
237
|
+
function vecToStr(vec) {
|
|
238
|
+
if (!vec || !Array.isArray(vec) || vec.length === 0) return null;
|
|
239
|
+
for (let i = 0; i < vec.length; i++) {
|
|
240
|
+
if (!Number.isFinite(vec[i])) throw new Error(`Vector contains non-finite value at index ${i}`);
|
|
241
|
+
}
|
|
242
|
+
return `[${vec.join(',')}]`;
|
|
243
|
+
}
|
|
244
|
+
|
|
147
245
|
function recallMemoryRecords(records = [], query, opts = {}) {
|
|
148
246
|
const q = String(query || '').trim().toLowerCase();
|
|
149
247
|
if (!q) throw new Error('memory.recall(query): query must be a non-empty string');
|
|
@@ -160,11 +258,13 @@ function recallMemoryRecords(records = [], query, opts = {}) {
|
|
|
160
258
|
.map(record => {
|
|
161
259
|
const haystack = textOf(record).toLowerCase();
|
|
162
260
|
const lexical = lexicalScore(haystack, q);
|
|
163
|
-
const typeRank = (TYPE_RANK[record.memoryType || record.memory_type] || 0) / 100;
|
|
261
|
+
const typeRank = ((TYPE_RANK[record.memoryType || record.memory_type] || 0) / 100) * RETRIEVAL_TYPE_BOOST;
|
|
164
262
|
const feedback = feedbackScore(record, feedbackEvents);
|
|
165
263
|
return {
|
|
166
264
|
...record,
|
|
167
265
|
score: lexical + typeRank + feedback,
|
|
266
|
+
signal_priority: SIGNAL_PRIORITY.memory_row,
|
|
267
|
+
match_signal: 'memory_row',
|
|
168
268
|
_debug: { lexical, typeRank, feedback },
|
|
169
269
|
};
|
|
170
270
|
})
|
|
@@ -174,11 +274,32 @@ function recallMemoryRecords(records = [], query, opts = {}) {
|
|
|
174
274
|
}
|
|
175
275
|
|
|
176
276
|
function createMemoryRecall({ pool, schema, defaultTenantId }) {
|
|
277
|
+
function applyCurrentMemoryFilters(where, params, opts = {}) {
|
|
278
|
+
const scopeKeys = activeScopeKeys(opts);
|
|
279
|
+
if (opts.scopeId) {
|
|
280
|
+
params.push(opts.scopeId);
|
|
281
|
+
where.push(`m.scope_id = $${params.length}`);
|
|
282
|
+
}
|
|
283
|
+
if (scopeKeys) {
|
|
284
|
+
params.push(scopeKeys);
|
|
285
|
+
where.push(`s.scope_key = ANY($${params.length}::text[])`);
|
|
286
|
+
}
|
|
287
|
+
if (opts.asOf) {
|
|
288
|
+
params.push(opts.asOf);
|
|
289
|
+
const at = `$${params.length}::timestamptz`;
|
|
290
|
+
where.push(`(m.valid_from IS NULL OR m.valid_from <= ${at})`);
|
|
291
|
+
where.push(`(m.valid_to IS NULL OR m.valid_to > ${at})`);
|
|
292
|
+
where.push(`(m.stale_after IS NULL OR m.stale_after > ${at})`);
|
|
293
|
+
}
|
|
294
|
+
return scopeKeys;
|
|
295
|
+
}
|
|
296
|
+
|
|
177
297
|
async function recall(query, opts = {}) {
|
|
178
298
|
const q = String(query || '').trim();
|
|
179
299
|
if (!q) throw new Error('memory.recall(query): query must be a non-empty string');
|
|
180
300
|
const tenantId = opts.tenantId || defaultTenantId;
|
|
181
301
|
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
302
|
+
const cfg = (opts.ftsConfig === 'zhcfg' || opts.ftsConfig === 'simple') ? opts.ftsConfig : 'simple';
|
|
182
303
|
const scopeKeys = activeScopeKeys(opts);
|
|
183
304
|
const fetchLimit = Math.max(limit, Math.min(200, scopeKeys ? limit * 4 : limit));
|
|
184
305
|
const feedbackScoreExpr = feedbackScoreSql(schema);
|
|
@@ -187,37 +308,25 @@ function createMemoryRecall({ pool, schema, defaultTenantId }) {
|
|
|
187
308
|
`m.tenant_id = $1`,
|
|
188
309
|
`m.status = 'active'`,
|
|
189
310
|
`m.visible_in_recall = true`,
|
|
190
|
-
`(m.search_tsv @@ plainto_tsquery('
|
|
311
|
+
`(m.search_tsv @@ plainto_tsquery('${cfg}', $2)
|
|
191
312
|
OR m.title ILIKE '%' || $2 || '%'
|
|
192
313
|
OR m.summary ILIKE '%' || $2 || '%'
|
|
193
314
|
OR m.context_key ILIKE '%' || $2 || '%'
|
|
194
315
|
OR m.topic_key ILIKE '%' || $2 || '%')`,
|
|
195
316
|
];
|
|
196
|
-
|
|
197
|
-
params.push(opts.scopeId);
|
|
198
|
-
where.push(`m.scope_id = $${params.length}`);
|
|
199
|
-
}
|
|
200
|
-
if (scopeKeys) {
|
|
201
|
-
params.push(scopeKeys);
|
|
202
|
-
where.push(`s.scope_key = ANY($${params.length}::text[])`);
|
|
203
|
-
}
|
|
204
|
-
if (opts.asOf) {
|
|
205
|
-
params.push(opts.asOf);
|
|
206
|
-
const at = `$${params.length}::timestamptz`;
|
|
207
|
-
where.push(`(m.valid_from IS NULL OR m.valid_from <= ${at})`);
|
|
208
|
-
where.push(`(m.valid_to IS NULL OR m.valid_to > ${at})`);
|
|
209
|
-
where.push(`(m.stale_after IS NULL OR m.stale_after > ${at})`);
|
|
210
|
-
}
|
|
317
|
+
applyCurrentMemoryFilters(where, params, opts);
|
|
211
318
|
params.push(fetchLimit);
|
|
212
319
|
const result = await pool.query(
|
|
213
320
|
`SELECT
|
|
214
321
|
m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode,
|
|
322
|
+
'memory_row'::text AS match_signal,
|
|
323
|
+
${SIGNAL_PRIORITY.memory_row}::int AS signal_priority,
|
|
215
324
|
(m.title ILIKE '%' || $2 || '%') AS title_match,
|
|
216
|
-
ts_rank(m.search_tsv, plainto_tsquery('
|
|
217
|
-
${
|
|
325
|
+
ts_rank(m.search_tsv, plainto_tsquery('${cfg}', $2)) AS lexical_rank,
|
|
326
|
+
${TYPE_BOOST_SQL} AS type_rank,
|
|
218
327
|
${feedbackScoreExpr} AS feedback_score,
|
|
219
|
-
ts_rank(m.search_tsv, plainto_tsquery('
|
|
220
|
-
+ ${
|
|
328
|
+
ts_rank(m.search_tsv, plainto_tsquery('${cfg}', $2))
|
|
329
|
+
+ ${TYPE_BOOST_SQL}
|
|
221
330
|
+ ${feedbackScoreExpr} AS recall_score
|
|
222
331
|
FROM ${schema}.memory_records m
|
|
223
332
|
JOIN ${schema}.scopes s ON s.id = m.scope_id
|
|
@@ -238,10 +347,220 @@ function createMemoryRecall({ pool, schema, defaultTenantId }) {
|
|
|
238
347
|
.slice(0, limit);
|
|
239
348
|
}
|
|
240
349
|
|
|
241
|
-
|
|
350
|
+
async function recallViaEvidenceItems(query, opts = {}) {
|
|
351
|
+
const q = String(query || '').trim();
|
|
352
|
+
if (!q) throw new Error('memory.recall(query): query must be a non-empty string');
|
|
353
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
354
|
+
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
355
|
+
const scopeKeys = activeScopeKeys(opts);
|
|
356
|
+
const fetchLimit = Math.max(limit, Math.min(200, scopeKeys ? limit * 4 : limit));
|
|
357
|
+
const feedbackScoreExpr = feedbackScoreSql(schema);
|
|
358
|
+
const params = [tenantId, q];
|
|
359
|
+
const where = [
|
|
360
|
+
`m.tenant_id = $1`,
|
|
361
|
+
`m.status = 'active'`,
|
|
362
|
+
`m.visible_in_recall = true`,
|
|
363
|
+
];
|
|
364
|
+
applyCurrentMemoryFilters(where, params, opts);
|
|
365
|
+
const queryVec = vecToStr(opts.queryVec);
|
|
366
|
+
let vectorScoreExpr = '0';
|
|
367
|
+
let evidencePredicate = `(ei.excerpt_text ILIKE '%' || $2 || '%'
|
|
368
|
+
OR ei.search_tsv @@ plainto_tsquery('simple', $2))`;
|
|
369
|
+
if (queryVec) {
|
|
370
|
+
params.push(queryVec);
|
|
371
|
+
const vecPos = params.length;
|
|
372
|
+
vectorScoreExpr = `COALESCE(1.0 - (ei.embedding <=> $${vecPos}::vector), 0)`;
|
|
373
|
+
evidencePredicate = opts.vectorOnly === true
|
|
374
|
+
? `ei.embedding IS NOT NULL`
|
|
375
|
+
: `(${evidencePredicate} OR ei.embedding IS NOT NULL)`;
|
|
376
|
+
}
|
|
377
|
+
params.push(fetchLimit);
|
|
378
|
+
const result = await pool.query(
|
|
379
|
+
`WITH eligible_memories AS (
|
|
380
|
+
SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
|
|
381
|
+
FROM ${schema}.memory_records m
|
|
382
|
+
JOIN ${schema}.scopes s ON s.id = m.scope_id
|
|
383
|
+
WHERE ${where.join(' AND ')}
|
|
384
|
+
),
|
|
385
|
+
evidence_hits AS (
|
|
386
|
+
SELECT
|
|
387
|
+
e.owner_id AS memory_id,
|
|
388
|
+
MAX(
|
|
389
|
+
CASE WHEN ei.excerpt_text ILIKE '%' || $2 || '%' THEN 1 ELSE 0 END
|
|
390
|
+
+ ts_rank(ei.search_tsv, plainto_tsquery('simple', $2))
|
|
391
|
+
+ similarity(ei.excerpt_text, $2)
|
|
392
|
+
+ ${vectorScoreExpr}
|
|
393
|
+
) AS evidence_score,
|
|
394
|
+
MAX(ei.created_at) AS latest_evidence_at
|
|
395
|
+
FROM ${schema}.evidence_items ei
|
|
396
|
+
JOIN ${schema}.evidence_refs e
|
|
397
|
+
ON e.tenant_id = ei.tenant_id
|
|
398
|
+
AND e.evidence_item_id = ei.id
|
|
399
|
+
AND e.owner_kind = 'memory_record'
|
|
400
|
+
JOIN eligible_memories em ON em.id = e.owner_id
|
|
401
|
+
WHERE ei.tenant_id = $1
|
|
402
|
+
AND ${evidencePredicate}
|
|
403
|
+
GROUP BY e.owner_id
|
|
404
|
+
)
|
|
405
|
+
SELECT
|
|
406
|
+
m.*,
|
|
407
|
+
'evidence_item'::text AS match_signal,
|
|
408
|
+
${SIGNAL_PRIORITY.evidence_item}::int AS signal_priority,
|
|
409
|
+
FALSE AS title_match,
|
|
410
|
+
0::real AS lexical_rank,
|
|
411
|
+
eh.evidence_score,
|
|
412
|
+
${TYPE_BOOST_SQL} AS type_rank,
|
|
413
|
+
${feedbackScoreExpr} AS feedback_score,
|
|
414
|
+
eh.evidence_score
|
|
415
|
+
+ ${TYPE_BOOST_SQL}
|
|
416
|
+
+ ${feedbackScoreExpr} AS recall_score
|
|
417
|
+
FROM evidence_hits eh
|
|
418
|
+
JOIN eligible_memories m ON m.id = eh.memory_id
|
|
419
|
+
ORDER BY
|
|
420
|
+
recall_score DESC,
|
|
421
|
+
eh.latest_evidence_at DESC NULLS LAST,
|
|
422
|
+
m.accepted_at DESC NULLS LAST,
|
|
423
|
+
m.id ASC
|
|
424
|
+
LIMIT $${params.length}`,
|
|
425
|
+
params,
|
|
426
|
+
);
|
|
427
|
+
const applicableRows = scopeKeys
|
|
428
|
+
? resolveApplicableRecords(result.rows, opts)
|
|
429
|
+
: result.rows;
|
|
430
|
+
return applicableRows
|
|
431
|
+
.sort(sortRecallRows)
|
|
432
|
+
.slice(0, limit);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function recallViaMemoryEmbeddings(queryVec, opts = {}) {
|
|
436
|
+
const vector = vecToStr(queryVec);
|
|
437
|
+
if (!vector) return [];
|
|
438
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
439
|
+
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
440
|
+
const scopeKeys = activeScopeKeys(opts);
|
|
441
|
+
const fetchLimit = Math.max(limit, Math.min(200, scopeKeys ? limit * 4 : limit));
|
|
442
|
+
const feedbackScoreExpr = feedbackScoreSql(schema);
|
|
443
|
+
const params = [tenantId, vector];
|
|
444
|
+
const where = [
|
|
445
|
+
`m.tenant_id = $1`,
|
|
446
|
+
`m.status = 'active'`,
|
|
447
|
+
`m.visible_in_recall = true`,
|
|
448
|
+
`m.embedding IS NOT NULL`,
|
|
449
|
+
];
|
|
450
|
+
applyCurrentMemoryFilters(where, params, opts);
|
|
451
|
+
params.push(fetchLimit);
|
|
452
|
+
const result = await pool.query(
|
|
453
|
+
`SELECT
|
|
454
|
+
m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode,
|
|
455
|
+
'memory_row'::text AS match_signal,
|
|
456
|
+
${SIGNAL_PRIORITY.memory_row}::int AS signal_priority,
|
|
457
|
+
FALSE AS title_match,
|
|
458
|
+
0::real AS lexical_rank,
|
|
459
|
+
1.0 - (m.embedding <=> $2::vector) AS semantic_score,
|
|
460
|
+
${TYPE_BOOST_SQL} AS type_rank,
|
|
461
|
+
${feedbackScoreExpr} AS feedback_score,
|
|
462
|
+
1.0 - (m.embedding <=> $2::vector)
|
|
463
|
+
+ ${TYPE_BOOST_SQL}
|
|
464
|
+
+ ${feedbackScoreExpr} AS recall_score
|
|
465
|
+
FROM ${schema}.memory_records m
|
|
466
|
+
JOIN ${schema}.scopes s ON s.id = m.scope_id
|
|
467
|
+
WHERE ${where.join(' AND ')}
|
|
468
|
+
ORDER BY
|
|
469
|
+
m.embedding <=> $2::vector ASC,
|
|
470
|
+
m.accepted_at DESC NULLS LAST,
|
|
471
|
+
m.id ASC
|
|
472
|
+
LIMIT $${params.length}`,
|
|
473
|
+
params,
|
|
474
|
+
);
|
|
475
|
+
const applicableRows = scopeKeys
|
|
476
|
+
? resolveApplicableRecords(result.rows, opts)
|
|
477
|
+
: result.rows;
|
|
478
|
+
return applicableRows
|
|
479
|
+
.sort(sortRecallRows)
|
|
480
|
+
.slice(0, limit);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function recallViaLinkedSummaryEmbeddings(queryVec, opts = {}) {
|
|
484
|
+
const vector = vecToStr(queryVec);
|
|
485
|
+
if (!vector) return [];
|
|
486
|
+
const tenantId = opts.tenantId || defaultTenantId;
|
|
487
|
+
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
488
|
+
const scopeKeys = activeScopeKeys(opts);
|
|
489
|
+
const fetchLimit = Math.max(limit, Math.min(200, scopeKeys ? limit * 4 : limit));
|
|
490
|
+
const feedbackScoreExpr = feedbackScoreSql(schema);
|
|
491
|
+
const params = [tenantId, vector];
|
|
492
|
+
const where = [
|
|
493
|
+
`m.tenant_id = $1`,
|
|
494
|
+
`m.status = 'active'`,
|
|
495
|
+
`m.visible_in_recall = true`,
|
|
496
|
+
];
|
|
497
|
+
applyCurrentMemoryFilters(where, params, opts);
|
|
498
|
+
params.push(fetchLimit);
|
|
499
|
+
const result = await pool.query(
|
|
500
|
+
`WITH eligible_memories AS (
|
|
501
|
+
SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
|
|
502
|
+
FROM ${schema}.memory_records m
|
|
503
|
+
JOIN ${schema}.scopes s ON s.id = m.scope_id
|
|
504
|
+
WHERE ${where.join(' AND ')}
|
|
505
|
+
),
|
|
506
|
+
linked_summary_hits AS (
|
|
507
|
+
SELECT
|
|
508
|
+
e.owner_id AS memory_id,
|
|
509
|
+
MAX(1.0 - (ss.embedding <=> $2::vector)) AS linked_summary_score,
|
|
510
|
+
MAX(ss.updated_at) AS latest_summary_at
|
|
511
|
+
FROM ${schema}.evidence_refs e
|
|
512
|
+
JOIN ${schema}.sessions src
|
|
513
|
+
ON src.tenant_id = e.tenant_id
|
|
514
|
+
AND src.session_id = e.source_ref
|
|
515
|
+
JOIN ${schema}.session_summaries ss
|
|
516
|
+
ON ss.session_row_id = src.id
|
|
517
|
+
WHERE e.tenant_id = $1
|
|
518
|
+
AND e.owner_kind = 'memory_record'
|
|
519
|
+
AND e.source_kind = 'session_summary'
|
|
520
|
+
AND ss.embedding IS NOT NULL
|
|
521
|
+
AND EXISTS (SELECT 1 FROM eligible_memories em WHERE em.id = e.owner_id)
|
|
522
|
+
GROUP BY e.owner_id
|
|
523
|
+
)
|
|
524
|
+
SELECT
|
|
525
|
+
m.*,
|
|
526
|
+
'linked_summary'::text AS match_signal,
|
|
527
|
+
${SIGNAL_PRIORITY.linked_summary}::int AS signal_priority,
|
|
528
|
+
FALSE AS title_match,
|
|
529
|
+
0::real AS lexical_rank,
|
|
530
|
+
lsh.linked_summary_score,
|
|
531
|
+
0::real AS type_rank,
|
|
532
|
+
${feedbackScoreExpr} AS feedback_score,
|
|
533
|
+
(lsh.linked_summary_score * 0.35)
|
|
534
|
+
+ ${feedbackScoreExpr} AS recall_score
|
|
535
|
+
FROM linked_summary_hits lsh
|
|
536
|
+
JOIN eligible_memories m ON m.id = lsh.memory_id
|
|
537
|
+
ORDER BY
|
|
538
|
+
recall_score DESC,
|
|
539
|
+
lsh.latest_summary_at DESC NULLS LAST,
|
|
540
|
+
m.accepted_at DESC NULLS LAST,
|
|
541
|
+
m.id ASC
|
|
542
|
+
LIMIT $${params.length}`,
|
|
543
|
+
params,
|
|
544
|
+
);
|
|
545
|
+
const applicableRows = scopeKeys
|
|
546
|
+
? resolveApplicableRecords(result.rows, opts)
|
|
547
|
+
: result.rows;
|
|
548
|
+
return applicableRows
|
|
549
|
+
.sort(sortRecallRows)
|
|
550
|
+
.slice(0, limit);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
recall,
|
|
555
|
+
recallViaEvidenceItems,
|
|
556
|
+
recallViaMemoryEmbeddings,
|
|
557
|
+
recallViaLinkedSummaryEmbeddings,
|
|
558
|
+
rankHybridMemoryRows,
|
|
559
|
+
};
|
|
242
560
|
}
|
|
243
561
|
|
|
244
562
|
module.exports = {
|
|
245
563
|
recallMemoryRecords,
|
|
246
564
|
createMemoryRecall,
|
|
565
|
+
rankHybridMemoryRows,
|
|
247
566
|
};
|
package/core/memory-records.js
CHANGED
|
@@ -17,6 +17,14 @@ function toJsonOrNull(value) {
|
|
|
17
17
|
return value === undefined || value === null ? null : JSON.stringify(value);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function vecToStr(vec) {
|
|
21
|
+
if (!vec || !Array.isArray(vec) || vec.length === 0) return null;
|
|
22
|
+
for (let i = 0; i < vec.length; i++) {
|
|
23
|
+
if (!Number.isFinite(vec[i])) throw new Error(`Vector contains non-finite value at index ${i}`);
|
|
24
|
+
}
|
|
25
|
+
return `[${vec.join(',')}]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function advisoryLockKeys(namespace, value) {
|
|
21
29
|
const digest = crypto.createHash('sha256').update(`${namespace}:${value}`).digest();
|
|
22
30
|
return [digest.readInt32BE(0), digest.readInt32BE(4)];
|
|
@@ -104,11 +112,13 @@ function compareRecordIdAsc(a, b) {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
function normalizeCurrentMemoryRow(row = {}) {
|
|
115
|
+
const { embedding: _embedding, ...publicRow } = row;
|
|
116
|
+
void _embedding;
|
|
107
117
|
const memoryId = row.memoryId ?? row.memory_id ?? row.id ?? null;
|
|
108
118
|
const evidenceRefsValue = row.evidenceRefs ?? row.evidence_refs ?? [];
|
|
109
119
|
const evidenceRefs = Array.isArray(evidenceRefsValue) ? evidenceRefsValue : [];
|
|
110
120
|
return {
|
|
111
|
-
...
|
|
121
|
+
...publicRow,
|
|
112
122
|
memoryId: memoryId === null ? null : String(memoryId),
|
|
113
123
|
canonicalKey: row.canonicalKey ?? row.canonical_key ?? null,
|
|
114
124
|
memoryType: row.memoryType ?? row.memory_type ?? null,
|
|
@@ -170,6 +180,7 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
170
180
|
const memories = `${schema}.memory_records`;
|
|
171
181
|
const factAssertions = `${schema}.fact_assertions_v1`;
|
|
172
182
|
const evidenceRefs = `${schema}.evidence_refs`;
|
|
183
|
+
const evidenceItems = `${schema}.evidence_items`;
|
|
173
184
|
const feedback = `${schema}.feedback`;
|
|
174
185
|
const canTransact = typeof pool.connect === 'function';
|
|
175
186
|
|
|
@@ -251,12 +262,12 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
251
262
|
valid_to, stale_after, superseded_by, backing_fact_id, observed_at,
|
|
252
263
|
revoked_at, superseded_at, version_id, visible_in_bootstrap,
|
|
253
264
|
visible_in_recall, rank_features, created_by_finalization_id,
|
|
254
|
-
created_by_compaction_run_id
|
|
265
|
+
created_by_compaction_run_id, embedding
|
|
255
266
|
)
|
|
256
267
|
VALUES (
|
|
257
268
|
$1,$2,$3,$4,$5,$6,$7,COALESCE($8,''),COALESCE($9::jsonb,'{}'::jsonb),
|
|
258
269
|
COALESCE($10,'candidate'),COALESCE($11,'llm_inference'),$12,$13,$14,$15,
|
|
259
|
-
$16,$17,$18,$19,$20,$21,COALESCE($22,false),COALESCE($23,false),COALESCE($24::jsonb,'{}'::jsonb),$25,$26
|
|
270
|
+
$16,$17,$18,$19,$20,$21,COALESCE($22,false),COALESCE($23,false),COALESCE($24::jsonb,'{}'::jsonb),$25,$26,$27::vector
|
|
260
271
|
)
|
|
261
272
|
ON CONFLICT (tenant_id, canonical_key) WHERE status = 'active' DO UPDATE SET
|
|
262
273
|
scope_id = EXCLUDED.scope_id,
|
|
@@ -279,7 +290,8 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
279
290
|
visible_in_recall = EXCLUDED.visible_in_recall,
|
|
280
291
|
rank_features = COALESCE(NULLIF(EXCLUDED.rank_features, '{}'::jsonb), ${memories}.rank_features),
|
|
281
292
|
created_by_finalization_id = COALESCE(${memories}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
|
|
282
|
-
created_by_compaction_run_id = COALESCE(${memories}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
|
|
293
|
+
created_by_compaction_run_id = COALESCE(${memories}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id),
|
|
294
|
+
embedding = COALESCE(EXCLUDED.embedding, ${memories}.embedding)
|
|
283
295
|
RETURNING *`,
|
|
284
296
|
[
|
|
285
297
|
tenantId,
|
|
@@ -308,6 +320,7 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
308
320
|
toJson(input.rankFeatures, {}),
|
|
309
321
|
input.createdByFinalizationId || input.created_by_finalization_id || null,
|
|
310
322
|
input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
|
|
323
|
+
vecToStr(input.embedding),
|
|
311
324
|
]
|
|
312
325
|
);
|
|
313
326
|
return result.rows[0] || null;
|
|
@@ -394,18 +407,27 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
394
407
|
requireField(input, 'sourceKind');
|
|
395
408
|
requireField(input, 'sourceRef');
|
|
396
409
|
const tenantId = input.tenantId || defaultTenantId;
|
|
410
|
+
const evidenceItemId = input.evidenceItemId || input.evidence_item_id || null;
|
|
411
|
+
const conflictTarget = evidenceItemId
|
|
412
|
+
? `(tenant_id, owner_kind, owner_id, evidence_item_id, relation_kind)
|
|
413
|
+
WHERE evidence_item_id IS NOT NULL`
|
|
414
|
+
: `(tenant_id, owner_kind, owner_id, source_kind, source_ref, relation_kind)
|
|
415
|
+
WHERE evidence_item_id IS NULL`;
|
|
397
416
|
const result = await pool.query(
|
|
398
417
|
`INSERT INTO ${evidenceRefs} (
|
|
399
418
|
tenant_id, owner_kind, owner_id, source_kind, source_ref,
|
|
400
419
|
relation_kind, weight, metadata, created_by_finalization_id,
|
|
401
|
-
created_by_compaction_run_id
|
|
420
|
+
created_by_compaction_run_id, evidence_item_id
|
|
402
421
|
)
|
|
403
|
-
VALUES ($1,$2,$3,$4,$5,COALESCE($6,'supporting'),COALESCE($7,1.0),COALESCE($8::jsonb,'{}'::jsonb),$9,$10)
|
|
404
|
-
ON CONFLICT
|
|
422
|
+
VALUES ($1,$2,$3,$4,$5,COALESCE($6,'supporting'),COALESCE($7,1.0),COALESCE($8::jsonb,'{}'::jsonb),$9,$10,$11)
|
|
423
|
+
ON CONFLICT ${conflictTarget}
|
|
405
424
|
DO UPDATE SET weight = EXCLUDED.weight,
|
|
425
|
+
source_kind = EXCLUDED.source_kind,
|
|
426
|
+
source_ref = EXCLUDED.source_ref,
|
|
406
427
|
metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${evidenceRefs}.metadata),
|
|
407
428
|
created_by_finalization_id = COALESCE(${evidenceRefs}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
|
|
408
|
-
created_by_compaction_run_id = COALESCE(${evidenceRefs}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
|
|
429
|
+
created_by_compaction_run_id = COALESCE(${evidenceRefs}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id),
|
|
430
|
+
evidence_item_id = COALESCE(${evidenceRefs}.evidence_item_id, EXCLUDED.evidence_item_id)
|
|
409
431
|
RETURNING *`,
|
|
410
432
|
[
|
|
411
433
|
tenantId,
|
|
@@ -418,11 +440,55 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
418
440
|
toJson(input.metadata, {}),
|
|
419
441
|
input.createdByFinalizationId || input.created_by_finalization_id || null,
|
|
420
442
|
input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
|
|
443
|
+
evidenceItemId,
|
|
421
444
|
]
|
|
422
445
|
);
|
|
423
446
|
return result.rows[0] || null;
|
|
424
447
|
}
|
|
425
448
|
|
|
449
|
+
async function upsertEvidenceItem(input = {}) {
|
|
450
|
+
requireField(input, 'sourceKind');
|
|
451
|
+
requireField(input, 'sourceRef');
|
|
452
|
+
requireField(input, 'excerptText');
|
|
453
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
454
|
+
const excerptText = String(input.excerptText || input.excerpt_text || '').trim();
|
|
455
|
+
const excerptHash = input.excerptHash || input.excerpt_hash || crypto
|
|
456
|
+
.createHash('sha256')
|
|
457
|
+
.update(excerptText)
|
|
458
|
+
.digest('hex');
|
|
459
|
+
const result = await pool.query(
|
|
460
|
+
`INSERT INTO ${evidenceItems} (
|
|
461
|
+
tenant_id, source_kind, source_ref, session_row_id, turn_embedding_id,
|
|
462
|
+
summary_row_id, created_by_finalization_id, excerpt_text, excerpt_hash,
|
|
463
|
+
embedding, metadata
|
|
464
|
+
)
|
|
465
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::vector,COALESCE($11::jsonb,'{}'::jsonb))
|
|
466
|
+
ON CONFLICT (tenant_id, source_kind, source_ref, excerpt_hash)
|
|
467
|
+
DO UPDATE SET
|
|
468
|
+
session_row_id = COALESCE(${evidenceItems}.session_row_id, EXCLUDED.session_row_id),
|
|
469
|
+
turn_embedding_id = COALESCE(${evidenceItems}.turn_embedding_id, EXCLUDED.turn_embedding_id),
|
|
470
|
+
summary_row_id = COALESCE(${evidenceItems}.summary_row_id, EXCLUDED.summary_row_id),
|
|
471
|
+
created_by_finalization_id = COALESCE(${evidenceItems}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
|
|
472
|
+
embedding = COALESCE(${evidenceItems}.embedding, EXCLUDED.embedding),
|
|
473
|
+
metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${evidenceItems}.metadata)
|
|
474
|
+
RETURNING *`,
|
|
475
|
+
[
|
|
476
|
+
tenantId,
|
|
477
|
+
input.sourceKind || input.source_kind,
|
|
478
|
+
input.sourceRef || input.source_ref,
|
|
479
|
+
input.sessionRowId || input.session_row_id || null,
|
|
480
|
+
input.turnEmbeddingId || input.turn_embedding_id || null,
|
|
481
|
+
input.summaryRowId || input.summary_row_id || null,
|
|
482
|
+
input.createdByFinalizationId || input.created_by_finalization_id || null,
|
|
483
|
+
excerptText,
|
|
484
|
+
excerptHash,
|
|
485
|
+
vecToStr(input.embedding),
|
|
486
|
+
toJson(input.metadata, {}),
|
|
487
|
+
],
|
|
488
|
+
);
|
|
489
|
+
return result.rows[0] || null;
|
|
490
|
+
}
|
|
491
|
+
|
|
426
492
|
async function recordFeedback(input = {}) {
|
|
427
493
|
requireField(input, 'targetKind');
|
|
428
494
|
requireField(input, 'targetId');
|
|
@@ -636,6 +702,9 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
636
702
|
params.push(input.visibleInRecall === true);
|
|
637
703
|
where.push(`m.visible_in_recall = $${params.length}`);
|
|
638
704
|
}
|
|
705
|
+
if (input.withoutEmbedding === true) {
|
|
706
|
+
where.push(`m.embedding IS NULL`);
|
|
707
|
+
}
|
|
639
708
|
params.push(Math.max(1, Math.min(200, input.limit || 50)));
|
|
640
709
|
const orderBy = input.visibleInBootstrap === true
|
|
641
710
|
? BOOTSTRAP_ORDER_SQL
|
|
@@ -652,6 +721,54 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
652
721
|
return result.rows;
|
|
653
722
|
}
|
|
654
723
|
|
|
724
|
+
async function updateMemoryEmbedding(input = {}) {
|
|
725
|
+
requireField(input, 'memoryId');
|
|
726
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
727
|
+
const embedding = vecToStr(input.embedding);
|
|
728
|
+
if (!embedding) throw new Error('embedding is required');
|
|
729
|
+
const result = await pool.query(
|
|
730
|
+
`UPDATE ${memories}
|
|
731
|
+
SET embedding = $3::vector,
|
|
732
|
+
updated_at = now()
|
|
733
|
+
WHERE tenant_id = $1 AND id = $2
|
|
734
|
+
AND embedding IS NULL
|
|
735
|
+
RETURNING *`,
|
|
736
|
+
[
|
|
737
|
+
tenantId,
|
|
738
|
+
input.memoryId,
|
|
739
|
+
embedding,
|
|
740
|
+
]
|
|
741
|
+
);
|
|
742
|
+
if (result.rows[0]) {
|
|
743
|
+
return {
|
|
744
|
+
status: 'updated',
|
|
745
|
+
updated: true,
|
|
746
|
+
skipped: false,
|
|
747
|
+
memory: result.rows[0],
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const existing = await pool.query(
|
|
751
|
+
`SELECT * FROM ${memories}
|
|
752
|
+
WHERE tenant_id = $1 AND id = $2
|
|
753
|
+
LIMIT 1`,
|
|
754
|
+
[tenantId, input.memoryId]
|
|
755
|
+
);
|
|
756
|
+
if (existing.rows[0]) {
|
|
757
|
+
return {
|
|
758
|
+
status: 'skipped_existing_embedding',
|
|
759
|
+
updated: false,
|
|
760
|
+
skipped: true,
|
|
761
|
+
memory: existing.rows[0],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
status: 'missing',
|
|
766
|
+
updated: false,
|
|
767
|
+
skipped: true,
|
|
768
|
+
memory: null,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
655
772
|
async function currentProjection(input = {}) {
|
|
656
773
|
const tenantId = input.tenantId || defaultTenantId;
|
|
657
774
|
let activeScopePath = normalizeScopePath(input.activeScopePath, input.activeScopeKey);
|
|
@@ -778,6 +895,7 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
778
895
|
createVersion,
|
|
779
896
|
upsertMemory,
|
|
780
897
|
upsertFactAssertion,
|
|
898
|
+
upsertEvidenceItem,
|
|
781
899
|
linkEvidence,
|
|
782
900
|
recordFeedback,
|
|
783
901
|
findActiveByCanonicalKey,
|
|
@@ -785,9 +903,11 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
785
903
|
lockCanonicalKey,
|
|
786
904
|
updateMemoryStatus,
|
|
787
905
|
updateMemoryStatusIfCurrent,
|
|
906
|
+
updateMemoryEmbedding,
|
|
788
907
|
updateFactAssertionStatus,
|
|
789
908
|
listActive,
|
|
790
909
|
currentProjection,
|
|
910
|
+
normalizeCurrentMemoryRow,
|
|
791
911
|
withTransaction,
|
|
792
912
|
};
|
|
793
913
|
|