@jungjaehoon/mama-core 1.4.2 → 1.5.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/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/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 +23 -0
- package/dist/cases/search-rollup.js +225 -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 +44 -0
- package/dist/connectors/event-index.js +366 -0
- package/dist/connectors/types.d.ts +69 -0
- package/dist/connectors/types.js +3 -0
- package/dist/db-manager.d.ts +4 -0
- package/dist/db-manager.js +10 -3
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/decision-formatter.js +11 -8
- package/dist/embedding-server/mobile/daemon.d.ts +1 -0
- package/dist/embedding-server/mobile/daemon.js +11 -1
- 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 +47 -2
- package/dist/index.js +66 -25
- package/dist/mama-api.d.ts +22 -1
- package/dist/mama-api.js +272 -64
- package/dist/memory/api.d.ts +28 -1
- package/dist/memory/api.js +293 -31
- package/dist/memory/types.d.ts +11 -2
- 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/test-utils.d.ts +14 -0
- package/dist/test-utils.js +52 -0
- package/package.json +21 -3
package/dist/memory/api.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.ingestConversation = ingestConversation;
|
|
|
19
19
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
20
|
const db_manager_js_1 = require("../db-manager.js");
|
|
21
21
|
const embeddings_js_1 = require("../embeddings.js");
|
|
22
|
+
const wiki_page_index_js_1 = require("../cases/wiki-page-index.js");
|
|
22
23
|
const profile_builder_js_1 = require("./profile-builder.js");
|
|
23
24
|
const bootstrap_builder_js_1 = require("./bootstrap-builder.js");
|
|
24
25
|
const evolution_engine_js_1 = require("./evolution-engine.js");
|
|
@@ -30,6 +31,8 @@ const types_js_1 = require("./types.js");
|
|
|
30
31
|
const channel_summary_store_js_1 = require("./channel-summary-store.js");
|
|
31
32
|
Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.getChannelSummary; } });
|
|
32
33
|
Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.upsertChannelSummary; } });
|
|
34
|
+
const recall_bridge_js_1 = require("../entities/recall-bridge.js");
|
|
35
|
+
const read_identity_js_1 = require("../entities/read-identity.js");
|
|
33
36
|
function buildDecisionId(topic) {
|
|
34
37
|
const safeTopic = topic.replace(/[^a-z0-9_]+/gi, '_').toLowerCase();
|
|
35
38
|
return `decision_${safeTopic}_${Date.now()}_${node_crypto_1.default.randomUUID().slice(0, 8)}`;
|
|
@@ -58,8 +61,128 @@ function toMemoryRecord(row, scopes, fallbackSource) {
|
|
|
58
61
|
created_at: row.created_at,
|
|
59
62
|
updated_at: row.updated_at ?? row.created_at,
|
|
60
63
|
event_date: row.event_date ?? null,
|
|
64
|
+
event_datetime: typeof row.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
65
|
+
? row.event_datetime
|
|
66
|
+
: null,
|
|
61
67
|
};
|
|
62
68
|
}
|
|
69
|
+
function loadEventDateTimeForObservations(adapter, observationIds) {
|
|
70
|
+
const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
|
|
71
|
+
if (uniqueIds.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const placeholders = uniqueIds.map(() => '?').join(', ');
|
|
75
|
+
const row = adapter
|
|
76
|
+
.prepare(`
|
|
77
|
+
SELECT MAX(COALESCE(timestamp_observed, created_at)) AS event_datetime
|
|
78
|
+
FROM entity_observations
|
|
79
|
+
WHERE id IN (${placeholders})
|
|
80
|
+
`)
|
|
81
|
+
.get(...uniqueIds);
|
|
82
|
+
return typeof row?.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
83
|
+
? row.event_datetime
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
86
|
+
function getTimelineEntityKindPriority(kind) {
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case 'work_item':
|
|
89
|
+
return 0;
|
|
90
|
+
case 'project':
|
|
91
|
+
return 1;
|
|
92
|
+
case 'organization':
|
|
93
|
+
return 2;
|
|
94
|
+
case 'person':
|
|
95
|
+
return 3;
|
|
96
|
+
default:
|
|
97
|
+
return 99;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function resolveTimelineTargetEntityIdFromObservations(adapter, observationIds) {
|
|
101
|
+
const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
|
|
102
|
+
if (uniqueIds.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const placeholders = uniqueIds.map(() => '?').join(', ');
|
|
106
|
+
const rows = adapter
|
|
107
|
+
.prepare(`
|
|
108
|
+
SELECT
|
|
109
|
+
n.id AS entity_id,
|
|
110
|
+
n.kind AS entity_kind,
|
|
111
|
+
COUNT(*) AS matched_observations
|
|
112
|
+
FROM entity_lineage_links l
|
|
113
|
+
JOIN entity_nodes n
|
|
114
|
+
ON n.id = l.canonical_entity_id
|
|
115
|
+
WHERE l.entity_observation_id IN (${placeholders})
|
|
116
|
+
AND l.status = 'active'
|
|
117
|
+
AND n.status = 'active'
|
|
118
|
+
AND n.merged_into IS NULL
|
|
119
|
+
GROUP BY n.id, n.kind
|
|
120
|
+
`)
|
|
121
|
+
.all(...uniqueIds);
|
|
122
|
+
rows.sort((left, right) => {
|
|
123
|
+
const kindDelta = getTimelineEntityKindPriority(left.entity_kind) -
|
|
124
|
+
getTimelineEntityKindPriority(right.entity_kind);
|
|
125
|
+
if (kindDelta !== 0) {
|
|
126
|
+
return kindDelta;
|
|
127
|
+
}
|
|
128
|
+
if (right.matched_observations !== left.matched_observations) {
|
|
129
|
+
return right.matched_observations - left.matched_observations;
|
|
130
|
+
}
|
|
131
|
+
return left.entity_id.localeCompare(right.entity_id);
|
|
132
|
+
});
|
|
133
|
+
return rows[0]?.entity_id ?? null;
|
|
134
|
+
}
|
|
135
|
+
function buildTimelineEventForSave(adapter, memoryId, topic, entityObservationIds, timelineEvent) {
|
|
136
|
+
if (!timelineEvent) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const resolvedEntityId = timelineEvent.entity_id ??
|
|
140
|
+
resolveTimelineTargetEntityIdFromObservations(adapter, entityObservationIds);
|
|
141
|
+
if (!resolvedEntityId) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id: timelineEvent.id ?? `et_${node_crypto_1.default.randomUUID()}`,
|
|
146
|
+
entity_id: resolvedEntityId,
|
|
147
|
+
event_type: timelineEvent.event_type,
|
|
148
|
+
role: timelineEvent.role ?? null,
|
|
149
|
+
valid_from: timelineEvent.valid_from ?? null,
|
|
150
|
+
valid_to: timelineEvent.valid_to ?? null,
|
|
151
|
+
observed_at: timelineEvent.observed_at ?? null,
|
|
152
|
+
source_ref: timelineEvent.source_ref ?? `decision:${memoryId}`,
|
|
153
|
+
summary: timelineEvent.summary,
|
|
154
|
+
details: timelineEvent.details ??
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
memory_id: memoryId,
|
|
157
|
+
topic,
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function insertTimelineEventForSave(adapter, event, createdAt) {
|
|
162
|
+
adapter
|
|
163
|
+
.prepare(`
|
|
164
|
+
INSERT INTO entity_timeline_events (
|
|
165
|
+
id, entity_id, event_type, role, valid_from, valid_to, observed_at,
|
|
166
|
+
source_ref, summary, details, created_at
|
|
167
|
+
)
|
|
168
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
169
|
+
`)
|
|
170
|
+
.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);
|
|
171
|
+
}
|
|
172
|
+
function cleanupFailedMemorySave(adapter, memoryId) {
|
|
173
|
+
const row = adapter.prepare(`SELECT rowid FROM decisions WHERE id = ?`).get(memoryId);
|
|
174
|
+
if (!row) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const cleanup = () => {
|
|
178
|
+
adapter.prepare(`DELETE FROM embeddings WHERE rowid = ?`).run(row.rowid);
|
|
179
|
+
adapter.prepare(`DELETE FROM decisions WHERE id = ?`).run(memoryId);
|
|
180
|
+
};
|
|
181
|
+
const txResult = adapter.transaction(cleanup);
|
|
182
|
+
if (typeof txResult === 'function') {
|
|
183
|
+
txResult();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
63
186
|
function batchLoadScopes(adapter, memoryIds) {
|
|
64
187
|
const scopeMap = new Map();
|
|
65
188
|
if (memoryIds.length === 0)
|
|
@@ -90,9 +213,9 @@ async function loadScopedMemories(scopes) {
|
|
|
90
213
|
rows = adapter
|
|
91
214
|
.prepare(`
|
|
92
215
|
SELECT id, topic, decision, reasoning, confidence, created_at, updated_at, trust_context,
|
|
93
|
-
kind, status, summary, event_date
|
|
216
|
+
kind, status, summary, event_date, event_datetime
|
|
94
217
|
FROM decisions
|
|
95
|
-
ORDER BY created_at DESC
|
|
218
|
+
ORDER BY COALESCE(event_datetime, created_at) DESC, created_at DESC
|
|
96
219
|
`)
|
|
97
220
|
.all();
|
|
98
221
|
}
|
|
@@ -102,11 +225,11 @@ async function loadScopedMemories(scopes) {
|
|
|
102
225
|
rows = adapter
|
|
103
226
|
.prepare(`
|
|
104
227
|
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
|
|
228
|
+
d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime
|
|
106
229
|
FROM decisions d
|
|
107
230
|
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
108
231
|
WHERE msb.scope_id IN (${placeholders})
|
|
109
|
-
ORDER BY d.created_at DESC
|
|
232
|
+
ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
|
|
110
233
|
`)
|
|
111
234
|
.all(...scopeIds);
|
|
112
235
|
}
|
|
@@ -364,6 +487,11 @@ async function saveMemory(input) {
|
|
|
364
487
|
})),
|
|
365
488
|
});
|
|
366
489
|
const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ?? null;
|
|
490
|
+
const entityObservationIds = Array.from(new Set(input.entityObservationIds ?? []));
|
|
491
|
+
const eventDateTime = typeof input.eventDateTime === 'number'
|
|
492
|
+
? input.eventDateTime
|
|
493
|
+
: loadEventDateTimeForObservations(adapter, entityObservationIds);
|
|
494
|
+
const timelineEvent = buildTimelineEventForSave(adapter, id, input.topic, entityObservationIds, input.timelineEvent);
|
|
367
495
|
await (0, db_manager_js_1.insertDecisionWithEmbedding)({
|
|
368
496
|
id,
|
|
369
497
|
topic: input.topic,
|
|
@@ -375,6 +503,7 @@ async function saveMemory(input) {
|
|
|
375
503
|
updated_at: now,
|
|
376
504
|
trust_context: JSON.stringify({ source: input.source }),
|
|
377
505
|
event_date: input.eventDate ?? null,
|
|
506
|
+
event_datetime: eventDateTime,
|
|
378
507
|
});
|
|
379
508
|
// Pre-resolve scope IDs before the synchronous transaction
|
|
380
509
|
const resolvedScopeIds = [];
|
|
@@ -383,36 +512,56 @@ async function saveMemory(input) {
|
|
|
383
512
|
resolvedScopeIds.push({ scopeId, isPrimary: index === 0 });
|
|
384
513
|
}
|
|
385
514
|
// 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) {
|
|
515
|
+
try {
|
|
516
|
+
adapter.transaction(() => {
|
|
395
517
|
adapter
|
|
396
518
|
.prepare(`
|
|
397
|
-
|
|
398
|
-
|
|
519
|
+
UPDATE decisions
|
|
520
|
+
SET kind = ?, status = ?, summary = ?, is_static = ?, trust_context = ?, updated_at = ?
|
|
521
|
+
WHERE id = ?
|
|
399
522
|
`)
|
|
400
|
-
.run(
|
|
401
|
-
|
|
402
|
-
for (const edge of evolution.edges) {
|
|
403
|
-
if (edge.type === 'supersedes') {
|
|
523
|
+
.run(input.kind, input.status ?? 'active', input.summary, input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0, JSON.stringify({ source: input.source }), now, id);
|
|
524
|
+
for (const { scopeId, isPrimary } of resolvedScopeIds) {
|
|
404
525
|
adapter
|
|
405
|
-
.prepare(`
|
|
406
|
-
|
|
526
|
+
.prepare(`
|
|
527
|
+
INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
|
|
528
|
+
VALUES (?, ?, ?)
|
|
529
|
+
`)
|
|
530
|
+
.run(id, scopeId, isPrimary ? 1 : 0);
|
|
407
531
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
532
|
+
for (const entityObservationId of entityObservationIds) {
|
|
533
|
+
adapter
|
|
534
|
+
.prepare(`
|
|
535
|
+
INSERT OR IGNORE INTO decision_entity_sources (
|
|
536
|
+
decision_id, entity_observation_id, relation_type, created_at
|
|
537
|
+
) VALUES (?, ?, ?, ?)
|
|
538
|
+
`)
|
|
539
|
+
.run(id, entityObservationId, 'support', now);
|
|
540
|
+
}
|
|
541
|
+
if (timelineEvent) {
|
|
542
|
+
insertTimelineEventForSave(adapter, timelineEvent, now);
|
|
543
|
+
}
|
|
544
|
+
for (const edge of evolution.edges) {
|
|
545
|
+
if (edge.type === 'supersedes') {
|
|
546
|
+
adapter
|
|
547
|
+
.prepare(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
|
|
548
|
+
.run(id, now, edge.to_id);
|
|
549
|
+
}
|
|
550
|
+
adapter
|
|
551
|
+
.prepare(`
|
|
552
|
+
INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
|
|
553
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
554
|
+
`)
|
|
555
|
+
.run(id, edge.to_id, edge.type, edge.reason ?? null, 1.0, now);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
cleanupFailedMemorySave(adapter, id);
|
|
561
|
+
const rollbackError = (error instanceof Error ? error : new Error(String(error)));
|
|
562
|
+
rollbackError.memoryId = id;
|
|
563
|
+
throw rollbackError;
|
|
564
|
+
}
|
|
416
565
|
// Project to memory_truth table (best-effort; failure should not break save)
|
|
417
566
|
try {
|
|
418
567
|
await (0, truth_store_js_1.projectMemoryTruth)({
|
|
@@ -430,7 +579,13 @@ async function saveMemory(input) {
|
|
|
430
579
|
catch {
|
|
431
580
|
// Truth projection is best-effort; do not fail the save
|
|
432
581
|
}
|
|
433
|
-
return {
|
|
582
|
+
return {
|
|
583
|
+
success: true,
|
|
584
|
+
id,
|
|
585
|
+
saved_decision_id: id,
|
|
586
|
+
timeline_event_id: timelineEvent?.id ?? null,
|
|
587
|
+
timeline_event_ids: timelineEvent ? [timelineEvent.id] : [],
|
|
588
|
+
};
|
|
434
589
|
}
|
|
435
590
|
async function buildProfile(scopes) {
|
|
436
591
|
const records = await loadScopedMemories(scopes);
|
|
@@ -445,7 +600,9 @@ const EXCLUDED_STATUSES = new Set([
|
|
|
445
600
|
async function recallMemory(query, options = {}) {
|
|
446
601
|
const bundle = (0, types_js_1.createEmptyRecallBundle)(query);
|
|
447
602
|
let matched = [];
|
|
603
|
+
let fusedHits = [];
|
|
448
604
|
let retrievalSource = 'none';
|
|
605
|
+
const projectionMode = process.env.MAMA_ENTITY_PROJECTION_MODE ?? 'shadow';
|
|
449
606
|
let _lexicalRecords = null;
|
|
450
607
|
const loadLexical = async () => {
|
|
451
608
|
if (_lexicalRecords === null) {
|
|
@@ -458,6 +615,7 @@ async function recallMemory(query, options = {}) {
|
|
|
458
615
|
const isAggregation = /\b(how many|how much|total|all|every|each|count|number of)\b/.test(lowerQuery);
|
|
459
616
|
const hasMultipleEntities = /\b(and|or|vs|versus|compared|between)\b/.test(lowerQuery);
|
|
460
617
|
const vectorLimit = isAggregation ? 50 : 20;
|
|
618
|
+
const requestedLimit = Math.max(1, Math.floor(options.limit ?? 10));
|
|
461
619
|
// Multi-query decomposition: extract sub-queries for complex questions
|
|
462
620
|
const subQueries = [query];
|
|
463
621
|
if (hasMultipleEntities) {
|
|
@@ -474,9 +632,13 @@ async function recallMemory(query, options = {}) {
|
|
|
474
632
|
await (0, db_manager_js_1.initDB)();
|
|
475
633
|
// Channel 1: Vector search (semantic similarity) — run all sub-queries
|
|
476
634
|
const vectorMatched = [];
|
|
635
|
+
let primaryQueryEmbedding = null;
|
|
477
636
|
try {
|
|
478
637
|
for (const sq of subQueries) {
|
|
479
638
|
const queryEmbedding = await (0, embeddings_js_1.generateEmbedding)(sq);
|
|
639
|
+
if (sq === query && primaryQueryEmbedding === null) {
|
|
640
|
+
primaryQueryEmbedding = queryEmbedding;
|
|
641
|
+
}
|
|
480
642
|
const vectorResults = await (0, db_manager_js_1.vectorSearch)(queryEmbedding, vectorLimit, 0.3, options.topicPrefix);
|
|
481
643
|
let filtered = vectorResults;
|
|
482
644
|
if (options.scopes && options.scopes.length > 0) {
|
|
@@ -508,6 +670,8 @@ async function recallMemory(query, options = {}) {
|
|
|
508
670
|
source: { package: 'mama-core', source_type: 'vector_search' },
|
|
509
671
|
created_at: result.created_at ?? Date.now(),
|
|
510
672
|
updated_at: result.created_at ?? Date.now(),
|
|
673
|
+
event_date: result.event_date ?? null,
|
|
674
|
+
event_datetime: result.event_datetime ?? null,
|
|
511
675
|
});
|
|
512
676
|
}
|
|
513
677
|
} // end sub-query loop
|
|
@@ -587,7 +751,7 @@ async function recallMemory(query, options = {}) {
|
|
|
587
751
|
for (const ftsRow of ftsResults) {
|
|
588
752
|
const row = adapter
|
|
589
753
|
.prepare(`SELECT id, topic, decision, reasoning, confidence, created_at, updated_at,
|
|
590
|
-
trust_context, kind, status, summary, event_date
|
|
754
|
+
trust_context, kind, status, summary, event_date, event_datetime
|
|
591
755
|
FROM decisions WHERE id = ?`)
|
|
592
756
|
.get(ftsRow.id);
|
|
593
757
|
if (!row)
|
|
@@ -688,6 +852,66 @@ async function recallMemory(query, options = {}) {
|
|
|
688
852
|
rrfScores.set(r.id, { record: r, score: rrfScore });
|
|
689
853
|
}
|
|
690
854
|
const sortedRrf = Array.from(rrfScores.values()).sort((a, b) => b.score - a.score);
|
|
855
|
+
const decisionFusedHits = sortedRrf.map((entry) => ({
|
|
856
|
+
source_type: 'decision',
|
|
857
|
+
source_id: entry.record.id,
|
|
858
|
+
record: entry.record,
|
|
859
|
+
fused_rank_score: entry.score,
|
|
860
|
+
}));
|
|
861
|
+
let hasWikiHits = false;
|
|
862
|
+
const wikiScores = new Map();
|
|
863
|
+
const wikiVectorRankMap = new Map();
|
|
864
|
+
try {
|
|
865
|
+
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
866
|
+
const wikiFtsHits = (0, wiki_page_index_js_1.ftsSearchWikiPages)(adapter, query, requestedLimit * 2);
|
|
867
|
+
let wikiVectorHits = [];
|
|
868
|
+
if (primaryQueryEmbedding) {
|
|
869
|
+
try {
|
|
870
|
+
wikiVectorHits = (0, wiki_page_index_js_1.vectorSearchWikiPages)(adapter, primaryQueryEmbedding, requestedLimit * 2);
|
|
871
|
+
}
|
|
872
|
+
catch (wikiVectorErr) {
|
|
873
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki vector search failed: ${wikiVectorErr instanceof Error ? wikiVectorErr.message : String(wikiVectorErr)}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
877
|
+
wikiVectorRankMap.set(wikiVectorHits[i].record.id, i);
|
|
878
|
+
}
|
|
879
|
+
for (let i = 0; i < wikiFtsHits.length; i++) {
|
|
880
|
+
const hit = wikiFtsHits[i];
|
|
881
|
+
const lexScore = 1 / (RRF_K + i + 1);
|
|
882
|
+
const vecRank = wikiVectorRankMap.get(hit.record.id);
|
|
883
|
+
const vecBoost = vecRank !== undefined ? 0.2 * (1 / (RRF_K + vecRank + 1)) : 0;
|
|
884
|
+
wikiScores.set(hit.record.id, {
|
|
885
|
+
record: hit.record,
|
|
886
|
+
score: lexScore + vecBoost,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
890
|
+
const hit = wikiVectorHits[i];
|
|
891
|
+
if (wikiScores.has(hit.record.id)) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
wikiScores.set(hit.record.id, {
|
|
895
|
+
record: hit.record,
|
|
896
|
+
score: 0.15 * (1 / (RRF_K + i + 1)),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
hasWikiHits = wikiScores.size > 0;
|
|
900
|
+
}
|
|
901
|
+
catch (wikiFtsErr) {
|
|
902
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki FTS search failed: ${wikiFtsErr instanceof Error ? wikiFtsErr.message : String(wikiFtsErr)}`);
|
|
903
|
+
}
|
|
904
|
+
const wikiFusedHits = Array.from(wikiScores.values())
|
|
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);
|
|
691
915
|
// Normalize RRF scores to 0-1 range so downstream consumers (threshold filters,
|
|
692
916
|
// similarity displays) get meaningful values. The raw RRF score for K=60 tops out
|
|
693
917
|
// around 0.033, which is misleading when compared against similarity thresholds.
|
|
@@ -705,6 +929,42 @@ async function recallMemory(query, options = {}) {
|
|
|
705
929
|
else if (lexicalCandidates.length > 0) {
|
|
706
930
|
retrievalSource = 'lexical_search';
|
|
707
931
|
}
|
|
932
|
+
if (hasWikiHits) {
|
|
933
|
+
retrievalSource = retrievalSource === 'none' ? 'wiki_page' : `${retrievalSource}+wiki_page`;
|
|
934
|
+
}
|
|
935
|
+
let canonicalMatched = [];
|
|
936
|
+
if (projectionMode !== 'off') {
|
|
937
|
+
try {
|
|
938
|
+
canonicalMatched = await (0, recall_bridge_js_1.queryCanonicalEntities)(query, options.scopes ?? [], { limit: 10 });
|
|
939
|
+
if (projectionMode === 'dual-write' && canonicalMatched.length > 0) {
|
|
940
|
+
const seenIds = new Set(matched.map((item) => item.id));
|
|
941
|
+
for (const canonical of canonicalMatched) {
|
|
942
|
+
if (!seenIds.has(canonical.id)) {
|
|
943
|
+
matched.push(canonical);
|
|
944
|
+
seenIds.add(canonical.id);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
retrievalSource =
|
|
948
|
+
retrievalSource === 'none' ? 'entity_canonical' : `${retrievalSource}+entity_canonical`;
|
|
949
|
+
}
|
|
950
|
+
else if (projectionMode === 'shadow' && canonicalMatched.length > 0) {
|
|
951
|
+
retrievalSource =
|
|
952
|
+
retrievalSource === 'none' ? 'shadow_entity_probe' : `${retrievalSource}+shadow_probe`;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (canonicalErr) {
|
|
956
|
+
(0, debug_logger_js_1.warn)(`[recallMemory] Canonical entity recall failed: ${canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr)}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (matched.length > 0) {
|
|
960
|
+
const readIdentityIndex = await (0, read_identity_js_1.loadDecisionReadIdentityIndex)(matched.filter((record) => !record.read_identity).map((record) => record.id));
|
|
961
|
+
for (const record of matched) {
|
|
962
|
+
if (record.read_identity) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
record.read_identity = (0, read_identity_js_1.resolveReadIdentity)(record, readIdentityIndex.get(record.id) ?? []);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
708
968
|
// Enrich active records with summaries from their superseded predecessors.
|
|
709
969
|
// When ingestConversation extracts multiple facts under the same topic, only
|
|
710
970
|
// the last survives as "active" — the earlier ones become superseded and are
|
|
@@ -807,6 +1067,7 @@ async function recallMemory(query, options = {}) {
|
|
|
807
1067
|
// Graph expansion is best-effort; do not fail recall
|
|
808
1068
|
}
|
|
809
1069
|
}
|
|
1070
|
+
bundle.fused_hits = fusedHits;
|
|
810
1071
|
bundle.search_meta.scope_order = (options.scopes ?? []).map((scope) => scope.kind);
|
|
811
1072
|
bundle.search_meta.retrieval_sources = [retrievalSource];
|
|
812
1073
|
if (options.includeProfile) {
|
|
@@ -828,6 +1089,7 @@ async function ingestMemory(input) {
|
|
|
828
1089
|
scopes: input.scopes ?? [],
|
|
829
1090
|
source: input.source,
|
|
830
1091
|
eventDate: input.eventDate,
|
|
1092
|
+
eventDateTime: input.eventDateTime,
|
|
831
1093
|
});
|
|
832
1094
|
}
|
|
833
1095
|
async function evolveMemory(input) {
|
package/dist/memory/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConnectorEventSearchHit } from '../connectors/types.js';
|
|
2
|
+
import type { ReadIdentity } from '../entities/read-identity.js';
|
|
1
3
|
export declare const MEMORY_SCOPE_KINDS: readonly ["global", "user", "channel", "project"];
|
|
2
4
|
export type MemoryScopeKind = (typeof MEMORY_SCOPE_KINDS)[number];
|
|
3
5
|
export declare const MEMORY_KINDS: readonly ["decision", "preference", "constraint", "lesson", "fact", "task", "schedule", "compiled"];
|
|
@@ -38,6 +40,9 @@ export interface MemoryRecord {
|
|
|
38
40
|
updated_at: number | string;
|
|
39
41
|
/** ISO 8601 date when the event actually occurred (e.g. "2023-01-15"). Null if not set. */
|
|
40
42
|
event_date?: string | null;
|
|
43
|
+
/** Source event timestamp in milliseconds when known. Null if not set. */
|
|
44
|
+
event_datetime?: number | null;
|
|
45
|
+
read_identity?: ReadIdentity;
|
|
41
46
|
}
|
|
42
47
|
export interface MemoryEdge {
|
|
43
48
|
from_id: string;
|
|
@@ -57,6 +62,8 @@ export interface ProfileSnapshot {
|
|
|
57
62
|
export interface RecallBundle {
|
|
58
63
|
profile: ProfileSnapshot;
|
|
59
64
|
memories: MemoryRecord[];
|
|
65
|
+
/** Phase 3: cross-connector event hits, populated when searchWithConnectorEvents=true */
|
|
66
|
+
connector_event_hits?: ConnectorEventSearchHit[];
|
|
60
67
|
graph_context: {
|
|
61
68
|
primary: MemoryRecord[];
|
|
62
69
|
expanded: MemoryRecord[];
|
|
@@ -68,10 +75,12 @@ export interface RecallBundle {
|
|
|
68
75
|
retrieval_sources: string[];
|
|
69
76
|
};
|
|
70
77
|
}
|
|
78
|
+
/** Phase 3: unified search result type — memory-authored or connector-enriched. */
|
|
79
|
+
export type MemorySearchResultHit = MemoryRecord | ConnectorEventSearchHit;
|
|
71
80
|
export interface MemoryEventRecord {
|
|
72
81
|
event_id: string;
|
|
73
|
-
event_type: 'observed_conversation' | 'save' | 'supersede' | 'contradict' | 'mark_stale' | 'quarantine' | 'no_op' | 'audit_failed' | 'notice_sent';
|
|
74
|
-
actor: 'memory_agent' | 'main_agent' | 'user' | 'system';
|
|
82
|
+
event_type: 'observed_conversation' | 'save' | 'supersede' | 'contradict' | 'mark_stale' | 'quarantine' | 'no_op' | 'audit_failed' | 'notice_sent' | 'provenance.empty_batch' | 'provenance.link_write_failed' | 'connector_event_index.retention_swept' | 'case.link_created' | 'case.link_revoked' | 'case.membership_pinned' | 'case.membership_unpinned' | 'case.source_promoted' | 'case.freshness_drifted' | 'automation.job.started' | 'automation.job.succeeded' | 'automation.job.failed' | 'automation.job.skipped_concurrent' | 'review.bulk.approved' | 'review.bulk.rejected' | 'review.bulk.deferred' | 'canary.drift.false_merge_rate' | 'canary.drift.projection_fragmentation_rate' | 'canary.drift.replay_stale' | 'auth.role_denied' | 'ontology.proposal' | 'ontology.approval' | 'repair.approval' | 'case.fast_write_applied' | 'case.fast_write_lock_skipped' | 'case.correction_applied' | 'case.correction_reverted' | 'case.correction_superseded' | 'case.membership_tombstoned' | 'case.merged' | 'case.split' | 'case.membership_matched' | 'case.membership_candidate';
|
|
83
|
+
actor: 'memory_agent' | 'main_agent' | 'user' | 'system' | `user:${string}` | `user_uuid:${string}` | `local:${string}` | `actor:${string}` | 'token:bearer';
|
|
75
84
|
source_turn_id?: string;
|
|
76
85
|
memory_id?: string;
|
|
77
86
|
topic?: string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RunResult } from '../db-adapter/statement.js';
|
|
2
|
+
import { type QuestionType } from './question-type.js';
|
|
3
|
+
export type SearchFeedbackKind = 'shown' | 'accept' | 'reject' | 'hide' | 'click';
|
|
4
|
+
export type SearchFeedbackSourceType = 'decision' | 'checkpoint' | 'wiki_page' | 'case' | 'connector_event';
|
|
5
|
+
export interface SearchFeedbackInput {
|
|
6
|
+
result_id: string;
|
|
7
|
+
session_id?: string | null;
|
|
8
|
+
query: string;
|
|
9
|
+
result_source_type: SearchFeedbackSourceType;
|
|
10
|
+
result_source_id: string;
|
|
11
|
+
feedback_kind: SearchFeedbackKind;
|
|
12
|
+
question_type?: QuestionType | null;
|
|
13
|
+
shown_index: number;
|
|
14
|
+
result_case_id?: string | null;
|
|
15
|
+
clicked_result_rank?: number | null;
|
|
16
|
+
created_at?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ListSearchFeedbackOptions {
|
|
19
|
+
since?: string;
|
|
20
|
+
until?: string;
|
|
21
|
+
limit?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SearchFeedbackRow {
|
|
24
|
+
feedback_id: string;
|
|
25
|
+
query: string;
|
|
26
|
+
query_hash_hex: string;
|
|
27
|
+
question_type: QuestionType;
|
|
28
|
+
result_source_type: SearchFeedbackSourceType;
|
|
29
|
+
result_source_id: string;
|
|
30
|
+
case_id: string | null;
|
|
31
|
+
feedback_kind: SearchFeedbackKind;
|
|
32
|
+
rank_position: number;
|
|
33
|
+
score_before: number | null;
|
|
34
|
+
score_after: number | null;
|
|
35
|
+
session_id: string | null;
|
|
36
|
+
actor: string;
|
|
37
|
+
metadata_json: string | null;
|
|
38
|
+
created_at: string;
|
|
39
|
+
updated_at: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface RecordSearchFeedbackResult {
|
|
42
|
+
feedback_id: string;
|
|
43
|
+
query_hash: string;
|
|
44
|
+
deduped: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface FeedbackStoreAdapter {
|
|
47
|
+
prepare(sql: string): {
|
|
48
|
+
run(...params: unknown[]): RunResult;
|
|
49
|
+
get(...params: unknown[]): unknown;
|
|
50
|
+
all(...params: unknown[]): unknown[];
|
|
51
|
+
};
|
|
52
|
+
transaction?<T>(fn: () => T): T | (() => T);
|
|
53
|
+
}
|
|
54
|
+
export declare function hashQuery(query: string): string;
|
|
55
|
+
export declare function getFeedbackRetentionDays(adapter: FeedbackStoreAdapter): number;
|
|
56
|
+
export declare function compactShownSearchFeedback(adapter: FeedbackStoreAdapter, retentionDays?: number): number;
|
|
57
|
+
export declare function recordSearchFeedback(adapter: FeedbackStoreAdapter, input: SearchFeedbackInput): RecordSearchFeedbackResult;
|
|
58
|
+
export declare function listSearchFeedback(adapter: FeedbackStoreAdapter, options?: ListSearchFeedbackOptions): SearchFeedbackRow[];
|
|
59
|
+
//# sourceMappingURL=feedback-store.d.ts.map
|