@jungjaehoon/mama-core 1.4.2 → 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/026-create-canonical-entity-tables.sql +99 -0
- package/db/migrations/027-create-entity-review-support.sql +44 -0
- package/db/migrations/028-create-entity-audit-tables.sql +45 -0
- package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
- package/db/migrations/030-case-first-memory-substrate.sql +901 -0
- package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
- 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/canonicalize.d.ts +38 -0
- package/dist/canonicalize.js +106 -0
- package/dist/cases/case-links.d.ts +116 -0
- package/dist/cases/case-links.js +794 -0
- package/dist/cases/composition-overrides.d.ts +90 -0
- package/dist/cases/composition-overrides.js +724 -0
- package/dist/cases/corrections.d.ts +156 -0
- package/dist/cases/corrections.js +1045 -0
- package/dist/cases/freshness.d.ts +57 -0
- package/dist/cases/freshness.js +283 -0
- package/dist/cases/live-state.d.ts +48 -0
- package/dist/cases/live-state.js +256 -0
- package/dist/cases/membership-explain.d.ts +56 -0
- package/dist/cases/membership-explain.js +140 -0
- package/dist/cases/membership-matcher.d.ts +23 -0
- package/dist/cases/membership-matcher.js +193 -0
- package/dist/cases/merge-split.d.ts +61 -0
- package/dist/cases/merge-split.js +368 -0
- package/dist/cases/role-inference.d.ts +10 -0
- package/dist/cases/role-inference.js +85 -0
- package/dist/cases/search-rollup.d.ts +27 -0
- package/dist/cases/search-rollup.js +309 -0
- package/dist/cases/sqlite-transaction.d.ts +10 -0
- package/dist/cases/sqlite-transaction.js +62 -0
- package/dist/cases/store.d.ts +74 -0
- package/dist/cases/store.js +816 -0
- package/dist/cases/target-ref.d.ts +25 -0
- package/dist/cases/target-ref.js +22 -0
- package/dist/cases/timeline-range.d.ts +6 -0
- package/dist/cases/timeline-range.js +322 -0
- package/dist/cases/tombstone-sweeper.d.ts +13 -0
- package/dist/cases/tombstone-sweeper.js +128 -0
- package/dist/cases/types.d.ts +244 -0
- package/dist/cases/types.js +47 -0
- package/dist/cases/wiki-page-index.d.ts +50 -0
- package/dist/cases/wiki-page-index.js +350 -0
- package/dist/connectors/event-index.d.ts +45 -0
- package/dist/connectors/event-index.js +381 -0
- package/dist/connectors/raw-query.d.ts +18 -0
- package/dist/connectors/raw-query.js +305 -0
- package/dist/connectors/types.d.ts +109 -0
- package/dist/connectors/types.js +3 -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 +12 -1
- package/dist/db-manager.js +60 -5
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/decision-formatter.js +11 -8
- 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/embedding-server/mobile/daemon.d.ts +1 -0
- package/dist/embedding-server/mobile/daemon.js +11 -1
- package/dist/embeddings.js +13 -0
- package/dist/entities/audit-metrics.d.ts +73 -0
- package/dist/entities/audit-metrics.js +128 -0
- package/dist/entities/candidate-generator.d.ts +9 -0
- package/dist/entities/candidate-generator.js +436 -0
- package/dist/entities/entity-impact.d.ts +41 -0
- package/dist/entities/entity-impact.js +140 -0
- package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
- package/dist/entities/entity-linked-decision-counts.js +29 -0
- package/dist/entities/entity-list.d.ts +23 -0
- package/dist/entities/entity-list.js +99 -0
- package/dist/entities/entity-orphan-list.d.ts +26 -0
- package/dist/entities/entity-orphan-list.js +91 -0
- package/dist/entities/entity-search.d.ts +21 -0
- package/dist/entities/entity-search.js +150 -0
- package/dist/entities/errors.d.ts +65 -0
- package/dist/entities/errors.js +124 -0
- package/dist/entities/exact-merge-backfill.d.ts +11 -0
- package/dist/entities/exact-merge-backfill.js +161 -0
- package/dist/entities/lineage-backfill.d.ts +12 -0
- package/dist/entities/lineage-backfill.js +101 -0
- package/dist/entities/lineage-store.d.ts +51 -0
- package/dist/entities/lineage-store.js +308 -0
- package/dist/entities/normalization.d.ts +14 -0
- package/dist/entities/normalization.js +63 -0
- package/dist/entities/policy-store.d.ts +17 -0
- package/dist/entities/policy-store.js +263 -0
- package/dist/entities/policy-types.d.ts +72 -0
- package/dist/entities/policy-types.js +59 -0
- package/dist/entities/projection.d.ts +8 -0
- package/dist/entities/projection.js +82 -0
- package/dist/entities/provenance-query.d.ts +33 -0
- package/dist/entities/provenance-query.js +167 -0
- package/dist/entities/read-identity.d.ts +31 -0
- package/dist/entities/read-identity.js +264 -0
- package/dist/entities/recall-bridge.d.ts +5 -0
- package/dist/entities/recall-bridge.js +171 -0
- package/dist/entities/resolution-engine.d.ts +15 -0
- package/dist/entities/resolution-engine.js +69 -0
- package/dist/entities/rollback-preview.d.ts +39 -0
- package/dist/entities/rollback-preview.js +235 -0
- package/dist/entities/source-locator.d.ts +3 -0
- package/dist/entities/source-locator.js +32 -0
- package/dist/entities/store.d.ts +75 -0
- package/dist/entities/store.js +465 -0
- package/dist/entities/types.d.ts +162 -0
- package/dist/entities/types.js +30 -0
- package/dist/index.d.ts +62 -3
- package/dist/index.js +116 -26
- package/dist/mama-api.d.ts +53 -35
- package/dist/mama-api.js +437 -78
- package/dist/memory/api.d.ts +26 -38
- package/dist/memory/api.js +772 -68
- 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 +97 -16
- 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/feedback-store.d.ts +59 -0
- package/dist/search/feedback-store.js +211 -0
- package/dist/search/question-type.d.ts +5 -0
- package/dist/search/question-type.js +41 -0
- package/dist/search/ranker-features.d.ts +23 -0
- package/dist/search/ranker-features.js +104 -0
- package/dist/search/ranker-rescore.d.ts +31 -0
- package/dist/search/ranker-rescore.js +226 -0
- package/dist/search/ranker-trainer.d.ts +94 -0
- package/dist/search/ranker-trainer.js +860 -0
- package/dist/search/search-quality.d.ts +36 -0
- package/dist/search/search-quality.js +29 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +52 -0
- package/package.json +28 -3
package/dist/memory/api.js
CHANGED
|
@@ -7,18 +7,22 @@ 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");
|
|
25
|
+
const wiki_page_index_js_1 = require("../cases/wiki-page-index.js");
|
|
22
26
|
const profile_builder_js_1 = require("./profile-builder.js");
|
|
23
27
|
const bootstrap_builder_js_1 = require("./bootstrap-builder.js");
|
|
24
28
|
const evolution_engine_js_1 = require("./evolution-engine.js");
|
|
@@ -30,10 +34,25 @@ const types_js_1 = require("./types.js");
|
|
|
30
34
|
const channel_summary_store_js_1 = require("./channel-summary-store.js");
|
|
31
35
|
Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.getChannelSummary; } });
|
|
32
36
|
Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.upsertChannelSummary; } });
|
|
37
|
+
const recall_bridge_js_1 = require("../entities/recall-bridge.js");
|
|
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");
|
|
33
42
|
function buildDecisionId(topic) {
|
|
34
43
|
const safeTopic = topic.replace(/[^a-z0-9_]+/gi, '_').toLowerCase();
|
|
35
44
|
return `decision_${safeTopic}_${Date.now()}_${node_crypto_1.default.randomUUID().slice(0, 8)}`;
|
|
36
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
|
+
}
|
|
37
56
|
function toMemoryRecord(row, scopes, fallbackSource) {
|
|
38
57
|
let trustContext = null;
|
|
39
58
|
if (typeof row.trust_context === 'string') {
|
|
@@ -58,8 +77,128 @@ function toMemoryRecord(row, scopes, fallbackSource) {
|
|
|
58
77
|
created_at: row.created_at,
|
|
59
78
|
updated_at: row.updated_at ?? row.created_at,
|
|
60
79
|
event_date: row.event_date ?? null,
|
|
80
|
+
event_datetime: typeof row.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
81
|
+
? row.event_datetime
|
|
82
|
+
: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function loadEventDateTimeForObservations(adapter, observationIds) {
|
|
86
|
+
const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
|
|
87
|
+
if (uniqueIds.length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const placeholders = uniqueIds.map(() => '?').join(', ');
|
|
91
|
+
const row = adapter
|
|
92
|
+
.prepare(`
|
|
93
|
+
SELECT MAX(COALESCE(timestamp_observed, created_at)) AS event_datetime
|
|
94
|
+
FROM entity_observations
|
|
95
|
+
WHERE id IN (${placeholders})
|
|
96
|
+
`)
|
|
97
|
+
.get(...uniqueIds);
|
|
98
|
+
return typeof row?.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
99
|
+
? row.event_datetime
|
|
100
|
+
: null;
|
|
101
|
+
}
|
|
102
|
+
function getTimelineEntityKindPriority(kind) {
|
|
103
|
+
switch (kind) {
|
|
104
|
+
case 'work_item':
|
|
105
|
+
return 0;
|
|
106
|
+
case 'project':
|
|
107
|
+
return 1;
|
|
108
|
+
case 'organization':
|
|
109
|
+
return 2;
|
|
110
|
+
case 'person':
|
|
111
|
+
return 3;
|
|
112
|
+
default:
|
|
113
|
+
return 99;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function resolveTimelineTargetEntityIdFromObservations(adapter, observationIds) {
|
|
117
|
+
const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
|
|
118
|
+
if (uniqueIds.length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const placeholders = uniqueIds.map(() => '?').join(', ');
|
|
122
|
+
const rows = adapter
|
|
123
|
+
.prepare(`
|
|
124
|
+
SELECT
|
|
125
|
+
n.id AS entity_id,
|
|
126
|
+
n.kind AS entity_kind,
|
|
127
|
+
COUNT(*) AS matched_observations
|
|
128
|
+
FROM entity_lineage_links l
|
|
129
|
+
JOIN entity_nodes n
|
|
130
|
+
ON n.id = l.canonical_entity_id
|
|
131
|
+
WHERE l.entity_observation_id IN (${placeholders})
|
|
132
|
+
AND l.status = 'active'
|
|
133
|
+
AND n.status = 'active'
|
|
134
|
+
AND n.merged_into IS NULL
|
|
135
|
+
GROUP BY n.id, n.kind
|
|
136
|
+
`)
|
|
137
|
+
.all(...uniqueIds);
|
|
138
|
+
rows.sort((left, right) => {
|
|
139
|
+
const kindDelta = getTimelineEntityKindPriority(left.entity_kind) -
|
|
140
|
+
getTimelineEntityKindPriority(right.entity_kind);
|
|
141
|
+
if (kindDelta !== 0) {
|
|
142
|
+
return kindDelta;
|
|
143
|
+
}
|
|
144
|
+
if (right.matched_observations !== left.matched_observations) {
|
|
145
|
+
return right.matched_observations - left.matched_observations;
|
|
146
|
+
}
|
|
147
|
+
return left.entity_id.localeCompare(right.entity_id);
|
|
148
|
+
});
|
|
149
|
+
return rows[0]?.entity_id ?? null;
|
|
150
|
+
}
|
|
151
|
+
function buildTimelineEventForSave(adapter, memoryId, topic, entityObservationIds, timelineEvent) {
|
|
152
|
+
if (!timelineEvent) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const resolvedEntityId = timelineEvent.entity_id ??
|
|
156
|
+
resolveTimelineTargetEntityIdFromObservations(adapter, entityObservationIds);
|
|
157
|
+
if (!resolvedEntityId) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
id: timelineEvent.id ?? `et_${node_crypto_1.default.randomUUID()}`,
|
|
162
|
+
entity_id: resolvedEntityId,
|
|
163
|
+
event_type: timelineEvent.event_type,
|
|
164
|
+
role: timelineEvent.role ?? null,
|
|
165
|
+
valid_from: timelineEvent.valid_from ?? null,
|
|
166
|
+
valid_to: timelineEvent.valid_to ?? null,
|
|
167
|
+
observed_at: timelineEvent.observed_at ?? null,
|
|
168
|
+
source_ref: timelineEvent.source_ref ?? `decision:${memoryId}`,
|
|
169
|
+
summary: timelineEvent.summary,
|
|
170
|
+
details: timelineEvent.details ??
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
memory_id: memoryId,
|
|
173
|
+
topic,
|
|
174
|
+
}),
|
|
61
175
|
};
|
|
62
176
|
}
|
|
177
|
+
function insertTimelineEventForSave(adapter, event, createdAt) {
|
|
178
|
+
adapter
|
|
179
|
+
.prepare(`
|
|
180
|
+
INSERT INTO entity_timeline_events (
|
|
181
|
+
id, entity_id, event_type, role, valid_from, valid_to, observed_at,
|
|
182
|
+
source_ref, summary, details, created_at
|
|
183
|
+
)
|
|
184
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
185
|
+
`)
|
|
186
|
+
.run(event.id, event.entity_id, event.event_type, event.role, event.valid_from, event.valid_to, event.observed_at, event.source_ref, event.summary, event.details, createdAt);
|
|
187
|
+
}
|
|
188
|
+
function cleanupFailedMemorySave(adapter, memoryId) {
|
|
189
|
+
const row = adapter.prepare(`SELECT rowid FROM decisions WHERE id = ?`).get(memoryId);
|
|
190
|
+
if (!row) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const cleanup = () => {
|
|
194
|
+
adapter.prepare(`DELETE FROM embeddings WHERE rowid = ?`).run(row.rowid);
|
|
195
|
+
adapter.prepare(`DELETE FROM decisions WHERE id = ?`).run(memoryId);
|
|
196
|
+
};
|
|
197
|
+
const txResult = adapter.transaction(cleanup);
|
|
198
|
+
if (typeof txResult === 'function') {
|
|
199
|
+
txResult();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
63
202
|
function batchLoadScopes(adapter, memoryIds) {
|
|
64
203
|
const scopeMap = new Map();
|
|
65
204
|
if (memoryIds.length === 0)
|
|
@@ -90,9 +229,9 @@ async function loadScopedMemories(scopes) {
|
|
|
90
229
|
rows = adapter
|
|
91
230
|
.prepare(`
|
|
92
231
|
SELECT id, topic, decision, reasoning, confidence, created_at, updated_at, trust_context,
|
|
93
|
-
kind, status, summary, event_date
|
|
232
|
+
kind, status, summary, event_date, event_datetime
|
|
94
233
|
FROM decisions
|
|
95
|
-
ORDER BY created_at DESC
|
|
234
|
+
ORDER BY COALESCE(event_datetime, created_at) DESC, created_at DESC
|
|
96
235
|
`)
|
|
97
236
|
.all();
|
|
98
237
|
}
|
|
@@ -102,11 +241,11 @@ async function loadScopedMemories(scopes) {
|
|
|
102
241
|
rows = adapter
|
|
103
242
|
.prepare(`
|
|
104
243
|
SELECT DISTINCT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at,
|
|
105
|
-
d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date
|
|
244
|
+
d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime
|
|
106
245
|
FROM decisions d
|
|
107
246
|
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
108
247
|
WHERE msb.scope_id IN (${placeholders})
|
|
109
|
-
ORDER BY d.created_at DESC
|
|
248
|
+
ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
|
|
110
249
|
`)
|
|
111
250
|
.all(...scopeIds);
|
|
112
251
|
}
|
|
@@ -181,6 +320,19 @@ function getLexicalQueryTokens(query) {
|
|
|
181
320
|
.split(/[\s,.!?;:()[\]{}"']+/)
|
|
182
321
|
.filter((token) => token.length > 2 && !exports.LEXICAL_STOPWORDS.has(token));
|
|
183
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
|
+
}
|
|
184
336
|
function buildLexicalCandidates(records, query) {
|
|
185
337
|
const tokens = getLexicalQueryTokens(query);
|
|
186
338
|
const normalizedQuery = query.toLowerCase();
|
|
@@ -284,11 +436,12 @@ async function loadEdgesForIds(ids) {
|
|
|
284
436
|
reason: row.reason ?? undefined,
|
|
285
437
|
}));
|
|
286
438
|
}
|
|
287
|
-
async function
|
|
439
|
+
async function saveMemoryInternal(input, options) {
|
|
288
440
|
await (0, db_manager_js_1.initDB)();
|
|
289
441
|
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
290
442
|
const id = buildDecisionId(input.topic);
|
|
291
443
|
const now = Date.now();
|
|
444
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)(options);
|
|
292
445
|
// Find evolution candidates: exact topic match first, then semantic search fallback
|
|
293
446
|
const primaryScope = input.scopes.length > 0 ? input.scopes[0] : null;
|
|
294
447
|
const excludeIds = new Set(input.excludeIds ?? []);
|
|
@@ -364,6 +517,11 @@ async function saveMemory(input) {
|
|
|
364
517
|
})),
|
|
365
518
|
});
|
|
366
519
|
const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ?? null;
|
|
520
|
+
const entityObservationIds = Array.from(new Set(input.entityObservationIds ?? []));
|
|
521
|
+
const eventDateTime = typeof input.eventDateTime === 'number'
|
|
522
|
+
? input.eventDateTime
|
|
523
|
+
: loadEventDateTimeForObservations(adapter, entityObservationIds);
|
|
524
|
+
const timelineEvent = buildTimelineEventForSave(adapter, id, input.topic, entityObservationIds, input.timelineEvent);
|
|
367
525
|
await (0, db_manager_js_1.insertDecisionWithEmbedding)({
|
|
368
526
|
id,
|
|
369
527
|
topic: input.topic,
|
|
@@ -375,6 +533,13 @@ async function saveMemory(input) {
|
|
|
375
533
|
updated_at: now,
|
|
376
534
|
trust_context: JSON.stringify({ source: input.source }),
|
|
377
535
|
event_date: input.eventDate ?? null,
|
|
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),
|
|
378
543
|
});
|
|
379
544
|
// Pre-resolve scope IDs before the synchronous transaction
|
|
380
545
|
const resolvedScopeIds = [];
|
|
@@ -383,36 +548,67 @@ async function saveMemory(input) {
|
|
|
383
548
|
resolvedScopeIds.push({ scopeId, isPrimary: index === 0 });
|
|
384
549
|
}
|
|
385
550
|
// Wrap all post-insert mutations in a transaction for atomicity
|
|
386
|
-
|
|
387
|
-
adapter
|
|
388
|
-
.prepare(`
|
|
389
|
-
UPDATE decisions
|
|
390
|
-
SET kind = ?, status = ?, summary = ?, is_static = ?, trust_context = ?, updated_at = ?
|
|
391
|
-
WHERE id = ?
|
|
392
|
-
`)
|
|
393
|
-
.run(input.kind, input.status ?? 'active', input.summary, input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0, JSON.stringify({ source: input.source }), now, id);
|
|
394
|
-
for (const { scopeId, isPrimary } of resolvedScopeIds) {
|
|
551
|
+
try {
|
|
552
|
+
adapter.transaction(() => {
|
|
395
553
|
adapter
|
|
396
554
|
.prepare(`
|
|
397
|
-
|
|
398
|
-
|
|
555
|
+
UPDATE decisions
|
|
556
|
+
SET kind = ?, status = ?, summary = ?, is_static = ?, trust_context = ?, updated_at = ?
|
|
557
|
+
WHERE id = ?
|
|
399
558
|
`)
|
|
400
|
-
.run(
|
|
401
|
-
|
|
402
|
-
for (const edge of evolution.edges) {
|
|
403
|
-
if (edge.type === 'supersedes') {
|
|
559
|
+
.run(input.kind, input.status ?? 'active', input.summary, input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0, JSON.stringify({ source: input.source }), now, id);
|
|
560
|
+
for (const { scopeId, isPrimary } of resolvedScopeIds) {
|
|
404
561
|
adapter
|
|
405
|
-
.prepare(`
|
|
406
|
-
|
|
562
|
+
.prepare(`
|
|
563
|
+
INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
|
|
564
|
+
VALUES (?, ?, ?)
|
|
565
|
+
`)
|
|
566
|
+
.run(id, scopeId, isPrimary ? 1 : 0);
|
|
407
567
|
}
|
|
408
|
-
adapter
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
});
|
|
579
|
+
for (const entityObservationId of entityObservationIds) {
|
|
580
|
+
adapter
|
|
581
|
+
.prepare(`
|
|
582
|
+
INSERT OR IGNORE INTO decision_entity_sources (
|
|
583
|
+
decision_id, entity_observation_id, relation_type, created_at
|
|
584
|
+
) VALUES (?, ?, ?, ?)
|
|
585
|
+
`)
|
|
586
|
+
.run(id, entityObservationId, 'support', now);
|
|
587
|
+
}
|
|
588
|
+
if (timelineEvent) {
|
|
589
|
+
insertTimelineEventForSave(adapter, timelineEvent, now);
|
|
590
|
+
}
|
|
591
|
+
for (const edge of evolution.edges) {
|
|
592
|
+
if (edge.type === 'supersedes') {
|
|
593
|
+
adapter
|
|
594
|
+
.prepare(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
|
|
595
|
+
.run(id, now, edge.to_id);
|
|
596
|
+
}
|
|
597
|
+
adapter
|
|
598
|
+
.prepare(`
|
|
599
|
+
INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
|
|
600
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
601
|
+
`)
|
|
602
|
+
.run(id, edge.to_id, edge.type, edge.reason ?? null, 1.0, now);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
cleanupFailedMemorySave(adapter, id);
|
|
608
|
+
const rollbackError = (error instanceof Error ? error : new Error(String(error)));
|
|
609
|
+
rollbackError.memoryId = id;
|
|
610
|
+
throw rollbackError;
|
|
611
|
+
}
|
|
416
612
|
// Project to memory_truth table (best-effort; failure should not break save)
|
|
417
613
|
try {
|
|
418
614
|
await (0, truth_store_js_1.projectMemoryTruth)({
|
|
@@ -430,7 +626,19 @@ async function saveMemory(input) {
|
|
|
430
626
|
catch {
|
|
431
627
|
// Truth projection is best-effort; do not fail the save
|
|
432
628
|
}
|
|
433
|
-
return {
|
|
629
|
+
return {
|
|
630
|
+
success: true,
|
|
631
|
+
id,
|
|
632
|
+
saved_decision_id: id,
|
|
633
|
+
timeline_event_id: timelineEvent?.id ?? null,
|
|
634
|
+
timeline_event_ids: timelineEvent ? [timelineEvent.id] : [],
|
|
635
|
+
};
|
|
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);
|
|
434
642
|
}
|
|
435
643
|
async function buildProfile(scopes) {
|
|
436
644
|
const records = await loadScopedMemories(scopes);
|
|
@@ -444,8 +652,26 @@ const EXCLUDED_STATUSES = new Set([
|
|
|
444
652
|
]);
|
|
445
653
|
async function recallMemory(query, options = {}) {
|
|
446
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
|
+
};
|
|
447
668
|
let matched = [];
|
|
669
|
+
let fusedHits = [];
|
|
448
670
|
let retrievalSource = 'none';
|
|
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();
|
|
449
675
|
let _lexicalRecords = null;
|
|
450
676
|
const loadLexical = async () => {
|
|
451
677
|
if (_lexicalRecords === null) {
|
|
@@ -458,6 +684,7 @@ async function recallMemory(query, options = {}) {
|
|
|
458
684
|
const isAggregation = /\b(how many|how much|total|all|every|each|count|number of)\b/.test(lowerQuery);
|
|
459
685
|
const hasMultipleEntities = /\b(and|or|vs|versus|compared|between)\b/.test(lowerQuery);
|
|
460
686
|
const vectorLimit = isAggregation ? 50 : 20;
|
|
687
|
+
const requestedLimit = Math.max(1, Math.floor(options.limit ?? 10));
|
|
461
688
|
// Multi-query decomposition: extract sub-queries for complex questions
|
|
462
689
|
const subQueries = [query];
|
|
463
690
|
if (hasMultipleEntities) {
|
|
@@ -474,17 +701,22 @@ async function recallMemory(query, options = {}) {
|
|
|
474
701
|
await (0, db_manager_js_1.initDB)();
|
|
475
702
|
// Channel 1: Vector search (semantic similarity) — run all sub-queries
|
|
476
703
|
const vectorMatched = [];
|
|
704
|
+
let primaryQueryEmbedding = null;
|
|
477
705
|
try {
|
|
478
706
|
for (const sq of subQueries) {
|
|
479
707
|
const queryEmbedding = await (0, embeddings_js_1.generateEmbedding)(sq);
|
|
480
|
-
|
|
708
|
+
if (sq === query && primaryQueryEmbedding === null) {
|
|
709
|
+
primaryQueryEmbedding = queryEmbedding;
|
|
710
|
+
}
|
|
711
|
+
const vectorResults = await (0, db_manager_js_1.vectorSearch)(queryEmbedding, vectorLimit, searchOptions.threshold, searchOptions.topicPrefix);
|
|
481
712
|
let filtered = vectorResults;
|
|
713
|
+
let vectorScopeMap = new Map();
|
|
482
714
|
if (options.scopes && options.scopes.length > 0) {
|
|
483
715
|
const vectorIds = vectorResults.map((r) => String(r.id));
|
|
484
|
-
|
|
716
|
+
vectorScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), vectorIds);
|
|
485
717
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
486
718
|
filtered = vectorResults.filter((r) => {
|
|
487
|
-
const scopes =
|
|
719
|
+
const scopes = vectorScopeMap.get(String(r.id)) ?? [];
|
|
488
720
|
// When scopes are requested, zero-binding results must NOT pass through
|
|
489
721
|
if (scopes.length === 0)
|
|
490
722
|
return false;
|
|
@@ -504,11 +736,17 @@ async function recallMemory(query, options = {}) {
|
|
|
504
736
|
details: String(result.reasoning || ''),
|
|
505
737
|
confidence: result.similarity ?? 0.5,
|
|
506
738
|
status: effectiveStatus || 'active',
|
|
507
|
-
scopes: [],
|
|
739
|
+
scopes: vectorScopeMap.get(String(result.id)) ?? [],
|
|
508
740
|
source: { package: 'mama-core', source_type: 'vector_search' },
|
|
509
741
|
created_at: result.created_at ?? Date.now(),
|
|
510
742
|
updated_at: result.created_at ?? Date.now(),
|
|
743
|
+
event_date: result.event_date ?? null,
|
|
744
|
+
event_datetime: result.event_datetime ?? null,
|
|
511
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
|
+
}
|
|
512
750
|
}
|
|
513
751
|
} // end sub-query loop
|
|
514
752
|
}
|
|
@@ -530,7 +768,14 @@ async function recallMemory(query, options = {}) {
|
|
|
530
768
|
// Lazy lexical: skip expensive FTS5/lexical when vector already returned enough results,
|
|
531
769
|
// unless this is an aggregation query that benefits from broader coverage.
|
|
532
770
|
const VECTOR_SUFFICIENT_THRESHOLD = 5;
|
|
533
|
-
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;
|
|
534
779
|
let lexicalCandidates = [];
|
|
535
780
|
const lexicalLimit = isAggregation ? 100 : 50;
|
|
536
781
|
if (needsLexical) {
|
|
@@ -587,7 +832,7 @@ async function recallMemory(query, options = {}) {
|
|
|
587
832
|
for (const ftsRow of ftsResults) {
|
|
588
833
|
const row = adapter
|
|
589
834
|
.prepare(`SELECT id, topic, decision, reasoning, confidence, created_at, updated_at,
|
|
590
|
-
trust_context, kind, status, summary, event_date
|
|
835
|
+
trust_context, kind, status, summary, event_date, event_datetime
|
|
591
836
|
FROM decisions WHERE id = ?`)
|
|
592
837
|
.get(ftsRow.id);
|
|
593
838
|
if (!row)
|
|
@@ -602,7 +847,7 @@ async function recallMemory(query, options = {}) {
|
|
|
602
847
|
const scopeMap = batchLoadScopes(adapter, memoryIds);
|
|
603
848
|
const record = toMemoryRecord(row, scopeMap.get(String(row.id)) ?? [], fallbackSource);
|
|
604
849
|
// Topic prefix filtering (matches vectorSearch behavior)
|
|
605
|
-
if (
|
|
850
|
+
if (searchOptions.topicPrefix && !record.topic.startsWith(searchOptions.topicPrefix))
|
|
606
851
|
continue;
|
|
607
852
|
// Scope filtering
|
|
608
853
|
if (options.scopes && options.scopes.length > 0) {
|
|
@@ -636,8 +881,8 @@ async function recallMemory(query, options = {}) {
|
|
|
636
881
|
});
|
|
637
882
|
}
|
|
638
883
|
// Topic prefix filtering for in-memory lexical (matches vectorSearch behavior)
|
|
639
|
-
if (
|
|
640
|
-
lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(
|
|
884
|
+
if (searchOptions.topicPrefix) {
|
|
885
|
+
lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(searchOptions.topicPrefix));
|
|
641
886
|
}
|
|
642
887
|
lexicalCandidates = buildLexicalCandidates(lexicalRecords, query);
|
|
643
888
|
if (subQueries.length > 1) {
|
|
@@ -653,6 +898,10 @@ async function recallMemory(query, options = {}) {
|
|
|
653
898
|
}
|
|
654
899
|
}
|
|
655
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
|
+
}
|
|
656
905
|
// RRF Fusion: combine vector and lexical/FTS5 results by reciprocal rank
|
|
657
906
|
// FTS5 BM25 gets 2x weight — it naturally demotes records where query terms
|
|
658
907
|
// appear only in passing (e.g. "wedding" mentioned once in a dating record),
|
|
@@ -688,6 +937,63 @@ async function recallMemory(query, options = {}) {
|
|
|
688
937
|
rrfScores.set(r.id, { record: r, score: rrfScore });
|
|
689
938
|
}
|
|
690
939
|
const sortedRrf = Array.from(rrfScores.values()).sort((a, b) => b.score - a.score);
|
|
940
|
+
const decisionFusedHits = sortedRrf.map((entry) => ({
|
|
941
|
+
source_type: 'decision',
|
|
942
|
+
source_id: entry.record.id,
|
|
943
|
+
record: entry.record,
|
|
944
|
+
fused_rank_score: entry.score,
|
|
945
|
+
}));
|
|
946
|
+
let hasWikiHits = false;
|
|
947
|
+
const wikiScores = new Map();
|
|
948
|
+
const wikiVectorRankMap = new Map();
|
|
949
|
+
const wikiVectorScoreById = new Map();
|
|
950
|
+
try {
|
|
951
|
+
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
952
|
+
const wikiFtsHits = (0, wiki_page_index_js_1.ftsSearchWikiPages)(adapter, query, requestedLimit * 2);
|
|
953
|
+
let wikiVectorHits = [];
|
|
954
|
+
if (primaryQueryEmbedding) {
|
|
955
|
+
try {
|
|
956
|
+
wikiVectorHits = (0, wiki_page_index_js_1.vectorSearchWikiPages)(adapter, primaryQueryEmbedding, requestedLimit * 2);
|
|
957
|
+
}
|
|
958
|
+
catch (wikiVectorErr) {
|
|
959
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki vector search failed: ${wikiVectorErr instanceof Error ? wikiVectorErr.message : String(wikiVectorErr)}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
963
|
+
wikiVectorRankMap.set(wikiVectorHits[i].record.id, i);
|
|
964
|
+
wikiVectorScoreById.set(wikiVectorHits[i].record.id, wikiVectorHits[i].raw_score);
|
|
965
|
+
}
|
|
966
|
+
diagnostics.candidate_counts.lexical += wikiFtsHits.length;
|
|
967
|
+
diagnostics.candidate_counts.vector += wikiVectorHits.length;
|
|
968
|
+
for (let i = 0; i < wikiFtsHits.length; i++) {
|
|
969
|
+
const hit = wikiFtsHits[i];
|
|
970
|
+
const lexScore = 1 / (RRF_K + i + 1);
|
|
971
|
+
const vecRank = wikiVectorRankMap.get(hit.record.id);
|
|
972
|
+
const vecBoost = vecRank !== undefined ? 0.2 * (1 / (RRF_K + vecRank + 1)) : 0;
|
|
973
|
+
wikiScores.set(hit.record.id, {
|
|
974
|
+
record: hit.record,
|
|
975
|
+
score: lexScore + vecBoost,
|
|
976
|
+
lexicalSupport: true,
|
|
977
|
+
vectorSimilarity: wikiVectorScoreById.get(hit.record.id) ?? null,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
981
|
+
const hit = wikiVectorHits[i];
|
|
982
|
+
if (wikiScores.has(hit.record.id)) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
wikiScores.set(hit.record.id, {
|
|
986
|
+
record: hit.record,
|
|
987
|
+
score: 0.15 * (1 / (RRF_K + i + 1)),
|
|
988
|
+
lexicalSupport: false,
|
|
989
|
+
vectorSimilarity: hit.raw_score,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
catch (wikiFtsErr) {
|
|
994
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki FTS search failed: ${wikiFtsErr instanceof Error ? wikiFtsErr.message : String(wikiFtsErr)}`);
|
|
995
|
+
}
|
|
996
|
+
fusedHits = decisionFusedHits;
|
|
691
997
|
// Normalize RRF scores to 0-1 range so downstream consumers (threshold filters,
|
|
692
998
|
// similarity displays) get meaningful values. The raw RRF score for K=60 tops out
|
|
693
999
|
// around 0.033, which is misleading when compared against similarity thresholds.
|
|
@@ -705,6 +1011,355 @@ async function recallMemory(query, options = {}) {
|
|
|
705
1011
|
else if (lexicalCandidates.length > 0) {
|
|
706
1012
|
retrievalSource = 'lexical_search';
|
|
707
1013
|
}
|
|
1014
|
+
let canonicalMatched = [];
|
|
1015
|
+
if (projectionMode !== 'off') {
|
|
1016
|
+
try {
|
|
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
|
+
}
|
|
1022
|
+
if (projectionMode === 'dual-write' && canonicalMatched.length > 0) {
|
|
1023
|
+
const seenIds = new Set(matched.map((item) => item.id));
|
|
1024
|
+
for (const canonical of canonicalMatched) {
|
|
1025
|
+
if (!seenIds.has(canonical.id)) {
|
|
1026
|
+
matched.push(canonical);
|
|
1027
|
+
seenIds.add(canonical.id);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
retrievalSource =
|
|
1031
|
+
retrievalSource === 'none' ? 'entity_canonical' : `${retrievalSource}+entity_canonical`;
|
|
1032
|
+
}
|
|
1033
|
+
else if (projectionMode === 'shadow' && canonicalMatched.length > 0) {
|
|
1034
|
+
retrievalSource =
|
|
1035
|
+
retrievalSource === 'none' ? 'shadow_entity_probe' : `${retrievalSource}+shadow_probe`;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch (canonicalErr) {
|
|
1039
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Canonical entity recall failed: ${canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr)}`);
|
|
1040
|
+
}
|
|
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
|
+
}
|
|
1354
|
+
if (matched.length > 0) {
|
|
1355
|
+
const readIdentityIndex = await (0, read_identity_js_1.loadDecisionReadIdentityIndex)(matched.filter((record) => !record.read_identity).map((record) => record.id));
|
|
1356
|
+
for (const record of matched) {
|
|
1357
|
+
if (record.read_identity) {
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
record.read_identity = (0, read_identity_js_1.resolveReadIdentity)(record, readIdentityIndex.get(record.id) ?? []);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
708
1363
|
// Enrich active records with summaries from their superseded predecessors.
|
|
709
1364
|
// When ingestConversation extracts multiple facts under the same topic, only
|
|
710
1365
|
// the last survives as "active" — the earlier ones become superseded and are
|
|
@@ -731,7 +1386,7 @@ async function recallMemory(query, options = {}) {
|
|
|
731
1386
|
bundle.graph_context.primary = matched;
|
|
732
1387
|
bundle.graph_context.expanded = [];
|
|
733
1388
|
bundle.graph_context.edges = [];
|
|
734
|
-
if (matched.length > 0 && !options.skipGraphExpansion) {
|
|
1389
|
+
if (matched.length > 0 && !options.skipGraphExpansion && searchOptions.includeRelated) {
|
|
735
1390
|
try {
|
|
736
1391
|
const candidates = matched.map((m) => ({
|
|
737
1392
|
id: m.id,
|
|
@@ -746,6 +1401,7 @@ async function recallMemory(query, options = {}) {
|
|
|
746
1401
|
const expanded = await mamaDefault.expandWithGraph(candidates);
|
|
747
1402
|
const primaryIds = new Set(matched.map((m) => m.id));
|
|
748
1403
|
let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
|
|
1404
|
+
let expandedScopeMap = new Map();
|
|
749
1405
|
// Re-filter expanded results: apply status and scope checks
|
|
750
1406
|
if (!options.includeHistory) {
|
|
751
1407
|
expandedOnly = expandedOnly.filter((e) => {
|
|
@@ -757,32 +1413,63 @@ async function recallMemory(query, options = {}) {
|
|
|
757
1413
|
}
|
|
758
1414
|
if (options.scopes && options.scopes.length > 0) {
|
|
759
1415
|
const expandedIds = expandedOnly.map((e) => e.id);
|
|
760
|
-
|
|
1416
|
+
expandedScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), expandedIds);
|
|
761
1417
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
762
1418
|
expandedOnly = expandedOnly.filter((e) => {
|
|
763
|
-
const scopes =
|
|
1419
|
+
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
764
1420
|
if (scopes.length === 0)
|
|
765
1421
|
return false;
|
|
766
1422
|
return scopes.some((s) => requestedScopes.has(`${s.kind}:${s.id}`));
|
|
767
1423
|
});
|
|
768
1424
|
}
|
|
769
|
-
bundle.graph_context.expanded = expandedOnly.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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];
|
|
786
1473
|
const allEdges = await loadEdgesForIds(allIds);
|
|
787
1474
|
// Filter out edges pointing to decisions with excluded statuses
|
|
788
1475
|
const activeIds = new Set(allIds);
|
|
@@ -807,16 +1494,20 @@ async function recallMemory(query, options = {}) {
|
|
|
807
1494
|
// Graph expansion is best-effort; do not fail recall
|
|
808
1495
|
}
|
|
809
1496
|
}
|
|
1497
|
+
bundle.fused_hits = fusedHits;
|
|
810
1498
|
bundle.search_meta.scope_order = (options.scopes ?? []).map((scope) => scope.kind);
|
|
811
1499
|
bundle.search_meta.retrieval_sources = [retrievalSource];
|
|
1500
|
+
if (searchOptions.diagnostics) {
|
|
1501
|
+
bundle.search_meta.diagnostics = diagnostics;
|
|
1502
|
+
}
|
|
812
1503
|
if (options.includeProfile) {
|
|
813
1504
|
bundle.profile = await buildProfile(options.scopes ?? []);
|
|
814
1505
|
}
|
|
815
1506
|
return bundle;
|
|
816
1507
|
}
|
|
817
|
-
async function
|
|
1508
|
+
async function ingestMemoryInternal(input, options) {
|
|
818
1509
|
const normalized = input.content.trim();
|
|
819
|
-
return
|
|
1510
|
+
return saveMemoryInternal({
|
|
820
1511
|
topic: normalized
|
|
821
1512
|
.slice(0, 40)
|
|
822
1513
|
.toLowerCase()
|
|
@@ -828,7 +1519,14 @@ async function ingestMemory(input) {
|
|
|
828
1519
|
scopes: input.scopes ?? [],
|
|
829
1520
|
source: input.source,
|
|
830
1521
|
eventDate: input.eventDate,
|
|
831
|
-
|
|
1522
|
+
eventDateTime: input.eventDateTime,
|
|
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);
|
|
832
1530
|
}
|
|
833
1531
|
async function evolveMemory(input) {
|
|
834
1532
|
return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
|
|
@@ -880,18 +1578,18 @@ let extractionFn = callExtractionLLM;
|
|
|
880
1578
|
function setExtractionFn(fn) {
|
|
881
1579
|
extractionFn = fn ?? callExtractionLLM;
|
|
882
1580
|
}
|
|
883
|
-
async function
|
|
1581
|
+
async function ingestConversationInternal(input, options) {
|
|
884
1582
|
if (!input.messages || input.messages.length === 0) {
|
|
885
1583
|
throw new Error('messages array must not be empty');
|
|
886
1584
|
}
|
|
887
1585
|
const conversationText = input.messages.map((m) => `${m.role}: ${m.content}`).join('\n');
|
|
888
1586
|
const topicPrefix = input.topicPrefix || '';
|
|
889
|
-
const rawResult = await
|
|
1587
|
+
const rawResult = await ingestMemoryInternal({
|
|
890
1588
|
content: topicPrefix ? `${topicPrefix}${conversationText}` : conversationText,
|
|
891
1589
|
scopes: input.scopes,
|
|
892
1590
|
source: input.source,
|
|
893
1591
|
eventDate: input.sessionDate,
|
|
894
|
-
});
|
|
1592
|
+
}, options);
|
|
895
1593
|
const result = {
|
|
896
1594
|
rawId: rawResult.id,
|
|
897
1595
|
extractedMemories: [],
|
|
@@ -940,7 +1638,7 @@ async function ingestConversation(input) {
|
|
|
940
1638
|
const batchSavedIds = [];
|
|
941
1639
|
for (const unit of units) {
|
|
942
1640
|
try {
|
|
943
|
-
const saved = await
|
|
1641
|
+
const saved = await saveMemoryInternal({
|
|
944
1642
|
topic: topicPrefix ? `${topicPrefix}${unit.topic}` : unit.topic,
|
|
945
1643
|
kind: unit.kind,
|
|
946
1644
|
summary: unit.summary,
|
|
@@ -950,7 +1648,7 @@ async function ingestConversation(input) {
|
|
|
950
1648
|
source: input.source,
|
|
951
1649
|
excludeIds: batchSavedIds,
|
|
952
1650
|
eventDate: input.sessionDate,
|
|
953
|
-
});
|
|
1651
|
+
}, (0, provenance_js_1.appendProvenanceSourceRefs)(options, [`raw_memory:${rawResult.id}`]));
|
|
954
1652
|
const now = Date.now();
|
|
955
1653
|
adapter
|
|
956
1654
|
.prepare(`INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
|
|
@@ -969,4 +1667,10 @@ async function ingestConversation(input) {
|
|
|
969
1667
|
}
|
|
970
1668
|
return result;
|
|
971
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
|
+
}
|
|
972
1676
|
//# sourceMappingURL=api.js.map
|