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,186 @@
1
+ /**
2
+ * AXIOMMIND canonical_key.py port
3
+ * Deterministic normalization ensuring identical titles always map to same keys
4
+ */
5
+
6
+ import { createHash } from 'crypto';
7
+
8
+ const MAX_KEY_LENGTH = 200;
9
+
10
+ /**
11
+ * Convert text to a normalized canonical key
12
+ *
13
+ * Normalization steps:
14
+ * 1. NFKC unicode normalization
15
+ * 2. Lowercase conversion
16
+ * 3. Punctuation removal
17
+ * 4. Consecutive whitespace cleanup
18
+ * 5. Context addition (optional)
19
+ * 6. Long key truncation with MD5
20
+ */
21
+ export function makeCanonicalKey(
22
+ title: string,
23
+ context?: { project?: string; sessionId?: string }
24
+ ): string {
25
+ // Step 1: NFKC normalization
26
+ let normalized = title.normalize('NFKC');
27
+
28
+ // Step 2: Lowercase conversion
29
+ normalized = normalized.toLowerCase();
30
+
31
+ // Step 3: Punctuation removal (unicode compatible)
32
+ normalized = normalized.replace(/[^\p{L}\p{N}\s]/gu, '');
33
+
34
+ // Step 4: Consecutive whitespace cleanup
35
+ normalized = normalized.replace(/\s+/g, ' ').trim();
36
+
37
+ // Step 5: Context addition
38
+ let key = normalized;
39
+ if (context?.project) {
40
+ key = `${context.project}::${key}`;
41
+ }
42
+
43
+ // Step 6: Long key handling
44
+ if (key.length > MAX_KEY_LENGTH) {
45
+ const hashSuffix = createHash('md5').update(key).digest('hex').slice(0, 8);
46
+ key = key.slice(0, MAX_KEY_LENGTH - 9) + '_' + hashSuffix;
47
+ }
48
+
49
+ return key;
50
+ }
51
+
52
+ /**
53
+ * Check if two texts have the same canonical key
54
+ */
55
+ export function isSameCanonicalKey(a: string, b: string): boolean {
56
+ return makeCanonicalKey(a) === makeCanonicalKey(b);
57
+ }
58
+
59
+ /**
60
+ * Generate dedupe key (content + session for uniqueness)
61
+ * AXIOMMIND Principle 3: Idempotency guarantee
62
+ */
63
+ export function makeDedupeKey(content: string, sessionId: string): string {
64
+ const contentHash = createHash('sha256').update(content).digest('hex');
65
+ return `${sessionId}:${contentHash}`;
66
+ }
67
+
68
+ /**
69
+ * Generate content hash for deduplication
70
+ */
71
+ export function hashContent(content: string): string {
72
+ return createHash('sha256').update(content).digest('hex');
73
+ }
74
+
75
+ // ============================================================
76
+ // Entity Canonical Keys (Task Entity System)
77
+ // ============================================================
78
+
79
+ export type EntityKeyType = 'task' | 'condition' | 'artifact';
80
+
81
+ /**
82
+ * Normalize text for entity key generation
83
+ */
84
+ function normalizeForKey(text: string): string {
85
+ return text
86
+ .normalize('NFKC')
87
+ .toLowerCase()
88
+ .replace(/[^\p{L}\p{N}\s]/gu, '')
89
+ .replace(/\s+/g, '_')
90
+ .trim();
91
+ }
92
+
93
+ /**
94
+ * Generate canonical key for entities
95
+ * Format: {type}:{project}:{normalized_identifier}
96
+ */
97
+ export function makeEntityCanonicalKey(
98
+ entityType: EntityKeyType,
99
+ identifier: string,
100
+ context?: { project?: string }
101
+ ): string {
102
+ const project = context?.project ?? 'default';
103
+
104
+ switch (entityType) {
105
+ case 'task':
106
+ return `task:${project}:${normalizeForKey(identifier)}`;
107
+ case 'condition':
108
+ return `cond:${project}:${normalizeForKey(identifier)}`;
109
+ case 'artifact':
110
+ return makeArtifactKey(identifier);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Generate canonical key for artifacts based on identifier pattern
116
+ * - URL: art:url:{sha1(url)}
117
+ * - JIRA key: art:jira:{key}
118
+ * - GitHub issue: art:gh_issue:{repo}:{num}
119
+ * - Generic: art:generic:{sha1(identifier)}
120
+ */
121
+ export function makeArtifactKey(identifier: string): string {
122
+ // URL pattern
123
+ if (/^https?:\/\//.test(identifier)) {
124
+ const hash = createHash('sha1').update(identifier).digest('hex').slice(0, 12);
125
+ return `art:url:${hash}`;
126
+ }
127
+
128
+ // JIRA key pattern (e.g., PROJ-123)
129
+ const jiraMatch = identifier.match(/^([A-Z]+-\d+)$/);
130
+ if (jiraMatch) {
131
+ return `art:jira:${jiraMatch[1].toLowerCase()}`;
132
+ }
133
+
134
+ // GitHub issue pattern (e.g., owner/repo#123)
135
+ const ghMatch = identifier.match(/^([^\/]+\/[^#]+)#(\d+)$/);
136
+ if (ghMatch) {
137
+ return `art:gh_issue:${ghMatch[1]}:${ghMatch[2]}`;
138
+ }
139
+
140
+ // Generic identifier
141
+ const hash = createHash('sha1').update(identifier).digest('hex').slice(0, 12);
142
+ return `art:generic:${hash}`;
143
+ }
144
+
145
+ /**
146
+ * Generate dedupe key for task events
147
+ */
148
+ export function makeTaskEventDedupeKey(
149
+ eventType: string,
150
+ taskId: string,
151
+ sessionId: string,
152
+ additionalContext?: string
153
+ ): string {
154
+ const parts = [eventType, taskId, sessionId];
155
+ if (additionalContext) {
156
+ parts.push(additionalContext);
157
+ }
158
+ const combined = parts.join(':');
159
+ return createHash('sha256').update(combined).digest('hex');
160
+ }
161
+
162
+ /**
163
+ * Parse entity canonical key to extract type and identifier
164
+ */
165
+ export function parseEntityCanonicalKey(canonicalKey: string): {
166
+ entityType: EntityKeyType;
167
+ project?: string;
168
+ identifier: string;
169
+ } | null {
170
+ const taskMatch = canonicalKey.match(/^task:([^:]+):(.+)$/);
171
+ if (taskMatch) {
172
+ return { entityType: 'task', project: taskMatch[1], identifier: taskMatch[2] };
173
+ }
174
+
175
+ const condMatch = canonicalKey.match(/^cond:([^:]+):(.+)$/);
176
+ if (condMatch) {
177
+ return { entityType: 'condition', project: condMatch[1], identifier: condMatch[2] };
178
+ }
179
+
180
+ const artMatch = canonicalKey.match(/^art:([^:]+):(.+)$/);
181
+ if (artMatch) {
182
+ return { entityType: 'artifact', identifier: artMatch[2] };
183
+ }
184
+
185
+ return null;
186
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Citation Generator
3
+ * Generates unique, short citation IDs for memory references
4
+ */
5
+
6
+ import { createHash } from 'crypto';
7
+
8
+ const ID_LENGTH = 6;
9
+ const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
10
+
11
+ /**
12
+ * Generate a citation ID from an event ID using SHA256
13
+ */
14
+ export function generateCitationId(eventId: string): string {
15
+ const hash = createHash('sha256')
16
+ .update(eventId)
17
+ .digest();
18
+
19
+ let id = '';
20
+ for (let i = 0; i < ID_LENGTH; i++) {
21
+ id += CHARSET[hash[i] % CHARSET.length];
22
+ }
23
+
24
+ return id;
25
+ }
26
+
27
+ /**
28
+ * Generate a unique citation ID with collision handling
29
+ */
30
+ export async function generateUniqueCitationId(
31
+ eventId: string,
32
+ existsCheck: (id: string) => Promise<boolean>
33
+ ): Promise<string> {
34
+ let id = generateCitationId(eventId);
35
+ let attempt = 0;
36
+
37
+ while (await existsCheck(id) && attempt < 10) {
38
+ // Add salt and regenerate
39
+ id = generateCitationId(`${eventId}:${attempt}`);
40
+ attempt++;
41
+ }
42
+
43
+ if (attempt >= 10) {
44
+ throw new Error('Failed to generate unique citation ID after 10 attempts');
45
+ }
46
+
47
+ return id;
48
+ }
49
+
50
+ /**
51
+ * Format a citation ID for display
52
+ */
53
+ export function formatCitationId(citationId: string): string {
54
+ return `[mem:${citationId}]`;
55
+ }
56
+
57
+ /**
58
+ * Parse a citation ID from formatted string
59
+ */
60
+ export function parseCitationId(formatted: string): string | null {
61
+ const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
62
+ return match ? match[1] : null;
63
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Consolidated Store
3
+ * Manages long-term integrated memories for Endless Mode
4
+ * Biomimetic: Simulates memory consolidation that occurs during sleep
5
+ */
6
+
7
+ import { randomUUID } from 'crypto';
8
+ import { Database } from 'duckdb';
9
+ import type {
10
+ ConsolidatedMemory,
11
+ ConsolidatedMemoryInput
12
+ } from './types.js';
13
+ import { EventStore } from './event-store.js';
14
+
15
+ export class ConsolidatedStore {
16
+ constructor(private eventStore: EventStore) {}
17
+
18
+ private get db(): Database {
19
+ return this.eventStore.getDatabase();
20
+ }
21
+
22
+ /**
23
+ * Create a new consolidated memory
24
+ */
25
+ async create(input: ConsolidatedMemoryInput): Promise<string> {
26
+ const memoryId = randomUUID();
27
+
28
+ await this.db.run(
29
+ `INSERT INTO consolidated_memories
30
+ (memory_id, summary, topics, source_events, confidence, created_at)
31
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
32
+ [
33
+ memoryId,
34
+ input.summary,
35
+ JSON.stringify(input.topics),
36
+ JSON.stringify(input.sourceEvents),
37
+ input.confidence
38
+ ]
39
+ );
40
+
41
+ return memoryId;
42
+ }
43
+
44
+ /**
45
+ * Get a consolidated memory by ID
46
+ */
47
+ async get(memoryId: string): Promise<ConsolidatedMemory | null> {
48
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
49
+ `SELECT * FROM consolidated_memories WHERE memory_id = ?`,
50
+ [memoryId]
51
+ );
52
+
53
+ if (rows.length === 0) return null;
54
+ return this.rowToMemory(rows[0]);
55
+ }
56
+
57
+ /**
58
+ * Search consolidated memories by query (simple text search)
59
+ */
60
+ async search(query: string, options?: { topK?: number }): Promise<ConsolidatedMemory[]> {
61
+ const topK = options?.topK || 5;
62
+
63
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
64
+ `SELECT * FROM consolidated_memories
65
+ WHERE summary LIKE ?
66
+ ORDER BY confidence DESC
67
+ LIMIT ?`,
68
+ [`%${query}%`, topK]
69
+ );
70
+
71
+ return rows.map(this.rowToMemory);
72
+ }
73
+
74
+ /**
75
+ * Search by topics
76
+ */
77
+ async searchByTopics(topics: string[], options?: { topK?: number }): Promise<ConsolidatedMemory[]> {
78
+ const topK = options?.topK || 5;
79
+
80
+ // Build topic filter
81
+ const topicConditions = topics.map(() => `topics LIKE ?`).join(' OR ');
82
+ const topicParams = topics.map(t => `%"${t}"%`);
83
+
84
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
85
+ `SELECT * FROM consolidated_memories
86
+ WHERE ${topicConditions}
87
+ ORDER BY confidence DESC
88
+ LIMIT ?`,
89
+ [...topicParams, topK]
90
+ );
91
+
92
+ return rows.map(this.rowToMemory);
93
+ }
94
+
95
+ /**
96
+ * Get all consolidated memories ordered by confidence
97
+ */
98
+ async getAll(options?: { limit?: number }): Promise<ConsolidatedMemory[]> {
99
+ const limit = options?.limit || 100;
100
+
101
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
102
+ `SELECT * FROM consolidated_memories
103
+ ORDER BY confidence DESC, created_at DESC
104
+ LIMIT ?`,
105
+ [limit]
106
+ );
107
+
108
+ return rows.map(this.rowToMemory);
109
+ }
110
+
111
+ /**
112
+ * Get recently created memories
113
+ */
114
+ async getRecent(options?: { limit?: number; hours?: number }): Promise<ConsolidatedMemory[]> {
115
+ const limit = options?.limit || 10;
116
+ const hours = options?.hours || 24;
117
+
118
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
119
+ `SELECT * FROM consolidated_memories
120
+ WHERE created_at > datetime('now', '-${hours} hours')
121
+ ORDER BY created_at DESC
122
+ LIMIT ?`,
123
+ [limit]
124
+ );
125
+
126
+ return rows.map(this.rowToMemory);
127
+ }
128
+
129
+ /**
130
+ * Mark a memory as accessed (tracks usage for importance scoring)
131
+ */
132
+ async markAccessed(memoryId: string): Promise<void> {
133
+ await this.db.run(
134
+ `UPDATE consolidated_memories
135
+ SET accessed_at = CURRENT_TIMESTAMP,
136
+ access_count = access_count + 1
137
+ WHERE memory_id = ?`,
138
+ [memoryId]
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Update confidence score for a memory
144
+ */
145
+ async updateConfidence(memoryId: string, confidence: number): Promise<void> {
146
+ await this.db.run(
147
+ `UPDATE consolidated_memories
148
+ SET confidence = ?
149
+ WHERE memory_id = ?`,
150
+ [confidence, memoryId]
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Delete a consolidated memory
156
+ */
157
+ async delete(memoryId: string): Promise<void> {
158
+ await this.db.run(
159
+ `DELETE FROM consolidated_memories WHERE memory_id = ?`,
160
+ [memoryId]
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Get count of consolidated memories
166
+ */
167
+ async count(): Promise<number> {
168
+ const result = await this.db.all<Array<{ count: number }>>(
169
+ `SELECT COUNT(*) as count FROM consolidated_memories`
170
+ );
171
+ return result[0]?.count || 0;
172
+ }
173
+
174
+ /**
175
+ * Get most accessed memories (for importance scoring)
176
+ */
177
+ async getMostAccessed(limit: number = 10): Promise<ConsolidatedMemory[]> {
178
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
179
+ `SELECT * FROM consolidated_memories
180
+ WHERE access_count > 0
181
+ ORDER BY access_count DESC
182
+ LIMIT ?`,
183
+ [limit]
184
+ );
185
+
186
+ return rows.map(this.rowToMemory);
187
+ }
188
+
189
+ /**
190
+ * Get statistics about consolidated memories
191
+ */
192
+ async getStats(): Promise<{
193
+ total: number;
194
+ averageConfidence: number;
195
+ topicCounts: Record<string, number>;
196
+ recentCount: number;
197
+ }> {
198
+ const total = await this.count();
199
+
200
+ const avgResult = await this.db.all<Array<{ avg: number | null }>>(
201
+ `SELECT AVG(confidence) as avg FROM consolidated_memories`
202
+ );
203
+ const averageConfidence = avgResult[0]?.avg || 0;
204
+
205
+ const recentResult = await this.db.all<Array<{ count: number }>>(
206
+ `SELECT COUNT(*) as count FROM consolidated_memories
207
+ WHERE created_at > datetime('now', '-24 hours')`
208
+ );
209
+ const recentCount = recentResult[0]?.count || 0;
210
+
211
+ // Get topic counts
212
+ const allMemories = await this.getAll({ limit: 1000 });
213
+ const topicCounts: Record<string, number> = {};
214
+ for (const memory of allMemories) {
215
+ for (const topic of memory.topics) {
216
+ topicCounts[topic] = (topicCounts[topic] || 0) + 1;
217
+ }
218
+ }
219
+
220
+ return {
221
+ total,
222
+ averageConfidence,
223
+ topicCounts,
224
+ recentCount
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Check if source events are already consolidated
230
+ */
231
+ async isAlreadyConsolidated(eventIds: string[]): Promise<boolean> {
232
+ for (const eventId of eventIds) {
233
+ const result = await this.db.all<Array<{ count: number }>>(
234
+ `SELECT COUNT(*) as count FROM consolidated_memories
235
+ WHERE source_events LIKE ?`,
236
+ [`%"${eventId}"%`]
237
+ );
238
+ if ((result[0]?.count || 0) > 0) return true;
239
+ }
240
+ return false;
241
+ }
242
+
243
+ /**
244
+ * Get the last consolidation time
245
+ */
246
+ async getLastConsolidationTime(): Promise<Date | null> {
247
+ const result = await this.db.all<Array<{ created_at: string }>>(
248
+ `SELECT created_at FROM consolidated_memories
249
+ ORDER BY created_at DESC
250
+ LIMIT 1`
251
+ );
252
+
253
+ if (result.length === 0) return null;
254
+ return new Date(result[0].created_at);
255
+ }
256
+
257
+ /**
258
+ * Convert database row to ConsolidatedMemory
259
+ */
260
+ private rowToMemory(row: Record<string, unknown>): ConsolidatedMemory {
261
+ return {
262
+ memoryId: row.memory_id as string,
263
+ summary: row.summary as string,
264
+ topics: JSON.parse(row.topics as string || '[]'),
265
+ sourceEvents: JSON.parse(row.source_events as string || '[]'),
266
+ confidence: row.confidence as number,
267
+ createdAt: new Date(row.created_at as string),
268
+ accessedAt: row.accessed_at ? new Date(row.accessed_at as string) : undefined,
269
+ accessCount: row.access_count as number || 0
270
+ };
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Create a Consolidated Store instance
276
+ */
277
+ export function createConsolidatedStore(eventStore: EventStore): ConsolidatedStore {
278
+ return new ConsolidatedStore(eventStore);
279
+ }