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.
- package/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- 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
|
+
}
|