@rlabs-inc/memory 0.1.0 → 0.2.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/dist/index.js +130 -18
- package/dist/index.mjs +130 -18
- package/dist/server/index.js +141 -18
- package/dist/server/index.mjs +141 -18
- package/hooks/curation.ts +89 -0
- package/hooks/session-start.ts +98 -0
- package/hooks/user-prompt.ts +97 -0
- package/package.json +14 -8
- package/src/cli/colors.ts +174 -0
- package/src/cli/commands/doctor.ts +143 -0
- package/src/cli/commands/install.ts +153 -0
- package/src/cli/commands/serve.ts +76 -0
- package/src/cli/commands/stats.ts +64 -0
- package/src/cli/index.ts +128 -0
- package/src/core/curator.ts +7 -48
- package/src/core/engine.test.ts +321 -0
- package/src/core/engine.ts +45 -8
- package/src/core/retrieval.ts +1 -1
- package/src/core/store.ts +109 -98
- package/src/server/index.ts +15 -40
- package/src/types/schema.ts +1 -1
- package/src/utils/logger.ts +158 -107
- package/bun.lock +0 -102
- package/test-retrieval.ts +0 -91
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MEMORY ENGINE TESTS
|
|
3
|
+
// Basic integration tests for the memory system
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
7
|
+
import { MemoryEngine, createEngine } from './engine'
|
|
8
|
+
import { MemoryStore, createStore } from './store'
|
|
9
|
+
import { SmartVectorRetrieval, createRetrieval } from './retrieval'
|
|
10
|
+
import { join } from 'path'
|
|
11
|
+
import { rmSync, existsSync } from 'fs'
|
|
12
|
+
import type { CuratedMemory, StoredMemory } from '../types/memory'
|
|
13
|
+
|
|
14
|
+
const TEST_DIR = join(import.meta.dir, '../../test-data')
|
|
15
|
+
|
|
16
|
+
describe('MemoryStore', () => {
|
|
17
|
+
let store: MemoryStore
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
// Clean test directory
|
|
21
|
+
if (existsSync(TEST_DIR)) {
|
|
22
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
23
|
+
}
|
|
24
|
+
store = createStore({ basePath: TEST_DIR })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
store.close()
|
|
29
|
+
if (existsSync(TEST_DIR)) {
|
|
30
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should store and retrieve a memory', async () => {
|
|
35
|
+
const memory: CuratedMemory = {
|
|
36
|
+
content: 'Test memory content',
|
|
37
|
+
reasoning: 'This is important because...',
|
|
38
|
+
importance_weight: 0.8,
|
|
39
|
+
confidence_score: 0.9,
|
|
40
|
+
context_type: 'technical',
|
|
41
|
+
temporal_relevance: 'persistent',
|
|
42
|
+
knowledge_domain: 'testing',
|
|
43
|
+
emotional_resonance: 'neutral',
|
|
44
|
+
action_required: false,
|
|
45
|
+
problem_solution_pair: false,
|
|
46
|
+
semantic_tags: ['test', 'memory'],
|
|
47
|
+
trigger_phrases: ['testing memory'],
|
|
48
|
+
question_types: ['how to test'],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const id = await store.storeMemory('test-project', 'session-1', memory)
|
|
52
|
+
expect(id).toBeTruthy()
|
|
53
|
+
|
|
54
|
+
const allMemories = await store.getAllMemories('test-project')
|
|
55
|
+
expect(allMemories.length).toBe(1)
|
|
56
|
+
expect(allMemories[0].content).toBe('Test memory content')
|
|
57
|
+
expect(allMemories[0].importance_weight).toBe(0.8)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should track sessions', async () => {
|
|
61
|
+
const result1 = await store.getOrCreateSession('test-project', 'session-1')
|
|
62
|
+
expect(result1.isNew).toBe(true)
|
|
63
|
+
expect(result1.messageCount).toBe(0)
|
|
64
|
+
|
|
65
|
+
const result2 = await store.getOrCreateSession('test-project', 'session-1')
|
|
66
|
+
expect(result2.isNew).toBe(false)
|
|
67
|
+
|
|
68
|
+
const count = await store.incrementMessageCount('test-project', 'session-1')
|
|
69
|
+
expect(count).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('should store and retrieve session summaries', async () => {
|
|
73
|
+
await store.getOrCreateSession('test-project', 'session-1')
|
|
74
|
+
|
|
75
|
+
const id = await store.storeSessionSummary(
|
|
76
|
+
'test-project',
|
|
77
|
+
'session-1',
|
|
78
|
+
'We worked on the memory system together',
|
|
79
|
+
'collaborative'
|
|
80
|
+
)
|
|
81
|
+
expect(id).toBeTruthy()
|
|
82
|
+
|
|
83
|
+
const summary = await store.getLatestSummary('test-project')
|
|
84
|
+
expect(summary).toBeTruthy()
|
|
85
|
+
expect(summary!.summary).toBe('We worked on the memory system together')
|
|
86
|
+
expect(summary!.interaction_tone).toBe('collaborative')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('should return project stats', async () => {
|
|
90
|
+
await store.getOrCreateSession('test-project', 'session-1')
|
|
91
|
+
|
|
92
|
+
const memory: CuratedMemory = {
|
|
93
|
+
content: 'Test memory',
|
|
94
|
+
reasoning: 'Test',
|
|
95
|
+
importance_weight: 0.5,
|
|
96
|
+
confidence_score: 0.5,
|
|
97
|
+
context_type: 'general',
|
|
98
|
+
temporal_relevance: 'session',
|
|
99
|
+
knowledge_domain: 'test',
|
|
100
|
+
emotional_resonance: 'neutral',
|
|
101
|
+
action_required: false,
|
|
102
|
+
problem_solution_pair: false,
|
|
103
|
+
semantic_tags: [],
|
|
104
|
+
trigger_phrases: [],
|
|
105
|
+
question_types: [],
|
|
106
|
+
}
|
|
107
|
+
await store.storeMemory('test-project', 'session-1', memory)
|
|
108
|
+
|
|
109
|
+
const stats = await store.getProjectStats('test-project')
|
|
110
|
+
expect(stats.totalMemories).toBe(1)
|
|
111
|
+
expect(stats.totalSessions).toBe(1)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('SmartVectorRetrieval', () => {
|
|
116
|
+
const retrieval = createRetrieval()
|
|
117
|
+
|
|
118
|
+
const createTestMemory = (overrides: Partial<StoredMemory> = {}): StoredMemory => ({
|
|
119
|
+
id: 'test-' + Math.random().toString(36).slice(2),
|
|
120
|
+
content: 'Test memory',
|
|
121
|
+
reasoning: 'Test reasoning',
|
|
122
|
+
importance_weight: 0.5,
|
|
123
|
+
confidence_score: 0.5,
|
|
124
|
+
context_type: 'general',
|
|
125
|
+
temporal_relevance: 'persistent',
|
|
126
|
+
knowledge_domain: 'test',
|
|
127
|
+
emotional_resonance: 'neutral',
|
|
128
|
+
action_required: false,
|
|
129
|
+
problem_solution_pair: false,
|
|
130
|
+
semantic_tags: [],
|
|
131
|
+
trigger_phrases: [],
|
|
132
|
+
question_types: [],
|
|
133
|
+
session_id: 'session-1',
|
|
134
|
+
project_id: 'test-project',
|
|
135
|
+
created_at: Date.now(),
|
|
136
|
+
updated_at: Date.now(),
|
|
137
|
+
...overrides,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('should score action_required memories higher', () => {
|
|
141
|
+
const memories = [
|
|
142
|
+
createTestMemory({
|
|
143
|
+
id: 'normal',
|
|
144
|
+
content: 'Normal memory about tasks',
|
|
145
|
+
action_required: false,
|
|
146
|
+
importance_weight: 0.8,
|
|
147
|
+
trigger_phrases: ['todo', 'tasks'],
|
|
148
|
+
}),
|
|
149
|
+
createTestMemory({
|
|
150
|
+
id: 'action',
|
|
151
|
+
content: 'Action required: Fix the bug',
|
|
152
|
+
action_required: true,
|
|
153
|
+
importance_weight: 0.9,
|
|
154
|
+
trigger_phrases: ['todo', 'tasks', 'action', 'do'],
|
|
155
|
+
}),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
159
|
+
memories,
|
|
160
|
+
'What tasks do I need to do?',
|
|
161
|
+
new Float32Array(384),
|
|
162
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
163
|
+
5
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Action required should be prioritized (if any pass the gatekeeper)
|
|
167
|
+
// The action memory should score higher due to action boost
|
|
168
|
+
if (results.length > 0) {
|
|
169
|
+
const actionMemory = results.find(r => r.id === 'action')
|
|
170
|
+
expect(actionMemory).toBeTruthy()
|
|
171
|
+
} else {
|
|
172
|
+
// If no results, it means relevance threshold wasn't met - that's ok
|
|
173
|
+
expect(results.length).toBe(0)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('should match trigger phrases', () => {
|
|
178
|
+
const memories = [
|
|
179
|
+
createTestMemory({
|
|
180
|
+
id: 'with-trigger',
|
|
181
|
+
content: 'Memory about debugging',
|
|
182
|
+
trigger_phrases: ['debugging', 'bug fix'],
|
|
183
|
+
importance_weight: 0.7,
|
|
184
|
+
}),
|
|
185
|
+
createTestMemory({
|
|
186
|
+
id: 'no-trigger',
|
|
187
|
+
content: 'Unrelated memory',
|
|
188
|
+
trigger_phrases: ['cooking', 'recipes'],
|
|
189
|
+
importance_weight: 0.7,
|
|
190
|
+
}),
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
194
|
+
memories,
|
|
195
|
+
'I am debugging an issue',
|
|
196
|
+
new Float32Array(384),
|
|
197
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
198
|
+
5
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// The debugging memory should score higher
|
|
202
|
+
if (results.length > 0) {
|
|
203
|
+
const triggerMemory = results.find(r => r.id === 'with-trigger')
|
|
204
|
+
expect(triggerMemory).toBeTruthy()
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('should respect maxMemories limit', () => {
|
|
209
|
+
const memories = Array.from({ length: 10 }, (_, i) =>
|
|
210
|
+
createTestMemory({
|
|
211
|
+
id: `memory-${i}`,
|
|
212
|
+
content: `Memory ${i}`,
|
|
213
|
+
importance_weight: 0.8,
|
|
214
|
+
trigger_phrases: ['test'],
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
219
|
+
memories,
|
|
220
|
+
'test query',
|
|
221
|
+
new Float32Array(384),
|
|
222
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
223
|
+
3 // Limit to 3
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
expect(results.length).toBeLessThanOrEqual(3)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('MemoryEngine', () => {
|
|
231
|
+
let engine: MemoryEngine
|
|
232
|
+
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
if (existsSync(TEST_DIR)) {
|
|
235
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
236
|
+
}
|
|
237
|
+
engine = createEngine({
|
|
238
|
+
centralPath: TEST_DIR,
|
|
239
|
+
maxMemories: 5,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
engine.close()
|
|
245
|
+
if (existsSync(TEST_DIR)) {
|
|
246
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('should return primer on first message', async () => {
|
|
251
|
+
const result = await engine.getContext({
|
|
252
|
+
sessionId: 'session-1',
|
|
253
|
+
projectId: 'test-project',
|
|
254
|
+
currentMessage: 'Hello!',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(result.primer).toBeTruthy()
|
|
258
|
+
expect(result.memories.length).toBe(0)
|
|
259
|
+
expect(result.formatted).toContain('Continuing Session')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('should deduplicate memories within session', async () => {
|
|
263
|
+
// Store some memories first
|
|
264
|
+
const memory: CuratedMemory = {
|
|
265
|
+
content: 'Important memory about TypeScript',
|
|
266
|
+
reasoning: 'Technical insight',
|
|
267
|
+
importance_weight: 0.9,
|
|
268
|
+
confidence_score: 0.9,
|
|
269
|
+
context_type: 'technical',
|
|
270
|
+
temporal_relevance: 'persistent',
|
|
271
|
+
knowledge_domain: 'typescript',
|
|
272
|
+
emotional_resonance: 'discovery',
|
|
273
|
+
action_required: false,
|
|
274
|
+
problem_solution_pair: false,
|
|
275
|
+
semantic_tags: ['typescript', 'memory'],
|
|
276
|
+
trigger_phrases: ['typescript', 'ts'],
|
|
277
|
+
question_types: ['how to'],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await engine.storeCurationResult('test-project', 'session-0', {
|
|
281
|
+
session_summary: 'Previous session',
|
|
282
|
+
memories: [memory],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// First context request should get the memory
|
|
286
|
+
const result1 = await engine.getContext({
|
|
287
|
+
sessionId: 'session-1',
|
|
288
|
+
projectId: 'test-project',
|
|
289
|
+
currentMessage: 'First message',
|
|
290
|
+
})
|
|
291
|
+
// This is the primer (message count 0)
|
|
292
|
+
|
|
293
|
+
// Track a message
|
|
294
|
+
await engine.trackMessage('test-project', 'session-1')
|
|
295
|
+
|
|
296
|
+
// Second request with TypeScript query
|
|
297
|
+
const result2 = await engine.getContext({
|
|
298
|
+
sessionId: 'session-1',
|
|
299
|
+
projectId: 'test-project',
|
|
300
|
+
currentMessage: 'Tell me about TypeScript',
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Third request - same session, should NOT get same memory again
|
|
304
|
+
await engine.trackMessage('test-project', 'session-1')
|
|
305
|
+
const result3 = await engine.getContext({
|
|
306
|
+
sessionId: 'session-1',
|
|
307
|
+
projectId: 'test-project',
|
|
308
|
+
currentMessage: 'More about TypeScript',
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// The memory should only appear once per session
|
|
312
|
+
const memory2Count = result2.memories.length
|
|
313
|
+
const memory3Count = result3.memories.length
|
|
314
|
+
|
|
315
|
+
// Second query should find the memory, third should not (already injected)
|
|
316
|
+
console.log(`Result2 memories: ${memory2Count}, Result3 memories: ${memory3Count}`)
|
|
317
|
+
|
|
318
|
+
// At least verify the deduplication logic is running
|
|
319
|
+
expect(result3.memories.length).toBeLessThanOrEqual(result2.memories.length)
|
|
320
|
+
})
|
|
321
|
+
})
|
package/src/core/engine.ts
CHANGED
|
@@ -69,6 +69,17 @@ export interface ContextRequest {
|
|
|
69
69
|
projectPath?: string // Required for 'local' storage mode
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Session metadata for deduplication
|
|
74
|
+
* Tracks which memories have been injected in each session
|
|
75
|
+
*/
|
|
76
|
+
interface SessionMetadata {
|
|
77
|
+
message_count: number
|
|
78
|
+
started_at: number
|
|
79
|
+
project_id: string
|
|
80
|
+
injected_memories: Set<string> // Memory IDs already shown in this session
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
/**
|
|
73
84
|
* Memory Engine - The main orchestrator
|
|
74
85
|
*/
|
|
@@ -76,6 +87,7 @@ export class MemoryEngine {
|
|
|
76
87
|
private _config: Required<Omit<EngineConfig, 'embedder'>> & { embedder?: EngineConfig['embedder'] }
|
|
77
88
|
private _stores = new Map<string, MemoryStore>() // projectPath -> store
|
|
78
89
|
private _retrieval: SmartVectorRetrieval
|
|
90
|
+
private _sessionMetadata = new Map<string, SessionMetadata>() // sessionId -> metadata
|
|
79
91
|
|
|
80
92
|
constructor(config: EngineConfig = {}) {
|
|
81
93
|
this._config = {
|
|
@@ -89,6 +101,21 @@ export class MemoryEngine {
|
|
|
89
101
|
this._retrieval = createRetrieval()
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Get or create session metadata for deduplication
|
|
106
|
+
*/
|
|
107
|
+
private _getSessionMetadata(sessionId: string, projectId: string): SessionMetadata {
|
|
108
|
+
if (!this._sessionMetadata.has(sessionId)) {
|
|
109
|
+
this._sessionMetadata.set(sessionId, {
|
|
110
|
+
message_count: 0,
|
|
111
|
+
started_at: Date.now(),
|
|
112
|
+
project_id: projectId,
|
|
113
|
+
injected_memories: new Set(),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
return this._sessionMetadata.get(sessionId)!
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
/**
|
|
93
120
|
* Get the appropriate store for a project
|
|
94
121
|
*/
|
|
@@ -97,13 +124,6 @@ export class MemoryEngine {
|
|
|
97
124
|
? projectPath
|
|
98
125
|
: projectId
|
|
99
126
|
|
|
100
|
-
console.log(`🏪 [DEBUG] _getStore called:`)
|
|
101
|
-
console.log(` projectId: ${projectId}`)
|
|
102
|
-
console.log(` projectPath: ${projectPath}`)
|
|
103
|
-
console.log(` storageMode: ${this._config.storageMode}`)
|
|
104
|
-
console.log(` cache key: ${key}`)
|
|
105
|
-
console.log(` cached: ${this._stores.has(key)}`)
|
|
106
|
-
|
|
107
127
|
if (this._stores.has(key)) {
|
|
108
128
|
return this._stores.get(key)!
|
|
109
129
|
}
|
|
@@ -166,6 +186,10 @@ export class MemoryEngine {
|
|
|
166
186
|
return { memories: [], formatted: '' }
|
|
167
187
|
}
|
|
168
188
|
|
|
189
|
+
// Get session metadata for deduplication
|
|
190
|
+
const sessionMeta = this._getSessionMetadata(sessionId, projectId)
|
|
191
|
+
const injectedIds = sessionMeta.injected_memories
|
|
192
|
+
|
|
169
193
|
// Get all memories for this project
|
|
170
194
|
const allMemories = await store.getAllMemories(projectId)
|
|
171
195
|
|
|
@@ -173,6 +197,13 @@ export class MemoryEngine {
|
|
|
173
197
|
return { memories: [], formatted: '' }
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
// Filter out already-injected memories (deduplication)
|
|
201
|
+
const candidateMemories = allMemories.filter(m => !injectedIds.has(m.id))
|
|
202
|
+
|
|
203
|
+
if (!candidateMemories.length) {
|
|
204
|
+
return { memories: [], formatted: '' }
|
|
205
|
+
}
|
|
206
|
+
|
|
176
207
|
// Generate embedding for query if embedder is available
|
|
177
208
|
let queryEmbedding: Float32Array | undefined
|
|
178
209
|
if (this._config.embedder) {
|
|
@@ -187,14 +218,20 @@ export class MemoryEngine {
|
|
|
187
218
|
}
|
|
188
219
|
|
|
189
220
|
// Retrieve relevant memories using 10-dimensional scoring
|
|
221
|
+
// Use candidateMemories (already filtered for deduplication)
|
|
190
222
|
const relevantMemories = this._retrieval.retrieveRelevantMemories(
|
|
191
|
-
|
|
223
|
+
candidateMemories,
|
|
192
224
|
currentMessage,
|
|
193
225
|
queryEmbedding ?? new Float32Array(384), // Empty embedding if no embedder
|
|
194
226
|
sessionContext,
|
|
195
227
|
maxMemories
|
|
196
228
|
)
|
|
197
229
|
|
|
230
|
+
// Update injected memories for deduplication
|
|
231
|
+
for (const memory of relevantMemories) {
|
|
232
|
+
injectedIds.add(memory.id)
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
return {
|
|
199
236
|
memories: relevantMemories,
|
|
200
237
|
formatted: this._formatMemories(relevantMemories),
|
package/src/core/retrieval.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// ============================================================================
|
|
6
6
|
|
|
7
7
|
import type { StoredMemory, RetrievalResult } from '../types/memory.ts'
|
|
8
|
-
import { cosineSimilarity } from '
|
|
8
|
+
import { cosineSimilarity } from '@rlabs-inc/fsdb'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Session context for retrieval
|