@shadowforge0/aquifer-memory 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- package/scripts/codex-recovery.js +246 -1
package/core/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
|
};
|