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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidation Worker
|
|
3
|
+
* Periodically consolidates working set into long-term memory
|
|
4
|
+
* Biomimetic: Simulates memory consolidation during sleep/idle periods
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
EndlessModeConfig,
|
|
9
|
+
MemoryEvent,
|
|
10
|
+
EventGroup,
|
|
11
|
+
WorkingSet
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { WorkingSetStore } from './working-set-store.js';
|
|
14
|
+
import { ConsolidatedStore } from './consolidated-store.js';
|
|
15
|
+
|
|
16
|
+
export class ConsolidationWorker {
|
|
17
|
+
private running = false;
|
|
18
|
+
private timeout: NodeJS.Timeout | null = null;
|
|
19
|
+
private lastActivity: Date = new Date();
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private workingSetStore: WorkingSetStore,
|
|
23
|
+
private consolidatedStore: ConsolidatedStore,
|
|
24
|
+
private config: EndlessModeConfig
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start the consolidation worker
|
|
29
|
+
*/
|
|
30
|
+
start(): void {
|
|
31
|
+
if (this.running) return;
|
|
32
|
+
this.running = true;
|
|
33
|
+
this.scheduleNext();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stop the consolidation worker
|
|
38
|
+
*/
|
|
39
|
+
stop(): void {
|
|
40
|
+
this.running = false;
|
|
41
|
+
if (this.timeout) {
|
|
42
|
+
clearTimeout(this.timeout);
|
|
43
|
+
this.timeout = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record activity (resets idle timer)
|
|
49
|
+
*/
|
|
50
|
+
recordActivity(): void {
|
|
51
|
+
this.lastActivity = new Date();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if currently running
|
|
56
|
+
*/
|
|
57
|
+
isRunning(): boolean {
|
|
58
|
+
return this.running;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Force a consolidation run (manual trigger)
|
|
63
|
+
*/
|
|
64
|
+
async forceRun(): Promise<number> {
|
|
65
|
+
return await this.consolidate();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Schedule the next consolidation check
|
|
70
|
+
*/
|
|
71
|
+
private scheduleNext(): void {
|
|
72
|
+
if (!this.running) return;
|
|
73
|
+
|
|
74
|
+
this.timeout = setTimeout(
|
|
75
|
+
() => this.run(),
|
|
76
|
+
this.config.consolidation.triggerIntervalMs
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run consolidation check
|
|
82
|
+
*/
|
|
83
|
+
private async run(): Promise<void> {
|
|
84
|
+
if (!this.running) return;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await this.checkAndConsolidate();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Consolidation error:', error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.scheduleNext();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check conditions and consolidate if needed
|
|
97
|
+
*/
|
|
98
|
+
private async checkAndConsolidate(): Promise<void> {
|
|
99
|
+
const workingSet = await this.workingSetStore.get();
|
|
100
|
+
|
|
101
|
+
if (!this.shouldConsolidate(workingSet)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await this.consolidate();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Perform consolidation
|
|
110
|
+
*/
|
|
111
|
+
private async consolidate(): Promise<number> {
|
|
112
|
+
const workingSet = await this.workingSetStore.get();
|
|
113
|
+
|
|
114
|
+
if (workingSet.recentEvents.length < 3) {
|
|
115
|
+
return 0; // Not enough events to consolidate
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Group events by topic
|
|
119
|
+
const groups = this.groupByTopic(workingSet.recentEvents);
|
|
120
|
+
let consolidatedCount = 0;
|
|
121
|
+
|
|
122
|
+
for (const group of groups) {
|
|
123
|
+
// Require minimum 3 events per group
|
|
124
|
+
if (group.events.length < 3) continue;
|
|
125
|
+
|
|
126
|
+
// Check if already consolidated
|
|
127
|
+
const eventIds = group.events.map(e => e.id);
|
|
128
|
+
const alreadyConsolidated = await this.consolidatedStore.isAlreadyConsolidated(eventIds);
|
|
129
|
+
if (alreadyConsolidated) continue;
|
|
130
|
+
|
|
131
|
+
// Generate summary
|
|
132
|
+
const summary = await this.summarize(group);
|
|
133
|
+
|
|
134
|
+
// Create consolidated memory
|
|
135
|
+
await this.consolidatedStore.create({
|
|
136
|
+
summary,
|
|
137
|
+
topics: group.topics,
|
|
138
|
+
sourceEvents: eventIds,
|
|
139
|
+
confidence: this.calculateConfidence(group)
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
consolidatedCount++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Prune consolidated events from working set
|
|
146
|
+
if (consolidatedCount > 0) {
|
|
147
|
+
const consolidatedEventIds = groups
|
|
148
|
+
.filter(g => g.events.length >= 3)
|
|
149
|
+
.flatMap(g => g.events.map(e => e.id));
|
|
150
|
+
|
|
151
|
+
// Only prune old events (keep recent for context)
|
|
152
|
+
const oldEventIds = consolidatedEventIds.filter(id => {
|
|
153
|
+
const event = workingSet.recentEvents.find(e => e.id === id);
|
|
154
|
+
if (!event) return false;
|
|
155
|
+
const ageHours = (Date.now() - event.timestamp.getTime()) / (1000 * 60 * 60);
|
|
156
|
+
return ageHours > this.config.workingSet.timeWindowHours / 2;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (oldEventIds.length > 0) {
|
|
160
|
+
await this.workingSetStore.prune(oldEventIds);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return consolidatedCount;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if consolidation should run
|
|
169
|
+
*/
|
|
170
|
+
private shouldConsolidate(workingSet: WorkingSet): boolean {
|
|
171
|
+
// Check event count trigger
|
|
172
|
+
if (workingSet.recentEvents.length >= this.config.consolidation.triggerEventCount) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check idle time trigger
|
|
177
|
+
const idleTime = Date.now() - this.lastActivity.getTime();
|
|
178
|
+
if (idleTime >= this.config.consolidation.triggerIdleMs) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Group events by topic using simple keyword extraction
|
|
187
|
+
*/
|
|
188
|
+
private groupByTopic(events: MemoryEvent[]): EventGroup[] {
|
|
189
|
+
const groups = new Map<string, EventGroup>();
|
|
190
|
+
|
|
191
|
+
for (const event of events) {
|
|
192
|
+
const topics = this.extractTopics(event.content);
|
|
193
|
+
|
|
194
|
+
for (const topic of topics) {
|
|
195
|
+
if (!groups.has(topic)) {
|
|
196
|
+
groups.set(topic, { topics: [topic], events: [] });
|
|
197
|
+
}
|
|
198
|
+
const group = groups.get(topic)!;
|
|
199
|
+
if (!group.events.find(e => e.id === event.id)) {
|
|
200
|
+
group.events.push(event);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Merge groups with overlapping events
|
|
206
|
+
const mergedGroups = this.mergeOverlappingGroups(Array.from(groups.values()));
|
|
207
|
+
|
|
208
|
+
return mergedGroups;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract topics from content using simple keyword extraction
|
|
213
|
+
*/
|
|
214
|
+
private extractTopics(content: string): string[] {
|
|
215
|
+
const topics: string[] = [];
|
|
216
|
+
|
|
217
|
+
// Extract code-related keywords
|
|
218
|
+
const codePatterns = [
|
|
219
|
+
/\b(function|class|interface|type|const|let|var)\s+(\w+)/gi,
|
|
220
|
+
/\b(import|export)\s+.*?from\s+['"]([^'"]+)['"]/gi,
|
|
221
|
+
/\bfile[:\s]+([^\s,]+)/gi
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (const pattern of codePatterns) {
|
|
225
|
+
let match;
|
|
226
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
227
|
+
const keyword = match[2] || match[1];
|
|
228
|
+
if (keyword && keyword.length > 2) {
|
|
229
|
+
topics.push(keyword.toLowerCase());
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extract common programming terms
|
|
235
|
+
const commonTerms = [
|
|
236
|
+
'bug', 'fix', 'error', 'issue', 'feature',
|
|
237
|
+
'test', 'refactor', 'implement', 'add', 'remove',
|
|
238
|
+
'update', 'change', 'modify', 'create', 'delete'
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const contentLower = content.toLowerCase();
|
|
242
|
+
for (const term of commonTerms) {
|
|
243
|
+
if (contentLower.includes(term)) {
|
|
244
|
+
topics.push(term);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [...new Set(topics)].slice(0, 5); // Limit to 5 topics
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Merge groups that have significant event overlap
|
|
253
|
+
*/
|
|
254
|
+
private mergeOverlappingGroups(groups: EventGroup[]): EventGroup[] {
|
|
255
|
+
const merged: EventGroup[] = [];
|
|
256
|
+
|
|
257
|
+
for (const group of groups) {
|
|
258
|
+
let foundMerge = false;
|
|
259
|
+
|
|
260
|
+
for (const existing of merged) {
|
|
261
|
+
const overlap = group.events.filter(e =>
|
|
262
|
+
existing.events.some(ex => ex.id === e.id)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// If > 50% overlap, merge
|
|
266
|
+
if (overlap.length > group.events.length / 2) {
|
|
267
|
+
existing.topics = [...new Set([...existing.topics, ...group.topics])];
|
|
268
|
+
for (const event of group.events) {
|
|
269
|
+
if (!existing.events.find(e => e.id === event.id)) {
|
|
270
|
+
existing.events.push(event);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
foundMerge = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!foundMerge) {
|
|
279
|
+
merged.push(group);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return merged;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Generate summary for a group of events
|
|
288
|
+
* Rule-based extraction (no LLM by default)
|
|
289
|
+
*/
|
|
290
|
+
private async summarize(group: EventGroup): Promise<string> {
|
|
291
|
+
if (this.config.consolidation.useLLMSummarization) {
|
|
292
|
+
// Future: LLM-based summarization
|
|
293
|
+
return this.ruleBasedSummary(group);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this.ruleBasedSummary(group);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Rule-based summary generation
|
|
301
|
+
*/
|
|
302
|
+
private ruleBasedSummary(group: EventGroup): string {
|
|
303
|
+
const keyPoints: string[] = [];
|
|
304
|
+
|
|
305
|
+
for (const event of group.events.slice(0, 10)) {
|
|
306
|
+
const keyPoint = this.extractKeyPoint(event.content);
|
|
307
|
+
if (keyPoint) {
|
|
308
|
+
keyPoints.push(keyPoint);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const topicsStr = group.topics.slice(0, 3).join(', ');
|
|
313
|
+
const summary = [
|
|
314
|
+
`Topics: ${topicsStr}`,
|
|
315
|
+
'',
|
|
316
|
+
'Key points:',
|
|
317
|
+
...keyPoints.map(kp => `- ${kp}`)
|
|
318
|
+
].join('\n');
|
|
319
|
+
|
|
320
|
+
return summary;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Extract key point from content
|
|
325
|
+
*/
|
|
326
|
+
private extractKeyPoint(content: string): string | null {
|
|
327
|
+
// Get first meaningful sentence
|
|
328
|
+
const sentences = content.split(/[.!?\n]+/).filter(s => s.trim().length > 10);
|
|
329
|
+
if (sentences.length === 0) return null;
|
|
330
|
+
|
|
331
|
+
const firstSentence = sentences[0].trim();
|
|
332
|
+
|
|
333
|
+
// Truncate if too long
|
|
334
|
+
if (firstSentence.length > 100) {
|
|
335
|
+
return firstSentence.slice(0, 100) + '...';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return firstSentence;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Calculate confidence score for a group
|
|
343
|
+
*/
|
|
344
|
+
private calculateConfidence(group: EventGroup): number {
|
|
345
|
+
// Factor 1: Event count (more events = higher confidence)
|
|
346
|
+
const eventScore = Math.min(group.events.length / 10, 1);
|
|
347
|
+
|
|
348
|
+
// Factor 2: Time proximity (events closer together = higher confidence)
|
|
349
|
+
const timeScore = this.calculateTimeProximity(group.events);
|
|
350
|
+
|
|
351
|
+
// Factor 3: Topic consistency (fewer topics per event = higher confidence)
|
|
352
|
+
const topicScore = Math.min(3 / group.topics.length, 1);
|
|
353
|
+
|
|
354
|
+
return (eventScore * 0.4 + timeScore * 0.4 + topicScore * 0.2);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Calculate time proximity score
|
|
359
|
+
*/
|
|
360
|
+
private calculateTimeProximity(events: MemoryEvent[]): number {
|
|
361
|
+
if (events.length < 2) return 1;
|
|
362
|
+
|
|
363
|
+
const timestamps = events.map(e => e.timestamp.getTime()).sort((a, b) => a - b);
|
|
364
|
+
const timeSpan = timestamps[timestamps.length - 1] - timestamps[0];
|
|
365
|
+
|
|
366
|
+
// Score based on average time between events
|
|
367
|
+
const avgGap = timeSpan / (events.length - 1);
|
|
368
|
+
const hourInMs = 60 * 60 * 1000;
|
|
369
|
+
|
|
370
|
+
// Within 1 hour average = score 1, 24 hours = score 0.5, etc.
|
|
371
|
+
return Math.max(0, 1 - (avgGap / (24 * hourInMs)));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Create a Consolidation Worker instance
|
|
377
|
+
*/
|
|
378
|
+
export function createConsolidationWorker(
|
|
379
|
+
workingSetStore: WorkingSetStore,
|
|
380
|
+
consolidatedStore: ConsolidatedStore,
|
|
381
|
+
config: EndlessModeConfig
|
|
382
|
+
): ConsolidationWorker {
|
|
383
|
+
return new ConsolidationWorker(workingSetStore, consolidatedStore, config);
|
|
384
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Formatter
|
|
3
|
+
* Formats progressive search results for Claude context injection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ProgressiveSearchResult,
|
|
8
|
+
SearchIndexItem,
|
|
9
|
+
TimelineItem,
|
|
10
|
+
FullDetail,
|
|
11
|
+
CitedSearchResult
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { formatCitationId } from './citation-generator.js';
|
|
14
|
+
|
|
15
|
+
export interface FormatOptions {
|
|
16
|
+
format?: 'inline' | 'footnote' | 'reference';
|
|
17
|
+
showTokens?: boolean;
|
|
18
|
+
maxWidth?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ContextFormatter {
|
|
22
|
+
/**
|
|
23
|
+
* Format progressive search result for Claude context
|
|
24
|
+
*/
|
|
25
|
+
formatProgressiveResult(
|
|
26
|
+
result: ProgressiveSearchResult,
|
|
27
|
+
options?: FormatOptions
|
|
28
|
+
): string {
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
|
|
31
|
+
// Layer 1: Always included (index)
|
|
32
|
+
parts.push(this.formatLayer1(result.index));
|
|
33
|
+
|
|
34
|
+
// Layer 2: Timeline (if expanded)
|
|
35
|
+
if (result.timeline && result.timeline.length > 0) {
|
|
36
|
+
parts.push(this.formatLayer2(result.timeline));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Layer 3: Details (if expanded)
|
|
40
|
+
if (result.details && result.details.length > 0) {
|
|
41
|
+
parts.push(this.formatLayer3(result.details, options));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Meta information
|
|
45
|
+
if (options?.showTokens !== false) {
|
|
46
|
+
parts.push(this.formatMeta(result.meta));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts.join('\n\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format Layer 1: Search Index
|
|
54
|
+
*/
|
|
55
|
+
private formatLayer1(items: SearchIndexItem[]): string {
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
return '## Related Memories\n\nNo relevant memories found.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const header = `## Related Memories (${items.length} matches)\n`;
|
|
61
|
+
const rows = items.map((item, i) => {
|
|
62
|
+
const date = item.timestamp.toISOString().split('T')[0];
|
|
63
|
+
return `${i + 1}. **[${item.id.slice(0, 8)}]** ${item.summary} _(${date}, score: ${item.score.toFixed(2)})_`;
|
|
64
|
+
}).join('\n');
|
|
65
|
+
|
|
66
|
+
return header + rows;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Format Layer 2: Timeline
|
|
71
|
+
*/
|
|
72
|
+
private formatLayer2(items: TimelineItem[]): string {
|
|
73
|
+
const header = '## Timeline Context\n';
|
|
74
|
+
const timeline = items.map(item => {
|
|
75
|
+
const marker = item.isTarget ? '**→**' : ' ';
|
|
76
|
+
const time = item.timestamp.toLocaleTimeString('en-US', {
|
|
77
|
+
hour: '2-digit',
|
|
78
|
+
minute: '2-digit'
|
|
79
|
+
});
|
|
80
|
+
const typeIcon = this.getTypeIcon(item.type);
|
|
81
|
+
return `${marker} ${time} ${typeIcon} ${item.preview}`;
|
|
82
|
+
}).join('\n');
|
|
83
|
+
|
|
84
|
+
return header + timeline;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format Layer 3: Full Details
|
|
89
|
+
*/
|
|
90
|
+
private formatLayer3(items: FullDetail[], options?: FormatOptions): string {
|
|
91
|
+
const format = options?.format ?? 'inline';
|
|
92
|
+
|
|
93
|
+
switch (format) {
|
|
94
|
+
case 'inline':
|
|
95
|
+
return this.formatDetailsInline(items);
|
|
96
|
+
case 'footnote':
|
|
97
|
+
return this.formatDetailsFootnote(items);
|
|
98
|
+
case 'reference':
|
|
99
|
+
return this.formatDetailsReference(items);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Inline format for details
|
|
105
|
+
*/
|
|
106
|
+
private formatDetailsInline(items: FullDetail[]): string {
|
|
107
|
+
return items.map(item => {
|
|
108
|
+
const date = item.timestamp.toLocaleDateString();
|
|
109
|
+
const session = item.sessionId.slice(0, 8);
|
|
110
|
+
const citation = item.citationId ? formatCitationId(item.citationId) : '';
|
|
111
|
+
|
|
112
|
+
const header = `## Detail: ${item.id.slice(0, 8)}`;
|
|
113
|
+
const meta = `_${item.type} | Session: ${session} | ${date}_`;
|
|
114
|
+
const content = item.content;
|
|
115
|
+
const footer = citation ? `\n${citation}` : '';
|
|
116
|
+
|
|
117
|
+
return [header, meta, '', content, footer].join('\n');
|
|
118
|
+
}).join('\n\n---\n\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Footnote format for details
|
|
123
|
+
*/
|
|
124
|
+
private formatDetailsFootnote(items: FullDetail[]): string {
|
|
125
|
+
const content = items.map((item, i) => {
|
|
126
|
+
return `${item.content} [${i + 1}]`;
|
|
127
|
+
}).join('\n\n');
|
|
128
|
+
|
|
129
|
+
const footnotes = items.map((item, i) => {
|
|
130
|
+
const citation = item.citationId ? formatCitationId(item.citationId) : `[${item.id.slice(0, 8)}]`;
|
|
131
|
+
const date = item.timestamp.toLocaleDateString();
|
|
132
|
+
return `[${i + 1}] ${citation} - ${date}`;
|
|
133
|
+
}).join('\n');
|
|
134
|
+
|
|
135
|
+
return `${content}\n\n---\n**References:**\n${footnotes}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reference format for details
|
|
140
|
+
*/
|
|
141
|
+
private formatDetailsReference(items: FullDetail[]): string {
|
|
142
|
+
const content = items.map(item => {
|
|
143
|
+
return `### ${item.type}\n${item.content}`;
|
|
144
|
+
}).join('\n\n');
|
|
145
|
+
|
|
146
|
+
const references = items.map(item => {
|
|
147
|
+
const citation = item.citationId ? formatCitationId(item.citationId) : `[${item.id.slice(0, 8)}]`;
|
|
148
|
+
const date = item.timestamp.toLocaleDateString();
|
|
149
|
+
return `- ${citation} Session ${item.sessionId.slice(0, 8)}, ${date}`;
|
|
150
|
+
}).join('\n');
|
|
151
|
+
|
|
152
|
+
return `## Content\n\n${content}\n\n## References\n${references}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format meta information
|
|
157
|
+
*/
|
|
158
|
+
private formatMeta(meta: ProgressiveSearchResult['meta']): string {
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
|
|
161
|
+
if (meta.expansionReason) {
|
|
162
|
+
const reasonText = this.getExpansionReasonText(meta.expansionReason);
|
|
163
|
+
parts.push(`_${reasonText}_`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parts.push(`_~${meta.estimatedTokens} tokens | ${meta.expandedCount} expanded_`);
|
|
167
|
+
|
|
168
|
+
return parts.join(' | ');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get icon for event type
|
|
173
|
+
*/
|
|
174
|
+
private getTypeIcon(type: string): string {
|
|
175
|
+
switch (type) {
|
|
176
|
+
case 'user_prompt':
|
|
177
|
+
return '👤';
|
|
178
|
+
case 'agent_response':
|
|
179
|
+
return '🤖';
|
|
180
|
+
case 'session_summary':
|
|
181
|
+
return '📋';
|
|
182
|
+
case 'tool_observation':
|
|
183
|
+
return '🔧';
|
|
184
|
+
default:
|
|
185
|
+
return '📄';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get human-readable expansion reason
|
|
191
|
+
*/
|
|
192
|
+
private getExpansionReasonText(reason: string): string {
|
|
193
|
+
switch (reason) {
|
|
194
|
+
case 'high_confidence_single':
|
|
195
|
+
return 'High confidence match - auto-expanded';
|
|
196
|
+
case 'clear_winner':
|
|
197
|
+
return 'Clear best match found';
|
|
198
|
+
case 'ambiguous_multiple_high':
|
|
199
|
+
return 'Multiple relevant results - showing timeline';
|
|
200
|
+
case 'low_confidence':
|
|
201
|
+
return 'No high confidence matches';
|
|
202
|
+
case 'no_results':
|
|
203
|
+
return 'No matches found';
|
|
204
|
+
default:
|
|
205
|
+
return reason;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format context with citations (for cited search results)
|
|
212
|
+
*/
|
|
213
|
+
export function formatContextWithCitations(
|
|
214
|
+
results: CitedSearchResult[],
|
|
215
|
+
options?: FormatOptions
|
|
216
|
+
): string {
|
|
217
|
+
const format = options?.format ?? 'inline';
|
|
218
|
+
|
|
219
|
+
switch (format) {
|
|
220
|
+
case 'inline':
|
|
221
|
+
return formatCitedInline(results);
|
|
222
|
+
case 'footnote':
|
|
223
|
+
return formatCitedFootnote(results);
|
|
224
|
+
case 'reference':
|
|
225
|
+
return formatCitedReference(results);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatCitedInline(results: CitedSearchResult[]): string {
|
|
230
|
+
return results.map(r => {
|
|
231
|
+
const date = r.event.timestamp.toLocaleDateString();
|
|
232
|
+
const session = r.event.sessionId.slice(0, 8);
|
|
233
|
+
const citation = formatCitationId(r.citation.citationId);
|
|
234
|
+
|
|
235
|
+
return [
|
|
236
|
+
`> ${r.event.content}`,
|
|
237
|
+
`>`,
|
|
238
|
+
`> ${citation} - ${date}, Session ${session}`
|
|
239
|
+
].join('\n');
|
|
240
|
+
}).join('\n\n---\n\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatCitedFootnote(results: CitedSearchResult[]): string {
|
|
244
|
+
const content = results.map((r, i) => {
|
|
245
|
+
return `${r.event.content} [${i + 1}]`;
|
|
246
|
+
}).join('\n\n');
|
|
247
|
+
|
|
248
|
+
const footnotes = results.map((r, i) => {
|
|
249
|
+
const citation = formatCitationId(r.citation.citationId);
|
|
250
|
+
const date = r.event.timestamp.toLocaleDateString();
|
|
251
|
+
return `[${i + 1}] ${citation} - ${date}`;
|
|
252
|
+
}).join('\n');
|
|
253
|
+
|
|
254
|
+
return `${content}\n\n---\n**References:**\n${footnotes}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatCitedReference(results: CitedSearchResult[]): string {
|
|
258
|
+
const content = results.map(r => {
|
|
259
|
+
return `### ${r.event.eventType}\n${r.event.content}`;
|
|
260
|
+
}).join('\n\n');
|
|
261
|
+
|
|
262
|
+
const references = results.map(r => {
|
|
263
|
+
const citation = formatCitationId(r.citation.citationId);
|
|
264
|
+
const date = r.event.timestamp.toLocaleDateString();
|
|
265
|
+
return `- ${citation} Session ${r.event.sessionId.slice(0, 8)}, ${date}`;
|
|
266
|
+
}).join('\n');
|
|
267
|
+
|
|
268
|
+
return `## Content\n\n${content}\n\n## References\n${references}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a context formatter instance
|
|
273
|
+
*/
|
|
274
|
+
export function createContextFormatter(): ContextFormatter {
|
|
275
|
+
return new ContextFormatter();
|
|
276
|
+
}
|