@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.
Files changed (118) hide show
  1. package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
  2. package/db/migrations/027-create-entity-review-support.sql +44 -0
  3. package/db/migrations/028-create-entity-audit-tables.sql +45 -0
  4. package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
  5. package/db/migrations/030-case-first-memory-substrate.sql +901 -0
  6. package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
  7. package/dist/canonicalize.d.ts +38 -0
  8. package/dist/canonicalize.js +106 -0
  9. package/dist/cases/case-links.d.ts +116 -0
  10. package/dist/cases/case-links.js +794 -0
  11. package/dist/cases/composition-overrides.d.ts +90 -0
  12. package/dist/cases/composition-overrides.js +724 -0
  13. package/dist/cases/corrections.d.ts +156 -0
  14. package/dist/cases/corrections.js +1045 -0
  15. package/dist/cases/freshness.d.ts +57 -0
  16. package/dist/cases/freshness.js +283 -0
  17. package/dist/cases/live-state.d.ts +48 -0
  18. package/dist/cases/live-state.js +256 -0
  19. package/dist/cases/membership-explain.d.ts +56 -0
  20. package/dist/cases/membership-explain.js +140 -0
  21. package/dist/cases/membership-matcher.d.ts +23 -0
  22. package/dist/cases/membership-matcher.js +193 -0
  23. package/dist/cases/merge-split.d.ts +61 -0
  24. package/dist/cases/merge-split.js +368 -0
  25. package/dist/cases/role-inference.d.ts +10 -0
  26. package/dist/cases/role-inference.js +85 -0
  27. package/dist/cases/search-rollup.d.ts +23 -0
  28. package/dist/cases/search-rollup.js +225 -0
  29. package/dist/cases/sqlite-transaction.d.ts +10 -0
  30. package/dist/cases/sqlite-transaction.js +62 -0
  31. package/dist/cases/store.d.ts +74 -0
  32. package/dist/cases/store.js +816 -0
  33. package/dist/cases/target-ref.d.ts +25 -0
  34. package/dist/cases/target-ref.js +22 -0
  35. package/dist/cases/timeline-range.d.ts +6 -0
  36. package/dist/cases/timeline-range.js +322 -0
  37. package/dist/cases/tombstone-sweeper.d.ts +13 -0
  38. package/dist/cases/tombstone-sweeper.js +128 -0
  39. package/dist/cases/types.d.ts +244 -0
  40. package/dist/cases/types.js +47 -0
  41. package/dist/cases/wiki-page-index.d.ts +50 -0
  42. package/dist/cases/wiki-page-index.js +350 -0
  43. package/dist/connectors/event-index.d.ts +44 -0
  44. package/dist/connectors/event-index.js +366 -0
  45. package/dist/connectors/types.d.ts +69 -0
  46. package/dist/connectors/types.js +3 -0
  47. package/dist/db-manager.d.ts +4 -0
  48. package/dist/db-manager.js +10 -3
  49. package/dist/decision-formatter.d.ts +2 -0
  50. package/dist/decision-formatter.js +11 -8
  51. package/dist/embedding-server/mobile/daemon.d.ts +1 -0
  52. package/dist/embedding-server/mobile/daemon.js +11 -1
  53. package/dist/entities/audit-metrics.d.ts +73 -0
  54. package/dist/entities/audit-metrics.js +128 -0
  55. package/dist/entities/candidate-generator.d.ts +9 -0
  56. package/dist/entities/candidate-generator.js +436 -0
  57. package/dist/entities/entity-impact.d.ts +41 -0
  58. package/dist/entities/entity-impact.js +140 -0
  59. package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
  60. package/dist/entities/entity-linked-decision-counts.js +29 -0
  61. package/dist/entities/entity-list.d.ts +23 -0
  62. package/dist/entities/entity-list.js +99 -0
  63. package/dist/entities/entity-orphan-list.d.ts +26 -0
  64. package/dist/entities/entity-orphan-list.js +91 -0
  65. package/dist/entities/entity-search.d.ts +21 -0
  66. package/dist/entities/entity-search.js +150 -0
  67. package/dist/entities/errors.d.ts +65 -0
  68. package/dist/entities/errors.js +124 -0
  69. package/dist/entities/exact-merge-backfill.d.ts +11 -0
  70. package/dist/entities/exact-merge-backfill.js +161 -0
  71. package/dist/entities/lineage-backfill.d.ts +12 -0
  72. package/dist/entities/lineage-backfill.js +101 -0
  73. package/dist/entities/lineage-store.d.ts +51 -0
  74. package/dist/entities/lineage-store.js +308 -0
  75. package/dist/entities/normalization.d.ts +14 -0
  76. package/dist/entities/normalization.js +63 -0
  77. package/dist/entities/policy-store.d.ts +17 -0
  78. package/dist/entities/policy-store.js +263 -0
  79. package/dist/entities/policy-types.d.ts +72 -0
  80. package/dist/entities/policy-types.js +59 -0
  81. package/dist/entities/projection.d.ts +8 -0
  82. package/dist/entities/projection.js +82 -0
  83. package/dist/entities/provenance-query.d.ts +33 -0
  84. package/dist/entities/provenance-query.js +167 -0
  85. package/dist/entities/read-identity.d.ts +31 -0
  86. package/dist/entities/read-identity.js +264 -0
  87. package/dist/entities/recall-bridge.d.ts +5 -0
  88. package/dist/entities/recall-bridge.js +171 -0
  89. package/dist/entities/resolution-engine.d.ts +15 -0
  90. package/dist/entities/resolution-engine.js +69 -0
  91. package/dist/entities/rollback-preview.d.ts +39 -0
  92. package/dist/entities/rollback-preview.js +235 -0
  93. package/dist/entities/source-locator.d.ts +3 -0
  94. package/dist/entities/source-locator.js +32 -0
  95. package/dist/entities/store.d.ts +75 -0
  96. package/dist/entities/store.js +465 -0
  97. package/dist/entities/types.d.ts +162 -0
  98. package/dist/entities/types.js +30 -0
  99. package/dist/index.d.ts +47 -2
  100. package/dist/index.js +66 -25
  101. package/dist/mama-api.d.ts +22 -1
  102. package/dist/mama-api.js +272 -64
  103. package/dist/memory/api.d.ts +28 -1
  104. package/dist/memory/api.js +293 -31
  105. package/dist/memory/types.d.ts +11 -2
  106. package/dist/search/feedback-store.d.ts +59 -0
  107. package/dist/search/feedback-store.js +211 -0
  108. package/dist/search/question-type.d.ts +5 -0
  109. package/dist/search/question-type.js +41 -0
  110. package/dist/search/ranker-features.d.ts +23 -0
  111. package/dist/search/ranker-features.js +104 -0
  112. package/dist/search/ranker-rescore.d.ts +31 -0
  113. package/dist/search/ranker-rescore.js +226 -0
  114. package/dist/search/ranker-trainer.d.ts +94 -0
  115. package/dist/search/ranker-trainer.js +860 -0
  116. package/dist/test-utils.d.ts +14 -0
  117. package/dist/test-utils.js +52 -0
  118. package/package.json +21 -3
@@ -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
- adapter.transaction(() => {
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
- INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
398
- VALUES (?, ?, ?)
519
+ UPDATE decisions
520
+ SET kind = ?, status = ?, summary = ?, is_static = ?, trust_context = ?, updated_at = ?
521
+ WHERE id = ?
399
522
  `)
400
- .run(id, scopeId, isPrimary ? 1 : 0);
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(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
406
- .run(id, now, edge.to_id);
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
- adapter
409
- .prepare(`
410
- INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
411
- VALUES (?, ?, ?, ?, ?, ?)
412
- `)
413
- .run(id, edge.to_id, edge.type, edge.reason ?? null, 1.0, now);
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 { success: true, id };
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) {
@@ -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