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,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
|
+
}
|