claude-memory-layer 1.0.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 (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Memory Graduation Pipeline - AXIOMMIND L0→L1→L2→L3→L4
3
+ *
4
+ * L0: EventStore (raw events, append-only)
5
+ * L1: Structured JSON (session summaries, patterns)
6
+ * L2: Type Candidates (Idris2-inspired, validated schemas)
7
+ * L3: Verified Knowledge (cross-session validated)
8
+ * L4: Active/Searchable (indexed, readily available)
9
+ */
10
+
11
+ import { EventStore } from './event-store.js';
12
+ import type {
13
+ MemoryEvent,
14
+ MemoryLevel,
15
+ GraduationResult,
16
+ Insight,
17
+ InsightType
18
+ } from './types.js';
19
+
20
+ export interface GraduationCriteria {
21
+ minAccessCount: number;
22
+ minConfidence: number;
23
+ minCrossSessionRefs: number;
24
+ maxAgeDays: number;
25
+ }
26
+
27
+ export interface LevelCriteria {
28
+ L0toL1: GraduationCriteria;
29
+ L1toL2: GraduationCriteria;
30
+ L2toL3: GraduationCriteria;
31
+ L3toL4: GraduationCriteria;
32
+ }
33
+
34
+ const DEFAULT_CRITERIA: LevelCriteria = {
35
+ L0toL1: {
36
+ minAccessCount: 1,
37
+ minConfidence: 0.5,
38
+ minCrossSessionRefs: 0,
39
+ maxAgeDays: 30
40
+ },
41
+ L1toL2: {
42
+ minAccessCount: 3,
43
+ minConfidence: 0.7,
44
+ minCrossSessionRefs: 1,
45
+ maxAgeDays: 60
46
+ },
47
+ L2toL3: {
48
+ minAccessCount: 5,
49
+ minConfidence: 0.85,
50
+ minCrossSessionRefs: 2,
51
+ maxAgeDays: 90
52
+ },
53
+ L3toL4: {
54
+ minAccessCount: 10,
55
+ minConfidence: 0.92,
56
+ minCrossSessionRefs: 3,
57
+ maxAgeDays: 180
58
+ }
59
+ };
60
+
61
+ export interface EventMetrics {
62
+ eventId: string;
63
+ accessCount: number;
64
+ lastAccessed: Date;
65
+ crossSessionRefs: number;
66
+ confidence: number;
67
+ }
68
+
69
+ export class GraduationPipeline {
70
+ private readonly eventStore: EventStore;
71
+ private readonly criteria: LevelCriteria;
72
+ private readonly metrics: Map<string, EventMetrics> = new Map();
73
+
74
+ constructor(
75
+ eventStore: EventStore,
76
+ criteria: Partial<LevelCriteria> = {}
77
+ ) {
78
+ this.eventStore = eventStore;
79
+ this.criteria = {
80
+ L0toL1: { ...DEFAULT_CRITERIA.L0toL1, ...criteria.L0toL1 },
81
+ L1toL2: { ...DEFAULT_CRITERIA.L1toL2, ...criteria.L1toL2 },
82
+ L2toL3: { ...DEFAULT_CRITERIA.L2toL3, ...criteria.L2toL3 },
83
+ L3toL4: { ...DEFAULT_CRITERIA.L3toL4, ...criteria.L3toL4 }
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Record an access to an event (used for graduation scoring)
89
+ */
90
+ recordAccess(eventId: string, fromSessionId: string, confidence: number = 1.0): void {
91
+ const existing = this.metrics.get(eventId);
92
+
93
+ if (existing) {
94
+ existing.accessCount++;
95
+ existing.lastAccessed = new Date();
96
+ existing.confidence = Math.max(existing.confidence, confidence);
97
+ // Track cross-session references
98
+ // This would need more sophisticated tracking in production
99
+ } else {
100
+ this.metrics.set(eventId, {
101
+ eventId,
102
+ accessCount: 1,
103
+ lastAccessed: new Date(),
104
+ crossSessionRefs: 0,
105
+ confidence
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Evaluate if an event should graduate to the next level
112
+ */
113
+ async evaluateGraduation(eventId: string, currentLevel: MemoryLevel): Promise<GraduationResult> {
114
+ const metrics = this.metrics.get(eventId);
115
+
116
+ if (!metrics) {
117
+ return {
118
+ eventId,
119
+ fromLevel: currentLevel,
120
+ toLevel: currentLevel,
121
+ success: false,
122
+ reason: 'No metrics available for event'
123
+ };
124
+ }
125
+
126
+ const nextLevel = this.getNextLevel(currentLevel);
127
+ if (!nextLevel) {
128
+ return {
129
+ eventId,
130
+ fromLevel: currentLevel,
131
+ toLevel: currentLevel,
132
+ success: false,
133
+ reason: 'Already at maximum level'
134
+ };
135
+ }
136
+
137
+ const criteria = this.getCriteria(currentLevel, nextLevel);
138
+ const evaluation = this.checkCriteria(metrics, criteria);
139
+
140
+ if (evaluation.passed) {
141
+ // Update level in event store
142
+ await this.eventStore.updateMemoryLevel(eventId, nextLevel);
143
+
144
+ return {
145
+ eventId,
146
+ fromLevel: currentLevel,
147
+ toLevel: nextLevel,
148
+ success: true
149
+ };
150
+ }
151
+
152
+ return {
153
+ eventId,
154
+ fromLevel: currentLevel,
155
+ toLevel: currentLevel,
156
+ success: false,
157
+ reason: evaluation.reason
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Run graduation evaluation for all events at a given level
163
+ */
164
+ async graduateBatch(level: MemoryLevel): Promise<GraduationResult[]> {
165
+ const results: GraduationResult[] = [];
166
+
167
+ for (const [eventId, metrics] of this.metrics) {
168
+ const result = await this.evaluateGraduation(eventId, level);
169
+ results.push(result);
170
+ }
171
+
172
+ return results;
173
+ }
174
+
175
+ /**
176
+ * Extract insights from graduated events (L1+)
177
+ */
178
+ extractInsights(events: MemoryEvent[]): Insight[] {
179
+ const insights: Insight[] = [];
180
+
181
+ // Pattern detection: Look for repeated themes
182
+ const patterns = this.detectPatterns(events);
183
+ for (const pattern of patterns) {
184
+ insights.push({
185
+ id: crypto.randomUUID(),
186
+ insightType: 'pattern',
187
+ content: pattern.description,
188
+ canonicalKey: pattern.key,
189
+ confidence: pattern.confidence,
190
+ sourceEvents: pattern.eventIds,
191
+ createdAt: new Date(),
192
+ lastUpdated: new Date()
193
+ });
194
+ }
195
+
196
+ // Preference detection: Look for user preferences
197
+ const preferences = this.detectPreferences(events);
198
+ for (const pref of preferences) {
199
+ insights.push({
200
+ id: crypto.randomUUID(),
201
+ insightType: 'preference',
202
+ content: pref.description,
203
+ canonicalKey: pref.key,
204
+ confidence: pref.confidence,
205
+ sourceEvents: pref.eventIds,
206
+ createdAt: new Date(),
207
+ lastUpdated: new Date()
208
+ });
209
+ }
210
+
211
+ return insights;
212
+ }
213
+
214
+ /**
215
+ * Get the next level in the graduation pipeline
216
+ */
217
+ private getNextLevel(current: MemoryLevel): MemoryLevel | null {
218
+ const levels: MemoryLevel[] = ['L0', 'L1', 'L2', 'L3', 'L4'];
219
+ const currentIndex = levels.indexOf(current);
220
+
221
+ if (currentIndex === -1 || currentIndex >= levels.length - 1) {
222
+ return null;
223
+ }
224
+
225
+ return levels[currentIndex + 1];
226
+ }
227
+
228
+ /**
229
+ * Get criteria for level transition
230
+ */
231
+ private getCriteria(from: MemoryLevel, to: MemoryLevel): GraduationCriteria {
232
+ const key = `${from}to${to}` as keyof LevelCriteria;
233
+ return this.criteria[key] || DEFAULT_CRITERIA.L0toL1;
234
+ }
235
+
236
+ /**
237
+ * Check if metrics meet criteria
238
+ */
239
+ private checkCriteria(
240
+ metrics: EventMetrics,
241
+ criteria: GraduationCriteria
242
+ ): { passed: boolean; reason?: string } {
243
+ if (metrics.accessCount < criteria.minAccessCount) {
244
+ return {
245
+ passed: false,
246
+ reason: `Access count ${metrics.accessCount} < ${criteria.minAccessCount}`
247
+ };
248
+ }
249
+
250
+ if (metrics.confidence < criteria.minConfidence) {
251
+ return {
252
+ passed: false,
253
+ reason: `Confidence ${metrics.confidence} < ${criteria.minConfidence}`
254
+ };
255
+ }
256
+
257
+ if (metrics.crossSessionRefs < criteria.minCrossSessionRefs) {
258
+ return {
259
+ passed: false,
260
+ reason: `Cross-session refs ${metrics.crossSessionRefs} < ${criteria.minCrossSessionRefs}`
261
+ };
262
+ }
263
+
264
+ const ageDays = (Date.now() - metrics.lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
265
+ if (ageDays > criteria.maxAgeDays) {
266
+ return {
267
+ passed: false,
268
+ reason: `Event too old: ${ageDays.toFixed(1)} days > ${criteria.maxAgeDays}`
269
+ };
270
+ }
271
+
272
+ return { passed: true };
273
+ }
274
+
275
+ /**
276
+ * Detect patterns in events
277
+ */
278
+ private detectPatterns(events: MemoryEvent[]): Array<{
279
+ key: string;
280
+ description: string;
281
+ confidence: number;
282
+ eventIds: string[];
283
+ }> {
284
+ // Simple pattern detection: group by canonical key and look for repeats
285
+ const keyGroups = new Map<string, MemoryEvent[]>();
286
+
287
+ for (const event of events) {
288
+ const existing = keyGroups.get(event.canonicalKey) || [];
289
+ existing.push(event);
290
+ keyGroups.set(event.canonicalKey, existing);
291
+ }
292
+
293
+ const patterns: Array<{
294
+ key: string;
295
+ description: string;
296
+ confidence: number;
297
+ eventIds: string[];
298
+ }> = [];
299
+
300
+ for (const [key, groupEvents] of keyGroups) {
301
+ if (groupEvents.length >= 2) {
302
+ patterns.push({
303
+ key,
304
+ description: `Repeated topic: ${key.slice(0, 50)}`,
305
+ confidence: Math.min(1.0, groupEvents.length / 5),
306
+ eventIds: groupEvents.map(e => e.id)
307
+ });
308
+ }
309
+ }
310
+
311
+ return patterns;
312
+ }
313
+
314
+ /**
315
+ * Detect user preferences from events
316
+ */
317
+ private detectPreferences(events: MemoryEvent[]): Array<{
318
+ key: string;
319
+ description: string;
320
+ confidence: number;
321
+ eventIds: string[];
322
+ }> {
323
+ // Simple preference detection: look for keywords
324
+ const preferenceKeywords = ['prefer', 'like', 'want', 'always', 'never', 'favorite'];
325
+ const preferences: Array<{
326
+ key: string;
327
+ description: string;
328
+ confidence: number;
329
+ eventIds: string[];
330
+ }> = [];
331
+
332
+ for (const event of events) {
333
+ if (event.eventType !== 'user_prompt') continue;
334
+
335
+ const lowerContent = event.content.toLowerCase();
336
+ for (const keyword of preferenceKeywords) {
337
+ if (lowerContent.includes(keyword)) {
338
+ preferences.push({
339
+ key: `preference_${keyword}_${event.id.slice(0, 8)}`,
340
+ description: `User preference: ${event.content.slice(0, 100)}`,
341
+ confidence: 0.7,
342
+ eventIds: [event.id]
343
+ });
344
+ break;
345
+ }
346
+ }
347
+ }
348
+
349
+ return preferences;
350
+ }
351
+
352
+ /**
353
+ * Get graduation statistics
354
+ */
355
+ async getStats(): Promise<{ level: string; count: number }[]> {
356
+ return this.eventStore.getLevelStats();
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Create graduation pipeline with default settings
362
+ */
363
+ export function createGraduationPipeline(eventStore: EventStore): GraduationPipeline {
364
+ return new GraduationPipeline(eventStore);
365
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Core module exports
3
+ * AXIOMMIND Memory Graduation Pipeline
4
+ */
5
+
6
+ // Types
7
+ export * from './types.js';
8
+
9
+ // Canonical Key (identity)
10
+ export * from './canonical-key.js';
11
+
12
+ // Storage
13
+ export * from './event-store.js';
14
+ export * from './entity-repo.js';
15
+ export * from './edge-repo.js';
16
+
17
+ // Vector
18
+ export * from './vector-store.js';
19
+ export * from './embedder.js';
20
+ export * from './vector-worker.js';
21
+ export * from './vector-outbox.js';
22
+
23
+ // Matching & Alignment
24
+ export * from './matcher.js';
25
+ export * from './evidence-aligner.js';
26
+
27
+ // Retrieval & Graduation
28
+ export * from './retriever.js';
29
+ export * from './graduation.js';
30
+
31
+ // Task Entity System
32
+ export * from './task/index.js';
@@ -0,0 +1,210 @@
1
+ /**
2
+ * AXIOMMIND Matcher - Weighted scoring with confidence classification
3
+ * Implements matching thresholds: high (≥0.92), suggested (≥0.75), none (<0.75)
4
+ */
5
+
6
+ import type {
7
+ MemoryEvent,
8
+ MemoryMatch,
9
+ MatchResult,
10
+ MatchConfidence,
11
+ MATCH_THRESHOLDS
12
+ } from './types.js';
13
+ import { SearchResult } from './vector-store.js';
14
+
15
+ export interface MatchWeights {
16
+ semanticSimilarity: number;
17
+ ftsScore: number;
18
+ recencyBonus: number;
19
+ statusWeight: number;
20
+ }
21
+
22
+ export interface MatcherConfig {
23
+ weights: MatchWeights;
24
+ minCombinedScore: number;
25
+ minGap: number;
26
+ suggestionThreshold: number;
27
+ }
28
+
29
+ const DEFAULT_CONFIG: MatcherConfig = {
30
+ weights: {
31
+ semanticSimilarity: 0.4,
32
+ ftsScore: 0.25,
33
+ recencyBonus: 0.2,
34
+ statusWeight: 0.15
35
+ },
36
+ minCombinedScore: 0.92,
37
+ minGap: 0.03,
38
+ suggestionThreshold: 0.75
39
+ };
40
+
41
+ export class Matcher {
42
+ private readonly config: MatcherConfig;
43
+
44
+ constructor(config: Partial<MatcherConfig> = {}) {
45
+ this.config = {
46
+ ...DEFAULT_CONFIG,
47
+ ...config,
48
+ weights: { ...DEFAULT_CONFIG.weights, ...config.weights }
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Calculate combined score using AXIOMMIND weighted formula
54
+ */
55
+ calculateCombinedScore(
56
+ semanticScore: number,
57
+ ftsScore: number = 0,
58
+ recencyDays: number = 0,
59
+ isActive: boolean = true
60
+ ): number {
61
+ const { weights } = this.config;
62
+
63
+ // Recency bonus: decays over 30 days
64
+ const recencyBonus = Math.max(0, 1 - recencyDays / 30);
65
+
66
+ // Status weight: active events get full weight
67
+ const statusMultiplier = isActive ? 1.0 : 0.7;
68
+
69
+ const combinedScore =
70
+ weights.semanticSimilarity * semanticScore +
71
+ weights.ftsScore * ftsScore +
72
+ weights.recencyBonus * recencyBonus +
73
+ weights.statusWeight * statusMultiplier;
74
+
75
+ return Math.min(1.0, combinedScore);
76
+ }
77
+
78
+ /**
79
+ * Classify match confidence based on AXIOMMIND thresholds
80
+ */
81
+ classifyConfidence(
82
+ topScore: number,
83
+ secondScore: number | null
84
+ ): MatchConfidence {
85
+ const { minCombinedScore, minGap, suggestionThreshold } = this.config;
86
+
87
+ // Calculate gap (infinity if no second match)
88
+ const gap = secondScore !== null ? topScore - secondScore : Infinity;
89
+
90
+ // High confidence: score ≥ 0.92 AND gap ≥ 0.03
91
+ if (topScore >= minCombinedScore && gap >= minGap) {
92
+ return 'high';
93
+ }
94
+
95
+ // Suggested: score ≥ 0.75
96
+ if (topScore >= suggestionThreshold) {
97
+ return 'suggested';
98
+ }
99
+
100
+ // No match
101
+ return 'none';
102
+ }
103
+
104
+ /**
105
+ * Match search results to find best memory
106
+ */
107
+ matchSearchResults(
108
+ results: SearchResult[],
109
+ getEventAge: (eventId: string) => number
110
+ ): MatchResult {
111
+ if (results.length === 0) {
112
+ return {
113
+ match: null,
114
+ confidence: 'none'
115
+ };
116
+ }
117
+
118
+ // Calculate combined scores
119
+ const scoredResults = results.map(result => {
120
+ const ageDays = getEventAge(result.eventId);
121
+ const combinedScore = this.calculateCombinedScore(
122
+ result.score,
123
+ 0, // FTS score - would need to be passed in
124
+ ageDays,
125
+ true // Assume active
126
+ );
127
+
128
+ return {
129
+ result,
130
+ combinedScore
131
+ };
132
+ });
133
+
134
+ // Sort by combined score
135
+ scoredResults.sort((a, b) => b.combinedScore - a.combinedScore);
136
+
137
+ const topResult = scoredResults[0];
138
+ const secondScore = scoredResults.length > 1 ? scoredResults[1].combinedScore : null;
139
+
140
+ // Classify confidence
141
+ const confidence = this.classifyConfidence(topResult.combinedScore, secondScore);
142
+
143
+ // Build match result
144
+ const match: MemoryMatch = {
145
+ event: {
146
+ id: topResult.result.eventId,
147
+ eventType: topResult.result.eventType as 'user_prompt' | 'agent_response' | 'session_summary',
148
+ sessionId: topResult.result.sessionId,
149
+ timestamp: new Date(topResult.result.timestamp),
150
+ content: topResult.result.content,
151
+ canonicalKey: '', // Would need to be fetched
152
+ dedupeKey: '' // Would need to be fetched
153
+ },
154
+ score: topResult.combinedScore
155
+ };
156
+
157
+ const gap = secondScore !== null ? topResult.combinedScore - secondScore : undefined;
158
+
159
+ // Build alternatives for suggested matches
160
+ const alternatives = confidence === 'suggested'
161
+ ? scoredResults.slice(1, 4).map(sr => ({
162
+ event: {
163
+ id: sr.result.eventId,
164
+ eventType: sr.result.eventType as 'user_prompt' | 'agent_response' | 'session_summary',
165
+ sessionId: sr.result.sessionId,
166
+ timestamp: new Date(sr.result.timestamp),
167
+ content: sr.result.content,
168
+ canonicalKey: '',
169
+ dedupeKey: ''
170
+ },
171
+ score: sr.combinedScore
172
+ }))
173
+ : undefined;
174
+
175
+ return {
176
+ match: confidence !== 'none' ? match : null,
177
+ confidence,
178
+ gap,
179
+ alternatives
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Calculate days between two dates
185
+ */
186
+ static calculateAgeDays(timestamp: Date): number {
187
+ const now = new Date();
188
+ const diffMs = now.getTime() - timestamp.getTime();
189
+ return diffMs / (1000 * 60 * 60 * 24);
190
+ }
191
+
192
+ /**
193
+ * Get current configuration
194
+ */
195
+ getConfig(): Readonly<MatcherConfig> {
196
+ return { ...this.config };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Default matcher instance
202
+ */
203
+ let defaultMatcher: Matcher | null = null;
204
+
205
+ export function getDefaultMatcher(): Matcher {
206
+ if (!defaultMatcher) {
207
+ defaultMatcher = new Matcher();
208
+ }
209
+ return defaultMatcher;
210
+ }