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,415 @@
1
+ /**
2
+ * Progressive Retriever
3
+ * Implements 3-layer progressive disclosure for token-efficient search
4
+ */
5
+
6
+ import type { EventStore } from './event-store.js';
7
+ import type { VectorStore } from './vector-store.js';
8
+ import type { Embedder } from './embedder.js';
9
+ import type {
10
+ SearchIndexItem,
11
+ TimelineItem,
12
+ FullDetail,
13
+ ProgressiveSearchResult,
14
+ ProgressiveDisclosureConfig,
15
+ MemoryEvent,
16
+ Citation
17
+ } from './types.js';
18
+ import { generateCitationId } from './citation-generator.js';
19
+
20
+ export interface SmartSearchOptions {
21
+ topK?: number;
22
+ minScore?: number;
23
+ maxTotalTokens?: number;
24
+ filter?: {
25
+ sessionId?: string;
26
+ eventType?: string;
27
+ };
28
+ }
29
+
30
+ interface ExpansionDecision {
31
+ expand: boolean;
32
+ expandTimeline?: boolean;
33
+ expandDetails?: boolean;
34
+ ids?: string[];
35
+ reason: string;
36
+ }
37
+
38
+ const DEFAULT_CONFIG: ProgressiveDisclosureConfig = {
39
+ enabled: true,
40
+ layer1: {
41
+ topK: 10,
42
+ minScore: 0.7
43
+ },
44
+ autoExpand: {
45
+ enabled: true,
46
+ highConfidenceThreshold: 0.92,
47
+ scoreGapThreshold: 0.1,
48
+ maxAutoExpandCount: 3
49
+ },
50
+ tokenBudget: {
51
+ maxTotalTokens: 2000,
52
+ layer1PerItem: 50,
53
+ layer2PerItem: 40,
54
+ layer3PerItem: 500
55
+ }
56
+ };
57
+
58
+ export class ProgressiveRetriever {
59
+ private config: ProgressiveDisclosureConfig;
60
+
61
+ constructor(
62
+ private eventStore: EventStore,
63
+ private vectorStore: VectorStore,
64
+ private embedder: Embedder,
65
+ config?: Partial<ProgressiveDisclosureConfig>
66
+ ) {
67
+ this.config = { ...DEFAULT_CONFIG, ...config };
68
+ }
69
+
70
+ /**
71
+ * Layer 1: Search Index (lightweight, ~50-100 tokens per result)
72
+ */
73
+ async searchIndex(
74
+ query: string,
75
+ options?: { topK?: number; filter?: SmartSearchOptions['filter'] }
76
+ ): Promise<SearchIndexItem[]> {
77
+ const topK = options?.topK ?? this.config.layer1.topK;
78
+
79
+ // Generate query embedding
80
+ const queryEmbedding = await this.embedder.embed(query);
81
+
82
+ // Search vector store
83
+ const vectorResults = await this.vectorStore.search(queryEmbedding.vector, {
84
+ limit: topK,
85
+ minScore: this.config.layer1.minScore,
86
+ sessionId: options?.filter?.sessionId
87
+ });
88
+
89
+ // Convert to index items with summaries
90
+ return vectorResults.map(r => ({
91
+ id: r.eventId,
92
+ summary: this.generateSummary(r.content),
93
+ score: r.score,
94
+ type: r.eventType as SearchIndexItem['type'],
95
+ timestamp: new Date(r.timestamp),
96
+ sessionId: r.sessionId
97
+ }));
98
+ }
99
+
100
+ /**
101
+ * Layer 2: Timeline (context around results, ~200 tokens)
102
+ */
103
+ async getTimeline(
104
+ targetIds: string[],
105
+ options?: { windowSize?: number }
106
+ ): Promise<TimelineItem[]> {
107
+ const windowSize = options?.windowSize ?? 3;
108
+ const items: TimelineItem[] = [];
109
+ const seenIds = new Set<string>();
110
+
111
+ for (const targetId of targetIds) {
112
+ const event = await this.eventStore.getEvent(targetId);
113
+ if (!event) continue;
114
+
115
+ // Get surrounding events from same session
116
+ const sessionEvents = await this.eventStore.getSessionEvents(event.sessionId);
117
+ const eventIndex = sessionEvents.findIndex(e => e.id === targetId);
118
+
119
+ if (eventIndex === -1) continue;
120
+
121
+ const start = Math.max(0, eventIndex - windowSize);
122
+ const end = Math.min(sessionEvents.length, eventIndex + windowSize + 1);
123
+
124
+ for (let i = start; i < end; i++) {
125
+ const e = sessionEvents[i];
126
+ if (seenIds.has(e.id)) continue;
127
+ seenIds.add(e.id);
128
+
129
+ items.push({
130
+ id: e.id,
131
+ timestamp: e.timestamp,
132
+ type: e.eventType as TimelineItem['type'],
133
+ preview: this.generatePreview(e.content),
134
+ isTarget: e.id === targetId
135
+ });
136
+ }
137
+ }
138
+
139
+ // Sort by timestamp
140
+ return items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
141
+ }
142
+
143
+ /**
144
+ * Layer 3: Full Details (complete content, ~500-1000 tokens per result)
145
+ */
146
+ async getDetails(ids: string[]): Promise<FullDetail[]> {
147
+ const details: FullDetail[] = [];
148
+
149
+ for (const id of ids) {
150
+ const event = await this.eventStore.getEvent(id);
151
+ if (!event) continue;
152
+
153
+ const citationId = generateCitationId(event.id);
154
+
155
+ details.push({
156
+ id: event.id,
157
+ content: event.content,
158
+ type: event.eventType as FullDetail['type'],
159
+ timestamp: event.timestamp,
160
+ sessionId: event.sessionId,
161
+ citationId,
162
+ metadata: this.extractMetadata(event)
163
+ });
164
+ }
165
+
166
+ return details;
167
+ }
168
+
169
+ /**
170
+ * Smart Search: Progressive search with auto-expansion
171
+ */
172
+ async smartSearch(
173
+ query: string,
174
+ options?: SmartSearchOptions
175
+ ): Promise<ProgressiveSearchResult> {
176
+ const config = { ...this.config };
177
+ if (options?.maxTotalTokens) {
178
+ config.tokenBudget.maxTotalTokens = options.maxTotalTokens;
179
+ }
180
+
181
+ // Layer 1: Always execute
182
+ const index = await this.searchIndex(query, {
183
+ topK: options?.topK ?? config.layer1.topK,
184
+ filter: options?.filter
185
+ });
186
+
187
+ const result: ProgressiveSearchResult = {
188
+ index,
189
+ meta: {
190
+ totalMatches: index.length,
191
+ expandedCount: 0,
192
+ estimatedTokens: this.estimateTokens(index, 'layer1')
193
+ }
194
+ };
195
+
196
+ // Auto-expansion decision
197
+ if (config.autoExpand.enabled) {
198
+ const decision = this.shouldAutoExpand(index, config);
199
+
200
+ if (decision.expand && decision.ids) {
201
+ // Expand timeline
202
+ if (decision.expandTimeline) {
203
+ result.timeline = await this.getTimeline(decision.ids);
204
+ result.meta.estimatedTokens += this.estimateTokens(result.timeline, 'layer2');
205
+ }
206
+
207
+ // Expand details (if budget allows)
208
+ if (decision.expandDetails) {
209
+ const remainingBudget = config.tokenBudget.maxTotalTokens - result.meta.estimatedTokens;
210
+ const idsToExpand = this.selectWithinBudget(
211
+ decision.ids,
212
+ remainingBudget,
213
+ config.tokenBudget.layer3PerItem
214
+ );
215
+
216
+ if (idsToExpand.length > 0) {
217
+ result.details = await this.getDetails(idsToExpand);
218
+ result.meta.expandedCount = idsToExpand.length;
219
+ result.meta.estimatedTokens += result.details.reduce(
220
+ (sum, d) => sum + this.estimateTokensForText(d.content),
221
+ 0
222
+ );
223
+ }
224
+ }
225
+
226
+ result.meta.expansionReason = decision.reason;
227
+ } else {
228
+ result.meta.expansionReason = decision.reason;
229
+ }
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * Determine whether to auto-expand results
237
+ */
238
+ private shouldAutoExpand(
239
+ results: SearchIndexItem[],
240
+ config: ProgressiveDisclosureConfig
241
+ ): ExpansionDecision {
242
+ if (results.length === 0) {
243
+ return { expand: false, reason: 'no_results' };
244
+ }
245
+
246
+ const topScore = results[0].score;
247
+
248
+ // Rule 1: High confidence single result
249
+ if (topScore >= config.autoExpand.highConfidenceThreshold && results.length === 1) {
250
+ return {
251
+ expand: true,
252
+ expandTimeline: true,
253
+ expandDetails: true,
254
+ ids: [results[0].id],
255
+ reason: 'high_confidence_single'
256
+ };
257
+ }
258
+
259
+ // Rule 2: Clear winner with score gap
260
+ if (results.length >= 2) {
261
+ const gap = results[0].score - results[1].score;
262
+ if (topScore >= 0.85 && gap >= config.autoExpand.scoreGapThreshold) {
263
+ return {
264
+ expand: true,
265
+ expandTimeline: true,
266
+ expandDetails: true,
267
+ ids: [results[0].id],
268
+ reason: 'clear_winner'
269
+ };
270
+ }
271
+ }
272
+
273
+ // Rule 3: Multiple high scores → timeline only
274
+ const highScoreCount = results.filter(r => r.score >= 0.8).length;
275
+ if (highScoreCount >= 3) {
276
+ return {
277
+ expand: true,
278
+ expandTimeline: true,
279
+ expandDetails: false,
280
+ ids: results.slice(0, 3).map(r => r.id),
281
+ reason: 'ambiguous_multiple_high'
282
+ };
283
+ }
284
+
285
+ // Rule 4: Low confidence
286
+ if (topScore < config.layer1.minScore) {
287
+ return { expand: false, reason: 'low_confidence' };
288
+ }
289
+
290
+ return { expand: false, reason: 'no_expansion_rule_matched' };
291
+ }
292
+
293
+ /**
294
+ * Select IDs that fit within token budget
295
+ */
296
+ private selectWithinBudget(
297
+ ids: string[],
298
+ budget: number,
299
+ perItemTokens: number
300
+ ): string[] {
301
+ const maxItems = Math.floor(budget / perItemTokens);
302
+ return ids.slice(0, Math.max(0, maxItems));
303
+ }
304
+
305
+ /**
306
+ * Generate a short summary for Layer 1
307
+ */
308
+ private generateSummary(content: string, maxLength: number = 100): string {
309
+ // Remove code blocks
310
+ const withoutCode = content.replace(/```[\s\S]*?```/g, '[code]');
311
+
312
+ // Extract first sentence
313
+ const firstSentence = withoutCode.match(/^[^.!?]+[.!?]/)?.[0] || '';
314
+
315
+ if (firstSentence.length <= maxLength) {
316
+ return firstSentence.trim();
317
+ }
318
+
319
+ // Truncate at word boundary
320
+ return withoutCode.slice(0, maxLength).replace(/\s+\S*$/, '') + '...';
321
+ }
322
+
323
+ /**
324
+ * Generate a preview for Layer 2
325
+ */
326
+ private generatePreview(content: string, maxLength: number = 200): string {
327
+ // Summarize code blocks
328
+ const withCodeSummary = content.replace(
329
+ /```(\w+)[\s\S]*?```/g,
330
+ (_, lang) => `[${lang} code]`
331
+ );
332
+
333
+ // Collapse whitespace
334
+ const singleLine = withCodeSummary.replace(/\n+/g, ' ').trim();
335
+
336
+ if (singleLine.length <= maxLength) {
337
+ return singleLine;
338
+ }
339
+
340
+ return singleLine.slice(0, maxLength).replace(/\s+\S*$/, '') + '...';
341
+ }
342
+
343
+ /**
344
+ * Extract metadata from event
345
+ */
346
+ private extractMetadata(event: MemoryEvent): FullDetail['metadata'] {
347
+ const content = event.content;
348
+
349
+ return {
350
+ tokenCount: this.estimateTokensForText(content),
351
+ hasCode: /```[\s\S]*?```/.test(content),
352
+ files: this.extractFiles(content),
353
+ tools: this.extractTools(content)
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Extract file paths from content
359
+ */
360
+ private extractFiles(content: string): string[] | undefined {
361
+ const filePattern = /(?:\/[\w.-]+)+\.\w+/g;
362
+ const matches = content.match(filePattern);
363
+ return matches ? [...new Set(matches)] : undefined;
364
+ }
365
+
366
+ /**
367
+ * Extract tool names from content
368
+ */
369
+ private extractTools(content: string): string[] | undefined {
370
+ const toolPattern = /\b(Read|Write|Edit|Bash|Grep|Glob|WebFetch|WebSearch)\b/g;
371
+ const matches = content.match(toolPattern);
372
+ return matches ? [...new Set(matches)] : undefined;
373
+ }
374
+
375
+ /**
376
+ * Estimate tokens for a layer
377
+ */
378
+ private estimateTokens(
379
+ items: unknown[],
380
+ layer: 'layer1' | 'layer2' | 'layer3'
381
+ ): number {
382
+ const config = this.config.tokenBudget;
383
+
384
+ switch (layer) {
385
+ case 'layer1':
386
+ return items.length * config.layer1PerItem;
387
+ case 'layer2':
388
+ return items.length * config.layer2PerItem;
389
+ case 'layer3':
390
+ return (items as FullDetail[]).reduce(
391
+ (sum, item) => sum + this.estimateTokensForText(item.content),
392
+ 0
393
+ );
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Estimate tokens for text (~4 chars per token)
399
+ */
400
+ private estimateTokensForText(text: string): number {
401
+ return Math.ceil(text.length / 4);
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Create a progressive retriever instance
407
+ */
408
+ export function createProgressiveRetriever(
409
+ eventStore: EventStore,
410
+ vectorStore: VectorStore,
411
+ embedder: Embedder,
412
+ config?: Partial<ProgressiveDisclosureConfig>
413
+ ): ProgressiveRetriever {
414
+ return new ProgressiveRetriever(eventStore, vectorStore, embedder, config);
415
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Memory Retriever - Unified retrieval interface
3
+ * Combines vector search, event store lookups, and matching
4
+ */
5
+
6
+ import { EventStore } from './event-store.js';
7
+ import { VectorStore, SearchResult } from './vector-store.js';
8
+ import { Embedder } from './embedder.js';
9
+ import { Matcher } from './matcher.js';
10
+ import type { MemoryEvent, MatchResult, Config } from './types.js';
11
+
12
+ export interface RetrievalOptions {
13
+ topK: number;
14
+ minScore: number;
15
+ sessionId?: string;
16
+ maxTokens: number;
17
+ includeSessionContext: boolean;
18
+ }
19
+
20
+ export interface RetrievalResult {
21
+ memories: MemoryWithContext[];
22
+ matchResult: MatchResult;
23
+ totalTokens: number;
24
+ context: string;
25
+ }
26
+
27
+ export interface MemoryWithContext {
28
+ event: MemoryEvent;
29
+ score: number;
30
+ sessionContext?: string;
31
+ }
32
+
33
+ const DEFAULT_OPTIONS: RetrievalOptions = {
34
+ topK: 5,
35
+ minScore: 0.7,
36
+ maxTokens: 2000,
37
+ includeSessionContext: true
38
+ };
39
+
40
+ export class Retriever {
41
+ private readonly eventStore: EventStore;
42
+ private readonly vectorStore: VectorStore;
43
+ private readonly embedder: Embedder;
44
+ private readonly matcher: Matcher;
45
+
46
+ constructor(
47
+ eventStore: EventStore,
48
+ vectorStore: VectorStore,
49
+ embedder: Embedder,
50
+ matcher: Matcher
51
+ ) {
52
+ this.eventStore = eventStore;
53
+ this.vectorStore = vectorStore;
54
+ this.embedder = embedder;
55
+ this.matcher = matcher;
56
+ }
57
+
58
+ /**
59
+ * Retrieve relevant memories for a query
60
+ */
61
+ async retrieve(
62
+ query: string,
63
+ options: Partial<RetrievalOptions> = {}
64
+ ): Promise<RetrievalResult> {
65
+ const opts = { ...DEFAULT_OPTIONS, ...options };
66
+
67
+ // Generate query embedding
68
+ const queryEmbedding = await this.embedder.embed(query);
69
+
70
+ // Search vector store
71
+ const searchResults = await this.vectorStore.search(queryEmbedding.vector, {
72
+ limit: opts.topK * 2, // Get extra for filtering
73
+ minScore: opts.minScore,
74
+ sessionId: opts.sessionId
75
+ });
76
+
77
+ // Get match result using AXIOMMIND matcher
78
+ const matchResult = this.matcher.matchSearchResults(
79
+ searchResults,
80
+ (eventId) => this.getEventAgeDays(eventId)
81
+ );
82
+
83
+ // Enrich results with full event data and session context
84
+ const memories = await this.enrichResults(searchResults.slice(0, opts.topK), opts);
85
+
86
+ // Build context string
87
+ const context = this.buildContext(memories, opts.maxTokens);
88
+
89
+ return {
90
+ memories,
91
+ matchResult,
92
+ totalTokens: this.estimateTokens(context),
93
+ context
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Retrieve memories from a specific session
99
+ */
100
+ async retrieveFromSession(sessionId: string): Promise<MemoryEvent[]> {
101
+ return this.eventStore.getSessionEvents(sessionId);
102
+ }
103
+
104
+ /**
105
+ * Get recent memories across all sessions
106
+ */
107
+ async retrieveRecent(limit: number = 100): Promise<MemoryEvent[]> {
108
+ return this.eventStore.getRecentEvents(limit);
109
+ }
110
+
111
+ /**
112
+ * Enrich search results with full event data
113
+ */
114
+ private async enrichResults(
115
+ results: SearchResult[],
116
+ options: RetrievalOptions
117
+ ): Promise<MemoryWithContext[]> {
118
+ const memories: MemoryWithContext[] = [];
119
+
120
+ for (const result of results) {
121
+ const event = await this.eventStore.getEvent(result.eventId);
122
+ if (!event) continue;
123
+
124
+ let sessionContext: string | undefined;
125
+ if (options.includeSessionContext) {
126
+ sessionContext = await this.getSessionContext(event.sessionId, event.id);
127
+ }
128
+
129
+ memories.push({
130
+ event,
131
+ score: result.score,
132
+ sessionContext
133
+ });
134
+ }
135
+
136
+ return memories;
137
+ }
138
+
139
+ /**
140
+ * Get surrounding context from the same session
141
+ */
142
+ private async getSessionContext(
143
+ sessionId: string,
144
+ eventId: string
145
+ ): Promise<string | undefined> {
146
+ const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
147
+
148
+ // Find the event index
149
+ const eventIndex = sessionEvents.findIndex(e => e.id === eventId);
150
+ if (eventIndex === -1) return undefined;
151
+
152
+ // Get 1 event before and after for context
153
+ const start = Math.max(0, eventIndex - 1);
154
+ const end = Math.min(sessionEvents.length, eventIndex + 2);
155
+ const contextEvents = sessionEvents.slice(start, end);
156
+
157
+ if (contextEvents.length <= 1) return undefined;
158
+
159
+ return contextEvents
160
+ .filter(e => e.id !== eventId)
161
+ .map(e => `[${e.eventType}]: ${e.content.slice(0, 200)}...`)
162
+ .join('\n');
163
+ }
164
+
165
+ /**
166
+ * Build context string from memories (respecting token limit)
167
+ */
168
+ private buildContext(memories: MemoryWithContext[], maxTokens: number): string {
169
+ const parts: string[] = [];
170
+ let currentTokens = 0;
171
+
172
+ for (const memory of memories) {
173
+ const memoryText = this.formatMemory(memory);
174
+ const memoryTokens = this.estimateTokens(memoryText);
175
+
176
+ if (currentTokens + memoryTokens > maxTokens) {
177
+ break;
178
+ }
179
+
180
+ parts.push(memoryText);
181
+ currentTokens += memoryTokens;
182
+ }
183
+
184
+ if (parts.length === 0) {
185
+ return '';
186
+ }
187
+
188
+ return `## Relevant Memories\n\n${parts.join('\n\n---\n\n')}`;
189
+ }
190
+
191
+ /**
192
+ * Format a single memory for context
193
+ */
194
+ private formatMemory(memory: MemoryWithContext): string {
195
+ const { event, score, sessionContext } = memory;
196
+ const date = event.timestamp.toISOString().split('T')[0];
197
+
198
+ let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})\n${event.content}`;
199
+
200
+ if (sessionContext) {
201
+ text += `\n\n_Context:_ ${sessionContext}`;
202
+ }
203
+
204
+ return text;
205
+ }
206
+
207
+ /**
208
+ * Estimate token count (rough approximation)
209
+ */
210
+ private estimateTokens(text: string): number {
211
+ // Rough estimate: ~4 characters per token
212
+ return Math.ceil(text.length / 4);
213
+ }
214
+
215
+ /**
216
+ * Get event age in days (for recency scoring)
217
+ */
218
+ private getEventAgeDays(eventId: string): number {
219
+ // This would ideally cache event timestamps
220
+ // For now, return 0 (assume recent)
221
+ return 0;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Create a retriever with default components
227
+ */
228
+ export function createRetriever(
229
+ eventStore: EventStore,
230
+ vectorStore: VectorStore,
231
+ embedder: Embedder,
232
+ matcher: Matcher
233
+ ): Retriever {
234
+ return new Retriever(eventStore, vectorStore, embedder, matcher);
235
+ }