@jungjaehoon/mama-core 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -10
- package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
- package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
- package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
- package/db/migrations/035-create-twin-edges.sql +63 -0
- package/db/migrations/036-create-agent-situation-packets.sql +61 -0
- package/dist/agent-graph/alias-write.d.ts +3 -0
- package/dist/agent-graph/alias-write.js +231 -0
- package/dist/agent-graph/entity-resolve.d.ts +3 -0
- package/dist/agent-graph/entity-resolve.js +275 -0
- package/dist/agent-graph/errors.d.ts +4 -0
- package/dist/agent-graph/errors.js +11 -0
- package/dist/agent-graph/graph-query.d.ts +5 -0
- package/dist/agent-graph/graph-query.js +359 -0
- package/dist/agent-graph/index.d.ts +6 -0
- package/dist/agent-graph/index.js +22 -0
- package/dist/agent-graph/types.d.ts +162 -0
- package/dist/agent-graph/types.js +3 -0
- package/dist/agent-situation/builder.d.ts +8 -0
- package/dist/agent-situation/builder.js +239 -0
- package/dist/agent-situation/cache-key.d.ts +4 -0
- package/dist/agent-situation/cache-key.js +64 -0
- package/dist/agent-situation/index.d.ts +7 -0
- package/dist/agent-situation/index.js +28 -0
- package/dist/agent-situation/packet-store.d.ts +27 -0
- package/dist/agent-situation/packet-store.js +228 -0
- package/dist/agent-situation/ranking-policy.d.ts +18 -0
- package/dist/agent-situation/ranking-policy.js +75 -0
- package/dist/agent-situation/source-readers.d.ts +52 -0
- package/dist/agent-situation/source-readers.js +300 -0
- package/dist/agent-situation/types.d.ts +195 -0
- package/dist/agent-situation/types.js +3 -0
- package/dist/cases/search-rollup.d.ts +4 -0
- package/dist/cases/search-rollup.js +84 -0
- package/dist/connectors/event-index.d.ts +1 -0
- package/dist/connectors/event-index.js +19 -4
- package/dist/connectors/raw-query.d.ts +18 -0
- package/dist/connectors/raw-query.js +305 -0
- package/dist/connectors/types.d.ts +40 -0
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
- package/dist/db-adapter/node-sqlite-adapter.js +131 -0
- package/dist/db-manager.d.ts +8 -1
- package/dist/db-manager.js +53 -5
- package/dist/edges/ref-validation.d.ts +8 -0
- package/dist/edges/ref-validation.js +262 -0
- package/dist/edges/store.d.ts +10 -0
- package/dist/edges/store.js +343 -0
- package/dist/edges/types.d.ts +86 -0
- package/dist/edges/types.js +18 -0
- package/dist/embeddings.js +13 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.js +53 -4
- package/dist/mama-api.d.ts +32 -35
- package/dist/mama-api.js +173 -22
- package/dist/memory/api.d.ts +17 -56
- package/dist/memory/api.js +494 -52
- package/dist/memory/event-store.d.ts +3 -0
- package/dist/memory/event-store.js +21 -2
- package/dist/memory/provenance-audit.d.ts +22 -0
- package/dist/memory/provenance-audit.js +112 -0
- package/dist/memory/provenance-query.d.ts +11 -0
- package/dist/memory/provenance-query.js +193 -0
- package/dist/memory/provenance.d.ts +29 -0
- package/dist/memory/provenance.js +136 -0
- package/dist/memory/scope-backfill.d.ts +17 -0
- package/dist/memory/scope-backfill.js +77 -0
- package/dist/memory/types.d.ts +86 -14
- package/dist/model-runs/store.d.ts +13 -0
- package/dist/model-runs/store.js +457 -0
- package/dist/model-runs/tool-trace-store.d.ts +4 -0
- package/dist/model-runs/tool-trace-store.js +114 -0
- package/dist/model-runs/types.d.ts +69 -0
- package/dist/model-runs/types.js +5 -0
- package/dist/runtime/trusted-provenance.d.ts +3 -0
- package/dist/runtime/trusted-provenance.js +6 -0
- package/dist/search/search-quality.d.ts +36 -0
- package/dist/search/search-quality.js +29 -0
- package/package.json +9 -2
package/dist/memory/api.js
CHANGED
|
@@ -7,15 +7,18 @@ exports.getChannelSummary = exports.upsertChannelSummary = exports.LEXICAL_STOPW
|
|
|
7
7
|
exports.getLexicalQueryTokens = getLexicalQueryTokens;
|
|
8
8
|
exports.loadEdgesForIds = loadEdgesForIds;
|
|
9
9
|
exports.saveMemory = saveMemory;
|
|
10
|
+
exports.saveMemoryWithTrustedProvenance = saveMemoryWithTrustedProvenance;
|
|
10
11
|
exports.buildProfile = buildProfile;
|
|
11
12
|
exports.recallMemory = recallMemory;
|
|
12
13
|
exports.ingestMemory = ingestMemory;
|
|
14
|
+
exports.ingestWithTrustedProvenance = ingestWithTrustedProvenance;
|
|
13
15
|
exports.evolveMemory = evolveMemory;
|
|
14
16
|
exports.buildMemoryBootstrap = buildMemoryBootstrap;
|
|
15
17
|
exports.createAuditAck = createAuditAck;
|
|
16
18
|
exports.recordMemoryAudit = recordMemoryAudit;
|
|
17
19
|
exports.setExtractionFn = setExtractionFn;
|
|
18
20
|
exports.ingestConversation = ingestConversation;
|
|
21
|
+
exports.ingestConversationWithTrustedProvenance = ingestConversationWithTrustedProvenance;
|
|
19
22
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
23
|
const db_manager_js_1 = require("../db-manager.js");
|
|
21
24
|
const embeddings_js_1 = require("../embeddings.js");
|
|
@@ -33,10 +36,23 @@ Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: fun
|
|
|
33
36
|
Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.upsertChannelSummary; } });
|
|
34
37
|
const recall_bridge_js_1 = require("../entities/recall-bridge.js");
|
|
35
38
|
const read_identity_js_1 = require("../entities/read-identity.js");
|
|
39
|
+
const search_quality_js_1 = require("../search/search-quality.js");
|
|
40
|
+
const event_store_js_1 = require("./event-store.js");
|
|
41
|
+
const provenance_js_1 = require("./provenance.js");
|
|
36
42
|
function buildDecisionId(topic) {
|
|
37
43
|
const safeTopic = topic.replace(/[^a-z0-9_]+/gi, '_').toLowerCase();
|
|
38
44
|
return `decision_${safeTopic}_${Date.now()}_${node_crypto_1.default.randomUUID().slice(0, 8)}`;
|
|
39
45
|
}
|
|
46
|
+
function buildSaveEventReason(toolName, gatewayCallId) {
|
|
47
|
+
const parts = ['saved'];
|
|
48
|
+
if (toolName) {
|
|
49
|
+
parts.push(`via ${toolName}`);
|
|
50
|
+
}
|
|
51
|
+
if (gatewayCallId) {
|
|
52
|
+
parts.push(`gateway_call_id=${gatewayCallId}`);
|
|
53
|
+
}
|
|
54
|
+
return parts.join(' ');
|
|
55
|
+
}
|
|
40
56
|
function toMemoryRecord(row, scopes, fallbackSource) {
|
|
41
57
|
let trustContext = null;
|
|
42
58
|
if (typeof row.trust_context === 'string') {
|
|
@@ -304,6 +320,19 @@ function getLexicalQueryTokens(query) {
|
|
|
304
320
|
.split(/[\s,.!?;:()[\]{}"']+/)
|
|
305
321
|
.filter((token) => token.length > 2 && !exports.LEXICAL_STOPWORDS.has(token));
|
|
306
322
|
}
|
|
323
|
+
function queryTokenCount(query) {
|
|
324
|
+
const lexicalTokens = getLexicalQueryTokens(query);
|
|
325
|
+
if (lexicalTokens.length > 0) {
|
|
326
|
+
return lexicalTokens.length;
|
|
327
|
+
}
|
|
328
|
+
return query.trim().split(/\s+/).filter(Boolean).length;
|
|
329
|
+
}
|
|
330
|
+
function looksMixedKoreanEnglish(query) {
|
|
331
|
+
return /[\uac00-\ud7a3]/.test(query) && /[a-z]/i.test(query);
|
|
332
|
+
}
|
|
333
|
+
function looksEntityLike(query) {
|
|
334
|
+
return /\b[A-Z][A-Za-z0-9_-]{2,}\b/.test(query) || /[#/][A-Za-z0-9_-]{2,}/.test(query);
|
|
335
|
+
}
|
|
307
336
|
function buildLexicalCandidates(records, query) {
|
|
308
337
|
const tokens = getLexicalQueryTokens(query);
|
|
309
338
|
const normalizedQuery = query.toLowerCase();
|
|
@@ -407,11 +436,12 @@ async function loadEdgesForIds(ids) {
|
|
|
407
436
|
reason: row.reason ?? undefined,
|
|
408
437
|
}));
|
|
409
438
|
}
|
|
410
|
-
async function
|
|
439
|
+
async function saveMemoryInternal(input, options) {
|
|
411
440
|
await (0, db_manager_js_1.initDB)();
|
|
412
441
|
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
413
442
|
const id = buildDecisionId(input.topic);
|
|
414
443
|
const now = Date.now();
|
|
444
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)(options);
|
|
415
445
|
// Find evolution candidates: exact topic match first, then semantic search fallback
|
|
416
446
|
const primaryScope = input.scopes.length > 0 ? input.scopes[0] : null;
|
|
417
447
|
const excludeIds = new Set(input.excludeIds ?? []);
|
|
@@ -504,6 +534,12 @@ async function saveMemory(input) {
|
|
|
504
534
|
trust_context: JSON.stringify({ source: input.source }),
|
|
505
535
|
event_date: input.eventDate ?? null,
|
|
506
536
|
event_datetime: eventDateTime,
|
|
537
|
+
agent_id: provenance.agent_id,
|
|
538
|
+
model_run_id: provenance.model_run_id,
|
|
539
|
+
envelope_hash: provenance.envelope_hash,
|
|
540
|
+
gateway_call_id: provenance.gateway_call_id,
|
|
541
|
+
source_refs_json: JSON.stringify(provenance.source_refs),
|
|
542
|
+
provenance_json: JSON.stringify(provenance.provenance),
|
|
507
543
|
});
|
|
508
544
|
// Pre-resolve scope IDs before the synchronous transaction
|
|
509
545
|
const resolvedScopeIds = [];
|
|
@@ -529,6 +565,17 @@ async function saveMemory(input) {
|
|
|
529
565
|
`)
|
|
530
566
|
.run(id, scopeId, isPrimary ? 1 : 0);
|
|
531
567
|
}
|
|
568
|
+
(0, event_store_js_1.insertMemoryEventInTransaction)(adapter, {
|
|
569
|
+
event_type: 'save',
|
|
570
|
+
actor: provenance.actor,
|
|
571
|
+
source_turn_id: provenance.source_turn_id ?? undefined,
|
|
572
|
+
memory_id: id,
|
|
573
|
+
topic: input.topic,
|
|
574
|
+
scope_refs: input.scopes,
|
|
575
|
+
evidence_refs: provenance.source_refs,
|
|
576
|
+
reason: buildSaveEventReason(provenance.tool_name, provenance.gateway_call_id),
|
|
577
|
+
created_at: now,
|
|
578
|
+
});
|
|
532
579
|
for (const entityObservationId of entityObservationIds) {
|
|
533
580
|
adapter
|
|
534
581
|
.prepare(`
|
|
@@ -587,6 +634,12 @@ async function saveMemory(input) {
|
|
|
587
634
|
timeline_event_ids: timelineEvent ? [timelineEvent.id] : [],
|
|
588
635
|
};
|
|
589
636
|
}
|
|
637
|
+
async function saveMemory(input) {
|
|
638
|
+
return saveMemoryInternal((0, provenance_js_1.sanitizePublicSaveMemoryInput)(input));
|
|
639
|
+
}
|
|
640
|
+
async function saveMemoryWithTrustedProvenance(input, options) {
|
|
641
|
+
return saveMemoryInternal((0, provenance_js_1.sanitizePublicSaveMemoryInput)(input), options);
|
|
642
|
+
}
|
|
590
643
|
async function buildProfile(scopes) {
|
|
591
644
|
const records = await loadScopedMemories(scopes);
|
|
592
645
|
return (0, profile_builder_js_1.classifyProfileEntries)(records);
|
|
@@ -599,10 +652,26 @@ const EXCLUDED_STATUSES = new Set([
|
|
|
599
652
|
]);
|
|
600
653
|
async function recallMemory(query, options = {}) {
|
|
601
654
|
const bundle = (0, types_js_1.createEmptyRecallBundle)(query);
|
|
655
|
+
const searchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)(options);
|
|
656
|
+
const diagnostics = {
|
|
657
|
+
candidate_counts: {
|
|
658
|
+
vector: 0,
|
|
659
|
+
lexical: 0,
|
|
660
|
+
entity: 0,
|
|
661
|
+
graph_expanded: 0,
|
|
662
|
+
vector_only: 0,
|
|
663
|
+
rejected_by_strictness: 0,
|
|
664
|
+
},
|
|
665
|
+
threshold: searchOptions.threshold,
|
|
666
|
+
strictness: searchOptions.strictness,
|
|
667
|
+
};
|
|
602
668
|
let matched = [];
|
|
603
669
|
let fusedHits = [];
|
|
604
670
|
let retrievalSource = 'none';
|
|
605
671
|
const projectionMode = process.env.MAMA_ENTITY_PROJECTION_MODE ?? 'shadow';
|
|
672
|
+
const vectorSimilarityById = new Map();
|
|
673
|
+
const lexicalScoreById = new Map();
|
|
674
|
+
const entitySupportIds = new Set();
|
|
606
675
|
let _lexicalRecords = null;
|
|
607
676
|
const loadLexical = async () => {
|
|
608
677
|
if (_lexicalRecords === null) {
|
|
@@ -639,14 +708,15 @@ async function recallMemory(query, options = {}) {
|
|
|
639
708
|
if (sq === query && primaryQueryEmbedding === null) {
|
|
640
709
|
primaryQueryEmbedding = queryEmbedding;
|
|
641
710
|
}
|
|
642
|
-
const vectorResults = await (0, db_manager_js_1.vectorSearch)(queryEmbedding, vectorLimit,
|
|
711
|
+
const vectorResults = await (0, db_manager_js_1.vectorSearch)(queryEmbedding, vectorLimit, searchOptions.threshold, searchOptions.topicPrefix);
|
|
643
712
|
let filtered = vectorResults;
|
|
713
|
+
let vectorScopeMap = new Map();
|
|
644
714
|
if (options.scopes && options.scopes.length > 0) {
|
|
645
715
|
const vectorIds = vectorResults.map((r) => String(r.id));
|
|
646
|
-
|
|
716
|
+
vectorScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), vectorIds);
|
|
647
717
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
648
718
|
filtered = vectorResults.filter((r) => {
|
|
649
|
-
const scopes =
|
|
719
|
+
const scopes = vectorScopeMap.get(String(r.id)) ?? [];
|
|
650
720
|
// When scopes are requested, zero-binding results must NOT pass through
|
|
651
721
|
if (scopes.length === 0)
|
|
652
722
|
return false;
|
|
@@ -666,13 +736,17 @@ async function recallMemory(query, options = {}) {
|
|
|
666
736
|
details: String(result.reasoning || ''),
|
|
667
737
|
confidence: result.similarity ?? 0.5,
|
|
668
738
|
status: effectiveStatus || 'active',
|
|
669
|
-
scopes: [],
|
|
739
|
+
scopes: vectorScopeMap.get(String(result.id)) ?? [],
|
|
670
740
|
source: { package: 'mama-core', source_type: 'vector_search' },
|
|
671
741
|
created_at: result.created_at ?? Date.now(),
|
|
672
742
|
updated_at: result.created_at ?? Date.now(),
|
|
673
743
|
event_date: result.event_date ?? null,
|
|
674
744
|
event_datetime: result.event_datetime ?? null,
|
|
675
745
|
});
|
|
746
|
+
diagnostics.candidate_counts.vector += 1;
|
|
747
|
+
if (typeof result.similarity === 'number' && Number.isFinite(result.similarity)) {
|
|
748
|
+
vectorSimilarityById.set(String(result.id), result.similarity);
|
|
749
|
+
}
|
|
676
750
|
}
|
|
677
751
|
} // end sub-query loop
|
|
678
752
|
}
|
|
@@ -694,7 +768,14 @@ async function recallMemory(query, options = {}) {
|
|
|
694
768
|
// Lazy lexical: skip expensive FTS5/lexical when vector already returned enough results,
|
|
695
769
|
// unless this is an aggregation query that benefits from broader coverage.
|
|
696
770
|
const VECTOR_SUFFICIENT_THRESHOLD = 5;
|
|
697
|
-
const
|
|
771
|
+
const shouldForceLexicalConfirmation = searchOptions.strictness !== 'recall' ||
|
|
772
|
+
searchOptions.minLexicalSupport ||
|
|
773
|
+
queryTokenCount(query) <= 3 ||
|
|
774
|
+
looksMixedKoreanEnglish(query) ||
|
|
775
|
+
looksEntityLike(query);
|
|
776
|
+
const needsLexical = isAggregation ||
|
|
777
|
+
vectorMatched.length < VECTOR_SUFFICIENT_THRESHOLD ||
|
|
778
|
+
shouldForceLexicalConfirmation;
|
|
698
779
|
let lexicalCandidates = [];
|
|
699
780
|
const lexicalLimit = isAggregation ? 100 : 50;
|
|
700
781
|
if (needsLexical) {
|
|
@@ -766,7 +847,7 @@ async function recallMemory(query, options = {}) {
|
|
|
766
847
|
const scopeMap = batchLoadScopes(adapter, memoryIds);
|
|
767
848
|
const record = toMemoryRecord(row, scopeMap.get(String(row.id)) ?? [], fallbackSource);
|
|
768
849
|
// Topic prefix filtering (matches vectorSearch behavior)
|
|
769
|
-
if (
|
|
850
|
+
if (searchOptions.topicPrefix && !record.topic.startsWith(searchOptions.topicPrefix))
|
|
770
851
|
continue;
|
|
771
852
|
// Scope filtering
|
|
772
853
|
if (options.scopes && options.scopes.length > 0) {
|
|
@@ -800,8 +881,8 @@ async function recallMemory(query, options = {}) {
|
|
|
800
881
|
});
|
|
801
882
|
}
|
|
802
883
|
// Topic prefix filtering for in-memory lexical (matches vectorSearch behavior)
|
|
803
|
-
if (
|
|
804
|
-
lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(
|
|
884
|
+
if (searchOptions.topicPrefix) {
|
|
885
|
+
lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(searchOptions.topicPrefix));
|
|
805
886
|
}
|
|
806
887
|
lexicalCandidates = buildLexicalCandidates(lexicalRecords, query);
|
|
807
888
|
if (subQueries.length > 1) {
|
|
@@ -817,6 +898,10 @@ async function recallMemory(query, options = {}) {
|
|
|
817
898
|
}
|
|
818
899
|
}
|
|
819
900
|
} // end needsLexical
|
|
901
|
+
diagnostics.candidate_counts.lexical = lexicalCandidates.length;
|
|
902
|
+
for (const candidate of lexicalCandidates) {
|
|
903
|
+
lexicalScoreById.set(candidate.memory.id, candidate.score);
|
|
904
|
+
}
|
|
820
905
|
// RRF Fusion: combine vector and lexical/FTS5 results by reciprocal rank
|
|
821
906
|
// FTS5 BM25 gets 2x weight — it naturally demotes records where query terms
|
|
822
907
|
// appear only in passing (e.g. "wedding" mentioned once in a dating record),
|
|
@@ -861,6 +946,7 @@ async function recallMemory(query, options = {}) {
|
|
|
861
946
|
let hasWikiHits = false;
|
|
862
947
|
const wikiScores = new Map();
|
|
863
948
|
const wikiVectorRankMap = new Map();
|
|
949
|
+
const wikiVectorScoreById = new Map();
|
|
864
950
|
try {
|
|
865
951
|
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
866
952
|
const wikiFtsHits = (0, wiki_page_index_js_1.ftsSearchWikiPages)(adapter, query, requestedLimit * 2);
|
|
@@ -875,7 +961,10 @@ async function recallMemory(query, options = {}) {
|
|
|
875
961
|
}
|
|
876
962
|
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
877
963
|
wikiVectorRankMap.set(wikiVectorHits[i].record.id, i);
|
|
964
|
+
wikiVectorScoreById.set(wikiVectorHits[i].record.id, wikiVectorHits[i].raw_score);
|
|
878
965
|
}
|
|
966
|
+
diagnostics.candidate_counts.lexical += wikiFtsHits.length;
|
|
967
|
+
diagnostics.candidate_counts.vector += wikiVectorHits.length;
|
|
879
968
|
for (let i = 0; i < wikiFtsHits.length; i++) {
|
|
880
969
|
const hit = wikiFtsHits[i];
|
|
881
970
|
const lexScore = 1 / (RRF_K + i + 1);
|
|
@@ -884,6 +973,8 @@ async function recallMemory(query, options = {}) {
|
|
|
884
973
|
wikiScores.set(hit.record.id, {
|
|
885
974
|
record: hit.record,
|
|
886
975
|
score: lexScore + vecBoost,
|
|
976
|
+
lexicalSupport: true,
|
|
977
|
+
vectorSimilarity: wikiVectorScoreById.get(hit.record.id) ?? null,
|
|
887
978
|
});
|
|
888
979
|
}
|
|
889
980
|
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
@@ -894,24 +985,15 @@ async function recallMemory(query, options = {}) {
|
|
|
894
985
|
wikiScores.set(hit.record.id, {
|
|
895
986
|
record: hit.record,
|
|
896
987
|
score: 0.15 * (1 / (RRF_K + i + 1)),
|
|
988
|
+
lexicalSupport: false,
|
|
989
|
+
vectorSimilarity: hit.raw_score,
|
|
897
990
|
});
|
|
898
991
|
}
|
|
899
|
-
hasWikiHits = wikiScores.size > 0;
|
|
900
992
|
}
|
|
901
993
|
catch (wikiFtsErr) {
|
|
902
994
|
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki FTS search failed: ${wikiFtsErr instanceof Error ? wikiFtsErr.message : String(wikiFtsErr)}`);
|
|
903
995
|
}
|
|
904
|
-
|
|
905
|
-
.sort((a, b) => b.score - a.score)
|
|
906
|
-
.map((entry) => ({
|
|
907
|
-
source_type: 'wiki_page',
|
|
908
|
-
source_id: entry.record.source_locator,
|
|
909
|
-
record: entry.record,
|
|
910
|
-
fused_rank_score: entry.score,
|
|
911
|
-
...(entry.record.page_type === 'case' ? { page_type: 'case' } : {}),
|
|
912
|
-
...(entry.record.case_id ? { case_id: entry.record.case_id } : {}),
|
|
913
|
-
}));
|
|
914
|
-
fusedHits = [...decisionFusedHits, ...wikiFusedHits].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
996
|
+
fusedHits = decisionFusedHits;
|
|
915
997
|
// Normalize RRF scores to 0-1 range so downstream consumers (threshold filters,
|
|
916
998
|
// similarity displays) get meaningful values. The raw RRF score for K=60 tops out
|
|
917
999
|
// around 0.033, which is misleading when compared against similarity thresholds.
|
|
@@ -929,13 +1011,14 @@ async function recallMemory(query, options = {}) {
|
|
|
929
1011
|
else if (lexicalCandidates.length > 0) {
|
|
930
1012
|
retrievalSource = 'lexical_search';
|
|
931
1013
|
}
|
|
932
|
-
if (hasWikiHits) {
|
|
933
|
-
retrievalSource = retrievalSource === 'none' ? 'wiki_page' : `${retrievalSource}+wiki_page`;
|
|
934
|
-
}
|
|
935
1014
|
let canonicalMatched = [];
|
|
936
1015
|
if (projectionMode !== 'off') {
|
|
937
1016
|
try {
|
|
938
1017
|
canonicalMatched = await (0, recall_bridge_js_1.queryCanonicalEntities)(query, options.scopes ?? [], { limit: 10 });
|
|
1018
|
+
diagnostics.candidate_counts.entity = canonicalMatched.length;
|
|
1019
|
+
for (const canonical of canonicalMatched) {
|
|
1020
|
+
entitySupportIds.add(canonical.id);
|
|
1021
|
+
}
|
|
939
1022
|
if (projectionMode === 'dual-write' && canonicalMatched.length > 0) {
|
|
940
1023
|
const seenIds = new Set(matched.map((item) => item.id));
|
|
941
1024
|
for (const canonical of canonicalMatched) {
|
|
@@ -956,6 +1039,318 @@ async function recallMemory(query, options = {}) {
|
|
|
956
1039
|
(0, debug_logger_js_1.warn)(`[recallMemory] Canonical entity recall failed: ${canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr)}`);
|
|
957
1040
|
}
|
|
958
1041
|
}
|
|
1042
|
+
const requestedScopeKeys = new Set((options.scopes ?? []).map((scope) => `${scope.kind}:${scope.id}`));
|
|
1043
|
+
const hasRequestedScopeSupport = (record) => {
|
|
1044
|
+
if (requestedScopeKeys.size === 0) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
return record.scopes.some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`));
|
|
1048
|
+
};
|
|
1049
|
+
// exact_topic confirmation is intentionally narrow: short substring matches
|
|
1050
|
+
// would let two-letter queries like "ai" trip on words like "email" and
|
|
1051
|
+
// promote vector-only hits past strict/balanced rejection. Require either
|
|
1052
|
+
// exact equality or whole-token containment, and ignore tiny queries.
|
|
1053
|
+
const EXACT_TOPIC_MIN_QUERY_LENGTH = 3;
|
|
1054
|
+
const tokenizeForExactTopic = (input) => input.split(/[^a-z0-9]+/).filter((token) => token.length > 0);
|
|
1055
|
+
const hasExactTopicSupport = (record) => {
|
|
1056
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1057
|
+
const normalizedTopic = record.topic.trim().toLowerCase();
|
|
1058
|
+
if (!normalizedQuery || !normalizedTopic) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
if (normalizedQuery.length < EXACT_TOPIC_MIN_QUERY_LENGTH) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
if (normalizedQuery === normalizedTopic) {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
const queryTokens = tokenizeForExactTopic(normalizedQuery);
|
|
1068
|
+
const topicTokens = tokenizeForExactTopic(normalizedTopic);
|
|
1069
|
+
// Whole-token match: a topic word equals the entire query, OR the topic
|
|
1070
|
+
// (when itself a single token) appears as a complete word in the query.
|
|
1071
|
+
return topicTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTopic);
|
|
1072
|
+
};
|
|
1073
|
+
const hasExactWikiSupport = (record) => {
|
|
1074
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1075
|
+
if (!normalizedQuery || normalizedQuery.length < EXACT_TOPIC_MIN_QUERY_LENGTH) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
const normalizedTitle = record.title.trim().toLowerCase();
|
|
1079
|
+
if (!normalizedTitle) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
if (normalizedQuery === normalizedTitle) {
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
const queryTokens = tokenizeForExactTopic(normalizedQuery);
|
|
1086
|
+
const titleTokens = tokenizeForExactTopic(normalizedTitle);
|
|
1087
|
+
// Wiki exact_topic uses TITLE only — page body matches are too lenient
|
|
1088
|
+
// for an "exact" signal and remain available through other diagnostics.
|
|
1089
|
+
return titleTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTitle);
|
|
1090
|
+
};
|
|
1091
|
+
const wikiDecisionIdCache = new Map();
|
|
1092
|
+
const normalizeWikiDecisionSourceId = (sourceId) => {
|
|
1093
|
+
const trimmed = sourceId.trim();
|
|
1094
|
+
if (!trimmed) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
if (trimmed.startsWith('decision://')) {
|
|
1098
|
+
const id = trimmed.slice('decision://'.length).trim();
|
|
1099
|
+
return id || null;
|
|
1100
|
+
}
|
|
1101
|
+
if (trimmed.startsWith('decision:')) {
|
|
1102
|
+
const id = trimmed.slice('decision:'.length).trim();
|
|
1103
|
+
return id || null;
|
|
1104
|
+
}
|
|
1105
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*:/.test(trimmed)) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
return trimmed;
|
|
1109
|
+
};
|
|
1110
|
+
const loadWikiDecisionIds = (record) => {
|
|
1111
|
+
const cached = wikiDecisionIdCache.get(record.id);
|
|
1112
|
+
if (cached) {
|
|
1113
|
+
return cached;
|
|
1114
|
+
}
|
|
1115
|
+
const ids = new Set();
|
|
1116
|
+
for (const sourceId of record.source_ids) {
|
|
1117
|
+
const decisionId = normalizeWikiDecisionSourceId(sourceId);
|
|
1118
|
+
if (decisionId) {
|
|
1119
|
+
ids.add(decisionId);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (record.case_id) {
|
|
1123
|
+
const rows = (0, db_manager_js_1.getAdapter)()
|
|
1124
|
+
.prepare(`
|
|
1125
|
+
SELECT source_id
|
|
1126
|
+
FROM case_memberships
|
|
1127
|
+
WHERE case_id = ?
|
|
1128
|
+
AND source_type = 'decision'
|
|
1129
|
+
AND status = 'active'
|
|
1130
|
+
`)
|
|
1131
|
+
.all(record.case_id);
|
|
1132
|
+
for (const row of rows) {
|
|
1133
|
+
if (row.source_id) {
|
|
1134
|
+
ids.add(row.source_id);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const decisionIds = Array.from(ids);
|
|
1139
|
+
wikiDecisionIdCache.set(record.id, decisionIds);
|
|
1140
|
+
return decisionIds;
|
|
1141
|
+
};
|
|
1142
|
+
const wikiScopeSupportCache = new Map();
|
|
1143
|
+
const hasRequestedWikiScopeSupport = (record) => {
|
|
1144
|
+
if (requestedScopeKeys.size === 0) {
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
const cached = wikiScopeSupportCache.get(record.id);
|
|
1148
|
+
if (cached !== undefined) {
|
|
1149
|
+
return cached;
|
|
1150
|
+
}
|
|
1151
|
+
const decisionIds = loadWikiDecisionIds(record);
|
|
1152
|
+
if (decisionIds.length === 0) {
|
|
1153
|
+
wikiScopeSupportCache.set(record.id, false);
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
const scopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), decisionIds);
|
|
1157
|
+
const supported = decisionIds.some((id) => (scopeMap.get(id) ?? []).some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`)));
|
|
1158
|
+
wikiScopeSupportCache.set(record.id, supported);
|
|
1159
|
+
return supported;
|
|
1160
|
+
};
|
|
1161
|
+
// Combined check used for the wiki *filter* path: a wiki entry must have at
|
|
1162
|
+
// least one linked decision that matches BOTH the requested scope AND the
|
|
1163
|
+
// requested topicPrefix. Without this, the page passes when scope matches
|
|
1164
|
+
// decision A and topic matches decision B — i.e., neither single decision
|
|
1165
|
+
// satisfies the caller's filter. Diagnostics reporting (which only signals
|
|
1166
|
+
// scope confirmation) keeps using hasRequestedWikiScopeSupport.
|
|
1167
|
+
const wikiCombinedSupportCache = new Map();
|
|
1168
|
+
const hasWikiDecisionWithScopeAndTopic = (record) => {
|
|
1169
|
+
const requiresScope = requestedScopeKeys.size > 0;
|
|
1170
|
+
const requiresTopic = !!searchOptions.topicPrefix;
|
|
1171
|
+
if (!requiresScope && !requiresTopic) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
const cached = wikiCombinedSupportCache.get(record.id);
|
|
1175
|
+
if (cached !== undefined) {
|
|
1176
|
+
return cached;
|
|
1177
|
+
}
|
|
1178
|
+
const decisionIds = loadWikiDecisionIds(record);
|
|
1179
|
+
if (decisionIds.length === 0) {
|
|
1180
|
+
wikiCombinedSupportCache.set(record.id, false);
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
1184
|
+
const scopeMap = requiresScope ? batchLoadScopes(adapter, decisionIds) : null;
|
|
1185
|
+
const topicByDecisionId = new Map();
|
|
1186
|
+
if (requiresTopic) {
|
|
1187
|
+
const IN_CHUNK_SIZE = 900;
|
|
1188
|
+
for (let i = 0; i < decisionIds.length; i += IN_CHUNK_SIZE) {
|
|
1189
|
+
const chunk = decisionIds.slice(i, i + IN_CHUNK_SIZE);
|
|
1190
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
1191
|
+
const rows = adapter
|
|
1192
|
+
.prepare(`SELECT id, topic FROM decisions WHERE id IN (${placeholders})`)
|
|
1193
|
+
.all(...chunk);
|
|
1194
|
+
for (const row of rows) {
|
|
1195
|
+
topicByDecisionId.set(row.id, String(row.topic ?? ''));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const supported = decisionIds.some((id) => {
|
|
1200
|
+
if (requiresScope) {
|
|
1201
|
+
const scopes = scopeMap?.get(id) ?? [];
|
|
1202
|
+
const scopeOk = scopes.some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`));
|
|
1203
|
+
if (!scopeOk) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (requiresTopic) {
|
|
1208
|
+
const topic = topicByDecisionId.get(id) ?? '';
|
|
1209
|
+
if (!topic.startsWith(searchOptions.topicPrefix)) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return true;
|
|
1214
|
+
});
|
|
1215
|
+
wikiCombinedSupportCache.set(record.id, supported);
|
|
1216
|
+
return supported;
|
|
1217
|
+
};
|
|
1218
|
+
const buildDiagnostics = (record, graphSource) => {
|
|
1219
|
+
const vectorSimilarity = vectorSimilarityById.get(record.id) ?? null;
|
|
1220
|
+
const lexicalSupport = lexicalScoreById.has(record.id);
|
|
1221
|
+
const entitySupport = entitySupportIds.has(record.id);
|
|
1222
|
+
const exactTopicSupport = hasExactTopicSupport(record);
|
|
1223
|
+
const scopeSupport = hasRequestedScopeSupport(record);
|
|
1224
|
+
const confirmationSignals = [
|
|
1225
|
+
lexicalSupport ? 'lexical' : null,
|
|
1226
|
+
entitySupport ? 'entity' : null,
|
|
1227
|
+
exactTopicSupport ? 'exact_topic' : null,
|
|
1228
|
+
].filter((signal) => signal !== null);
|
|
1229
|
+
const metadataSignals = [
|
|
1230
|
+
requestedScopeKeys.size > 0 && scopeSupport ? 'scope' : null,
|
|
1231
|
+
graphSource === 'primary' ? 'graph_primary' : null,
|
|
1232
|
+
graphSource === 'expanded' ? 'graph_expanded' : null,
|
|
1233
|
+
].filter((signal) => signal !== null);
|
|
1234
|
+
const retrievalSourceForRecord = vectorSimilarity !== null && lexicalSupport
|
|
1235
|
+
? 'hybrid_rrf'
|
|
1236
|
+
: vectorSimilarity !== null
|
|
1237
|
+
? 'vector_search'
|
|
1238
|
+
: lexicalSupport
|
|
1239
|
+
? 'lexical_search'
|
|
1240
|
+
: entitySupport
|
|
1241
|
+
? 'entity_canonical'
|
|
1242
|
+
: String(record.source.source_type || 'unknown');
|
|
1243
|
+
return {
|
|
1244
|
+
retrieval_source: retrievalSourceForRecord,
|
|
1245
|
+
vector_similarity: vectorSimilarity,
|
|
1246
|
+
lexical_support: lexicalSupport,
|
|
1247
|
+
entity_support: entitySupport,
|
|
1248
|
+
scope_support: scopeSupport,
|
|
1249
|
+
graph_source: graphSource,
|
|
1250
|
+
is_vector_only: vectorSimilarity !== null && confirmationSignals.length === 0,
|
|
1251
|
+
confirmation_signals: confirmationSignals,
|
|
1252
|
+
metadata_signals: metadataSignals,
|
|
1253
|
+
candidate_threshold_used: searchOptions.threshold,
|
|
1254
|
+
};
|
|
1255
|
+
};
|
|
1256
|
+
const buildWikiDiagnostics = (entry) => {
|
|
1257
|
+
const lexicalSupport = entry.lexicalSupport;
|
|
1258
|
+
const exactTopicSupport = hasExactWikiSupport(entry.record);
|
|
1259
|
+
const scopeSupport = hasRequestedWikiScopeSupport(entry.record);
|
|
1260
|
+
const confirmationSignals = [
|
|
1261
|
+
lexicalSupport ? 'lexical' : null,
|
|
1262
|
+
exactTopicSupport ? 'exact_topic' : null,
|
|
1263
|
+
].filter((signal) => signal !== null);
|
|
1264
|
+
// Only attribute 'scope' when the caller actually requested a scope filter.
|
|
1265
|
+
// hasRequestedWikiScopeSupport() returns true by default for unscoped searches,
|
|
1266
|
+
// so without this guard the diagnostics would overstate why the wiki hit passed.
|
|
1267
|
+
const metadataSignals = [
|
|
1268
|
+
requestedScopeKeys.size > 0 && scopeSupport ? 'scope' : null,
|
|
1269
|
+
'graph_primary',
|
|
1270
|
+
].filter((signal) => signal !== null);
|
|
1271
|
+
const retrievalSourceForRecord = entry.vectorSimilarity !== null && lexicalSupport
|
|
1272
|
+
? 'wiki_hybrid_rrf'
|
|
1273
|
+
: entry.vectorSimilarity !== null
|
|
1274
|
+
? 'wiki_vector_search'
|
|
1275
|
+
: lexicalSupport
|
|
1276
|
+
? 'wiki_lexical_search'
|
|
1277
|
+
: 'wiki_page';
|
|
1278
|
+
return {
|
|
1279
|
+
retrieval_source: retrievalSourceForRecord,
|
|
1280
|
+
vector_similarity: entry.vectorSimilarity,
|
|
1281
|
+
lexical_support: lexicalSupport,
|
|
1282
|
+
entity_support: false,
|
|
1283
|
+
scope_support: scopeSupport,
|
|
1284
|
+
graph_source: 'primary',
|
|
1285
|
+
is_vector_only: entry.vectorSimilarity !== null && confirmationSignals.length === 0,
|
|
1286
|
+
confirmation_signals: confirmationSignals,
|
|
1287
|
+
metadata_signals: metadataSignals,
|
|
1288
|
+
candidate_threshold_used: searchOptions.threshold,
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
const passesStrictness = (hitDiagnostics) => {
|
|
1292
|
+
if (searchOptions.strictness === 'recall') {
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
if (hitDiagnostics.confirmation_signals.length === 0) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
if (!searchOptions.minLexicalSupport) {
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
return (hitDiagnostics.lexical_support ||
|
|
1302
|
+
hitDiagnostics.entity_support ||
|
|
1303
|
+
hitDiagnostics.confirmation_signals.includes('exact_topic'));
|
|
1304
|
+
};
|
|
1305
|
+
const acceptedPrimaryIds = new Set();
|
|
1306
|
+
matched = matched.flatMap((record) => {
|
|
1307
|
+
const recordDiagnostics = buildDiagnostics(record, 'primary');
|
|
1308
|
+
if (recordDiagnostics.is_vector_only) {
|
|
1309
|
+
diagnostics.candidate_counts.vector_only += 1;
|
|
1310
|
+
}
|
|
1311
|
+
if (!passesStrictness(recordDiagnostics)) {
|
|
1312
|
+
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1313
|
+
return [];
|
|
1314
|
+
}
|
|
1315
|
+
acceptedPrimaryIds.add(record.id);
|
|
1316
|
+
return [
|
|
1317
|
+
searchOptions.diagnostics ? { ...record, retrieval_diagnostics: recordDiagnostics } : record,
|
|
1318
|
+
];
|
|
1319
|
+
});
|
|
1320
|
+
fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || acceptedPrimaryIds.has(hit.source_id));
|
|
1321
|
+
const acceptedWikiFusedHits = Array.from(wikiScores.values())
|
|
1322
|
+
.sort((a, b) => b.score - a.score)
|
|
1323
|
+
.flatMap((entry) => {
|
|
1324
|
+
if (!hasWikiDecisionWithScopeAndTopic(entry.record)) {
|
|
1325
|
+
return [];
|
|
1326
|
+
}
|
|
1327
|
+
const wikiDiagnostics = buildWikiDiagnostics(entry);
|
|
1328
|
+
if (wikiDiagnostics.is_vector_only) {
|
|
1329
|
+
diagnostics.candidate_counts.vector_only += 1;
|
|
1330
|
+
}
|
|
1331
|
+
if (!passesStrictness(wikiDiagnostics)) {
|
|
1332
|
+
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1333
|
+
return [];
|
|
1334
|
+
}
|
|
1335
|
+
return [
|
|
1336
|
+
{
|
|
1337
|
+
source_type: 'wiki_page',
|
|
1338
|
+
source_id: entry.record.source_locator,
|
|
1339
|
+
record: searchOptions.diagnostics
|
|
1340
|
+
? { ...entry.record, retrieval_diagnostics: wikiDiagnostics }
|
|
1341
|
+
: entry.record,
|
|
1342
|
+
fused_rank_score: entry.score,
|
|
1343
|
+
...(searchOptions.diagnostics ? { retrieval_diagnostics: wikiDiagnostics } : {}),
|
|
1344
|
+
...(entry.record.page_type === 'case' ? { page_type: 'case' } : {}),
|
|
1345
|
+
...(entry.record.case_id ? { case_id: entry.record.case_id } : {}),
|
|
1346
|
+
},
|
|
1347
|
+
];
|
|
1348
|
+
});
|
|
1349
|
+
hasWikiHits = acceptedWikiFusedHits.length > 0;
|
|
1350
|
+
fusedHits = [...fusedHits, ...acceptedWikiFusedHits].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1351
|
+
if (hasWikiHits) {
|
|
1352
|
+
retrievalSource = retrievalSource === 'none' ? 'wiki_page' : `${retrievalSource}+wiki_page`;
|
|
1353
|
+
}
|
|
959
1354
|
if (matched.length > 0) {
|
|
960
1355
|
const readIdentityIndex = await (0, read_identity_js_1.loadDecisionReadIdentityIndex)(matched.filter((record) => !record.read_identity).map((record) => record.id));
|
|
961
1356
|
for (const record of matched) {
|
|
@@ -991,7 +1386,7 @@ async function recallMemory(query, options = {}) {
|
|
|
991
1386
|
bundle.graph_context.primary = matched;
|
|
992
1387
|
bundle.graph_context.expanded = [];
|
|
993
1388
|
bundle.graph_context.edges = [];
|
|
994
|
-
if (matched.length > 0 && !options.skipGraphExpansion) {
|
|
1389
|
+
if (matched.length > 0 && !options.skipGraphExpansion && searchOptions.includeRelated) {
|
|
995
1390
|
try {
|
|
996
1391
|
const candidates = matched.map((m) => ({
|
|
997
1392
|
id: m.id,
|
|
@@ -1006,6 +1401,7 @@ async function recallMemory(query, options = {}) {
|
|
|
1006
1401
|
const expanded = await mamaDefault.expandWithGraph(candidates);
|
|
1007
1402
|
const primaryIds = new Set(matched.map((m) => m.id));
|
|
1008
1403
|
let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
|
|
1404
|
+
let expandedScopeMap = new Map();
|
|
1009
1405
|
// Re-filter expanded results: apply status and scope checks
|
|
1010
1406
|
if (!options.includeHistory) {
|
|
1011
1407
|
expandedOnly = expandedOnly.filter((e) => {
|
|
@@ -1017,32 +1413,63 @@ async function recallMemory(query, options = {}) {
|
|
|
1017
1413
|
}
|
|
1018
1414
|
if (options.scopes && options.scopes.length > 0) {
|
|
1019
1415
|
const expandedIds = expandedOnly.map((e) => e.id);
|
|
1020
|
-
|
|
1416
|
+
expandedScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), expandedIds);
|
|
1021
1417
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1022
1418
|
expandedOnly = expandedOnly.filter((e) => {
|
|
1023
|
-
const scopes =
|
|
1419
|
+
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
1024
1420
|
if (scopes.length === 0)
|
|
1025
1421
|
return false;
|
|
1026
1422
|
return scopes.some((s) => requestedScopes.has(`${s.kind}:${s.id}`));
|
|
1027
1423
|
});
|
|
1028
1424
|
}
|
|
1029
|
-
bundle.graph_context.expanded = expandedOnly.
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1425
|
+
bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
|
|
1426
|
+
const expandedRecord = {
|
|
1427
|
+
id: String(e.id),
|
|
1428
|
+
topic: String(e.topic || ''),
|
|
1429
|
+
kind: 'decision',
|
|
1430
|
+
summary: String(e.decision || ''),
|
|
1431
|
+
details: '',
|
|
1432
|
+
confidence: e.graph_rank ?? 0.5,
|
|
1433
|
+
status: 'active',
|
|
1434
|
+
scopes: expandedScopeMap.get(e.id) ?? [],
|
|
1435
|
+
source: {
|
|
1436
|
+
package: 'mama-core',
|
|
1437
|
+
source_type: String(e.graph_source || 'graph_expansion'),
|
|
1438
|
+
},
|
|
1439
|
+
created_at: e.created_at ?? Date.now(),
|
|
1440
|
+
updated_at: e.created_at ?? Date.now(),
|
|
1441
|
+
};
|
|
1442
|
+
const expandedDiagnostics = buildDiagnostics(expandedRecord, 'expanded');
|
|
1443
|
+
if (!passesStrictness(expandedDiagnostics)) {
|
|
1444
|
+
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1445
|
+
return [];
|
|
1446
|
+
}
|
|
1447
|
+
return [
|
|
1448
|
+
searchOptions.diagnostics
|
|
1449
|
+
? {
|
|
1450
|
+
...expandedRecord,
|
|
1451
|
+
retrieval_diagnostics: expandedDiagnostics,
|
|
1452
|
+
}
|
|
1453
|
+
: expandedRecord,
|
|
1454
|
+
];
|
|
1455
|
+
});
|
|
1456
|
+
diagnostics.candidate_counts.graph_expanded = bundle.graph_context.expanded.length;
|
|
1457
|
+
fusedHits = [
|
|
1458
|
+
...fusedHits,
|
|
1459
|
+
...bundle.graph_context.expanded.map((record) => ({
|
|
1460
|
+
source_type: 'decision',
|
|
1461
|
+
source_id: record.id,
|
|
1462
|
+
record,
|
|
1463
|
+
fused_rank_score: (record.confidence ?? 0.5) * 0.1,
|
|
1464
|
+
retrieval_diagnostics: record.retrieval_diagnostics,
|
|
1465
|
+
})),
|
|
1466
|
+
].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1467
|
+
// bundle.graph_context.expanded is the strictness-filtered set of
|
|
1468
|
+
// expanded nodes that will actually be returned. Use those IDs (not
|
|
1469
|
+
// expandedOnly, which still contains rejected candidates) so edges
|
|
1470
|
+
// never point at nodes that never made it into the graph payload.
|
|
1471
|
+
const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
|
|
1472
|
+
const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
|
|
1046
1473
|
const allEdges = await loadEdgesForIds(allIds);
|
|
1047
1474
|
// Filter out edges pointing to decisions with excluded statuses
|
|
1048
1475
|
const activeIds = new Set(allIds);
|
|
@@ -1070,14 +1497,17 @@ async function recallMemory(query, options = {}) {
|
|
|
1070
1497
|
bundle.fused_hits = fusedHits;
|
|
1071
1498
|
bundle.search_meta.scope_order = (options.scopes ?? []).map((scope) => scope.kind);
|
|
1072
1499
|
bundle.search_meta.retrieval_sources = [retrievalSource];
|
|
1500
|
+
if (searchOptions.diagnostics) {
|
|
1501
|
+
bundle.search_meta.diagnostics = diagnostics;
|
|
1502
|
+
}
|
|
1073
1503
|
if (options.includeProfile) {
|
|
1074
1504
|
bundle.profile = await buildProfile(options.scopes ?? []);
|
|
1075
1505
|
}
|
|
1076
1506
|
return bundle;
|
|
1077
1507
|
}
|
|
1078
|
-
async function
|
|
1508
|
+
async function ingestMemoryInternal(input, options) {
|
|
1079
1509
|
const normalized = input.content.trim();
|
|
1080
|
-
return
|
|
1510
|
+
return saveMemoryInternal({
|
|
1081
1511
|
topic: normalized
|
|
1082
1512
|
.slice(0, 40)
|
|
1083
1513
|
.toLowerCase()
|
|
@@ -1090,7 +1520,13 @@ async function ingestMemory(input) {
|
|
|
1090
1520
|
source: input.source,
|
|
1091
1521
|
eventDate: input.eventDate,
|
|
1092
1522
|
eventDateTime: input.eventDateTime,
|
|
1093
|
-
});
|
|
1523
|
+
}, options);
|
|
1524
|
+
}
|
|
1525
|
+
async function ingestMemory(input) {
|
|
1526
|
+
return ingestMemoryInternal((0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
|
|
1527
|
+
}
|
|
1528
|
+
async function ingestWithTrustedProvenance(input, options) {
|
|
1529
|
+
return ingestMemoryInternal((0, provenance_js_1.sanitizePublicIngestMemoryInput)(input), options);
|
|
1094
1530
|
}
|
|
1095
1531
|
async function evolveMemory(input) {
|
|
1096
1532
|
return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
|
|
@@ -1142,18 +1578,18 @@ let extractionFn = callExtractionLLM;
|
|
|
1142
1578
|
function setExtractionFn(fn) {
|
|
1143
1579
|
extractionFn = fn ?? callExtractionLLM;
|
|
1144
1580
|
}
|
|
1145
|
-
async function
|
|
1581
|
+
async function ingestConversationInternal(input, options) {
|
|
1146
1582
|
if (!input.messages || input.messages.length === 0) {
|
|
1147
1583
|
throw new Error('messages array must not be empty');
|
|
1148
1584
|
}
|
|
1149
1585
|
const conversationText = input.messages.map((m) => `${m.role}: ${m.content}`).join('\n');
|
|
1150
1586
|
const topicPrefix = input.topicPrefix || '';
|
|
1151
|
-
const rawResult = await
|
|
1587
|
+
const rawResult = await ingestMemoryInternal({
|
|
1152
1588
|
content: topicPrefix ? `${topicPrefix}${conversationText}` : conversationText,
|
|
1153
1589
|
scopes: input.scopes,
|
|
1154
1590
|
source: input.source,
|
|
1155
1591
|
eventDate: input.sessionDate,
|
|
1156
|
-
});
|
|
1592
|
+
}, options);
|
|
1157
1593
|
const result = {
|
|
1158
1594
|
rawId: rawResult.id,
|
|
1159
1595
|
extractedMemories: [],
|
|
@@ -1202,7 +1638,7 @@ async function ingestConversation(input) {
|
|
|
1202
1638
|
const batchSavedIds = [];
|
|
1203
1639
|
for (const unit of units) {
|
|
1204
1640
|
try {
|
|
1205
|
-
const saved = await
|
|
1641
|
+
const saved = await saveMemoryInternal({
|
|
1206
1642
|
topic: topicPrefix ? `${topicPrefix}${unit.topic}` : unit.topic,
|
|
1207
1643
|
kind: unit.kind,
|
|
1208
1644
|
summary: unit.summary,
|
|
@@ -1212,7 +1648,7 @@ async function ingestConversation(input) {
|
|
|
1212
1648
|
source: input.source,
|
|
1213
1649
|
excludeIds: batchSavedIds,
|
|
1214
1650
|
eventDate: input.sessionDate,
|
|
1215
|
-
});
|
|
1651
|
+
}, (0, provenance_js_1.appendProvenanceSourceRefs)(options, [`raw_memory:${rawResult.id}`]));
|
|
1216
1652
|
const now = Date.now();
|
|
1217
1653
|
adapter
|
|
1218
1654
|
.prepare(`INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
|
|
@@ -1231,4 +1667,10 @@ async function ingestConversation(input) {
|
|
|
1231
1667
|
}
|
|
1232
1668
|
return result;
|
|
1233
1669
|
}
|
|
1670
|
+
async function ingestConversation(input) {
|
|
1671
|
+
return ingestConversationInternal((0, provenance_js_1.sanitizePublicIngestConversationInput)(input));
|
|
1672
|
+
}
|
|
1673
|
+
async function ingestConversationWithTrustedProvenance(input, options) {
|
|
1674
|
+
return ingestConversationInternal((0, provenance_js_1.sanitizePublicIngestConversationInput)(input), options);
|
|
1675
|
+
}
|
|
1234
1676
|
//# sourceMappingURL=api.js.map
|