@rlabs-inc/memory 0.1.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.
@@ -0,0 +1,404 @@
1
+ // ============================================================================
2
+ // MEMORY ENGINE - Main orchestrator
3
+ // Coordinates storage, retrieval, and curation
4
+ // ============================================================================
5
+
6
+ import { homedir } from 'os'
7
+ import { join } from 'path'
8
+ import { MemoryStore, createStore } from './store.ts'
9
+ import { SmartVectorRetrieval, createRetrieval, type SessionContext } from './retrieval.ts'
10
+ import type {
11
+ CuratedMemory,
12
+ StoredMemory,
13
+ RetrievalResult,
14
+ SessionPrimer,
15
+ CurationResult,
16
+ } from '../types/memory.ts'
17
+
18
+ /**
19
+ * Storage mode for memories
20
+ */
21
+ export type StorageMode = 'central' | 'local'
22
+
23
+ /**
24
+ * Engine configuration
25
+ */
26
+ export interface EngineConfig {
27
+ /**
28
+ * Storage mode:
29
+ * - 'central': ~/.local/share/memory/[project]/ (default)
30
+ * - 'local': [project]/.memory/
31
+ */
32
+ storageMode?: StorageMode
33
+
34
+ /**
35
+ * Base path for central storage
36
+ * Only used when storageMode is 'central'
37
+ * Default: ~/.local/share/memory
38
+ */
39
+ centralPath?: string
40
+
41
+ /**
42
+ * Local folder name for project-local storage
43
+ * Only used when storageMode is 'local'
44
+ * Default: .memory
45
+ */
46
+ localFolder?: string
47
+
48
+ /**
49
+ * Maximum memories to return in context
50
+ * Default: 5
51
+ */
52
+ maxMemories?: number
53
+
54
+ /**
55
+ * Embedding generator function
56
+ * Takes text, returns 384-dimensional embedding
57
+ */
58
+ embedder?: (text: string) => Promise<Float32Array>
59
+ }
60
+
61
+ /**
62
+ * Context request parameters
63
+ */
64
+ export interface ContextRequest {
65
+ sessionId: string
66
+ projectId: string
67
+ currentMessage: string
68
+ maxMemories?: number
69
+ projectPath?: string // Required for 'local' storage mode
70
+ }
71
+
72
+ /**
73
+ * Memory Engine - The main orchestrator
74
+ */
75
+ export class MemoryEngine {
76
+ private _config: Required<Omit<EngineConfig, 'embedder'>> & { embedder?: EngineConfig['embedder'] }
77
+ private _stores = new Map<string, MemoryStore>() // projectPath -> store
78
+ private _retrieval: SmartVectorRetrieval
79
+
80
+ constructor(config: EngineConfig = {}) {
81
+ this._config = {
82
+ storageMode: config.storageMode ?? 'central',
83
+ centralPath: config.centralPath ?? join(homedir(), '.local', 'share', 'memory'),
84
+ localFolder: config.localFolder ?? '.memory',
85
+ maxMemories: config.maxMemories ?? 5,
86
+ embedder: config.embedder,
87
+ }
88
+
89
+ this._retrieval = createRetrieval()
90
+ }
91
+
92
+ /**
93
+ * Get the appropriate store for a project
94
+ */
95
+ private async _getStore(projectId: string, projectPath?: string): Promise<MemoryStore> {
96
+ const key = this._config.storageMode === 'local' && projectPath
97
+ ? projectPath
98
+ : projectId
99
+
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
+ if (this._stores.has(key)) {
108
+ return this._stores.get(key)!
109
+ }
110
+
111
+ let basePath: string
112
+ if (this._config.storageMode === 'local' && projectPath) {
113
+ // Project-local storage: [project]/.memory/
114
+ basePath = join(projectPath, this._config.localFolder)
115
+ } else {
116
+ // Central storage: ~/.local/share/memory/
117
+ basePath = this._config.centralPath
118
+ }
119
+
120
+ const store = createStore({ basePath })
121
+ this._stores.set(key, store)
122
+ return store
123
+ }
124
+
125
+ // ================================================================
126
+ // MAIN API - Used by hooks and server
127
+ // ================================================================
128
+
129
+ /**
130
+ * Get context for a session
131
+ * This is the main entry point called for each user message
132
+ */
133
+ async getContext(request: ContextRequest): Promise<{
134
+ primer?: SessionPrimer
135
+ memories: RetrievalResult[]
136
+ formatted: string
137
+ }> {
138
+ const {
139
+ sessionId,
140
+ projectId,
141
+ currentMessage,
142
+ maxMemories = this._config.maxMemories,
143
+ projectPath,
144
+ } = request
145
+
146
+ const store = await this._getStore(projectId, projectPath)
147
+
148
+ // Get or create session
149
+ const { isNew, messageCount, firstSessionCompleted } = await store.getOrCreateSession(
150
+ projectId,
151
+ sessionId
152
+ )
153
+
154
+ // First message of session: return primer
155
+ if (messageCount === 0) {
156
+ const primer = await this._generateSessionPrimer(store, projectId)
157
+ return {
158
+ primer,
159
+ memories: [],
160
+ formatted: this._formatPrimer(primer),
161
+ }
162
+ }
163
+
164
+ // Subsequent messages: return relevant memories
165
+ if (!currentMessage.trim()) {
166
+ return { memories: [], formatted: '' }
167
+ }
168
+
169
+ // Get all memories for this project
170
+ const allMemories = await store.getAllMemories(projectId)
171
+
172
+ if (!allMemories.length) {
173
+ return { memories: [], formatted: '' }
174
+ }
175
+
176
+ // Generate embedding for query if embedder is available
177
+ let queryEmbedding: Float32Array | undefined
178
+ if (this._config.embedder) {
179
+ queryEmbedding = await this._config.embedder(currentMessage)
180
+ }
181
+
182
+ // Build session context
183
+ const sessionContext: SessionContext = {
184
+ session_id: sessionId,
185
+ project_id: projectId,
186
+ message_count: messageCount,
187
+ }
188
+
189
+ // Retrieve relevant memories using 10-dimensional scoring
190
+ const relevantMemories = this._retrieval.retrieveRelevantMemories(
191
+ allMemories,
192
+ currentMessage,
193
+ queryEmbedding ?? new Float32Array(384), // Empty embedding if no embedder
194
+ sessionContext,
195
+ maxMemories
196
+ )
197
+
198
+ return {
199
+ memories: relevantMemories,
200
+ formatted: this._formatMemories(relevantMemories),
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Register a message was sent (increment counter)
206
+ */
207
+ async trackMessage(
208
+ projectId: string,
209
+ sessionId: string,
210
+ projectPath?: string
211
+ ): Promise<number> {
212
+ const store = await this._getStore(projectId, projectPath)
213
+ return store.incrementMessageCount(projectId, sessionId)
214
+ }
215
+
216
+ /**
217
+ * Store curation results (called after session ends)
218
+ */
219
+ async storeCurationResult(
220
+ projectId: string,
221
+ sessionId: string,
222
+ result: CurationResult,
223
+ projectPath?: string
224
+ ): Promise<{ memoriesStored: number }> {
225
+ const store = await this._getStore(projectId, projectPath)
226
+ let memoriesStored = 0
227
+
228
+ // Store each memory
229
+ for (const memory of result.memories) {
230
+ // Generate embedding if embedder available
231
+ let embedding: Float32Array | undefined
232
+ if (this._config.embedder) {
233
+ embedding = await this._config.embedder(memory.content)
234
+ }
235
+
236
+ await store.storeMemory(projectId, sessionId, memory, embedding)
237
+ memoriesStored++
238
+ }
239
+
240
+ // Store session summary
241
+ if (result.session_summary) {
242
+ await store.storeSessionSummary(
243
+ projectId,
244
+ sessionId,
245
+ result.session_summary,
246
+ result.interaction_tone
247
+ )
248
+ }
249
+
250
+ // Store project snapshot
251
+ if (result.project_snapshot) {
252
+ await store.storeProjectSnapshot(projectId, sessionId, result.project_snapshot)
253
+ }
254
+
255
+ // Mark first session completed
256
+ await store.markFirstSessionCompleted(projectId, sessionId)
257
+
258
+ return { memoriesStored }
259
+ }
260
+
261
+ /**
262
+ * Get statistics for a project
263
+ */
264
+ async getStats(projectId: string, projectPath?: string): Promise<{
265
+ totalMemories: number
266
+ totalSessions: number
267
+ staleMemories: number
268
+ latestSession: string | null
269
+ }> {
270
+ const store = await this._getStore(projectId, projectPath)
271
+ return store.getProjectStats(projectId)
272
+ }
273
+
274
+ // ================================================================
275
+ // FORMATTING
276
+ // ================================================================
277
+
278
+ /**
279
+ * Generate session primer for first message
280
+ */
281
+ private async _generateSessionPrimer(
282
+ store: MemoryStore,
283
+ projectId: string
284
+ ): Promise<SessionPrimer> {
285
+ const [summary, snapshot, stats] = await Promise.all([
286
+ store.getLatestSummary(projectId),
287
+ store.getLatestSnapshot(projectId),
288
+ store.getProjectStats(projectId),
289
+ ])
290
+
291
+ // Calculate temporal context
292
+ let temporalContext = ''
293
+ if (summary) {
294
+ const timeSince = Date.now() - summary.created_at
295
+ temporalContext = this._formatTimeSince(timeSince)
296
+ }
297
+
298
+ return {
299
+ temporal_context: temporalContext,
300
+ session_summary: summary?.summary,
301
+ project_status: snapshot ? this._formatSnapshot(snapshot) : undefined,
302
+ }
303
+ }
304
+
305
+ private _formatTimeSince(ms: number): string {
306
+ const minutes = Math.floor(ms / 60000)
307
+ const hours = Math.floor(minutes / 60)
308
+ const days = Math.floor(hours / 24)
309
+
310
+ if (days > 0) {
311
+ return `Last session: ${days} day${days === 1 ? '' : 's'} ago`
312
+ } else if (hours > 0) {
313
+ return `Last session: ${hours} hour${hours === 1 ? '' : 's'} ago`
314
+ } else if (minutes > 0) {
315
+ return `Last session: ${minutes} minute${minutes === 1 ? '' : 's'} ago`
316
+ } else {
317
+ return 'Last session: just now'
318
+ }
319
+ }
320
+
321
+ private _formatSnapshot(snapshot: {
322
+ current_phase: string
323
+ recent_achievements: string[]
324
+ active_challenges: string[]
325
+ next_steps: string[]
326
+ }): string {
327
+ const parts: string[] = []
328
+
329
+ if (snapshot.current_phase) {
330
+ parts.push(`Phase: ${snapshot.current_phase}`)
331
+ }
332
+ if (snapshot.recent_achievements?.length) {
333
+ parts.push(`Recent: ${snapshot.recent_achievements.join(', ')}`)
334
+ }
335
+ if (snapshot.active_challenges?.length) {
336
+ parts.push(`Challenges: ${snapshot.active_challenges.join(', ')}`)
337
+ }
338
+ if (snapshot.next_steps?.length) {
339
+ parts.push(`Next: ${snapshot.next_steps.join(', ')}`)
340
+ }
341
+
342
+ return parts.join(' | ')
343
+ }
344
+
345
+ /**
346
+ * Format primer for injection
347
+ */
348
+ private _formatPrimer(primer: SessionPrimer): string {
349
+ const parts: string[] = ['# Continuing Session']
350
+
351
+ if (primer.temporal_context) {
352
+ parts.push(`*${primer.temporal_context}*`)
353
+ }
354
+
355
+ if (primer.session_summary) {
356
+ parts.push(`\n**Previous session**: ${primer.session_summary}`)
357
+ }
358
+
359
+ if (primer.project_status) {
360
+ parts.push(`\n**Project status**: ${primer.project_status}`)
361
+ }
362
+
363
+ parts.push(`\n*Memories will surface naturally as we converse.*`)
364
+
365
+ return parts.join('\n')
366
+ }
367
+
368
+ /**
369
+ * Format memories for injection
370
+ */
371
+ private _formatMemories(memories: RetrievalResult[]): string {
372
+ if (!memories.length) return ''
373
+
374
+ const parts: string[] = ['# Memory Context (Consciousness Continuity)']
375
+ parts.push('\n## Key Memories (Claude-Curated)')
376
+
377
+ for (const memory of memories) {
378
+ const tags = memory.semantic_tags?.join(', ') || ''
379
+ const importance = memory.importance_weight?.toFixed(1) || '0.5'
380
+ const contextType = memory.context_type?.toUpperCase() || 'GENERAL'
381
+
382
+ parts.push(`[${contextType} • ${importance}] [${tags}] ${memory.content}`)
383
+ }
384
+
385
+ return parts.join('\n')
386
+ }
387
+
388
+ /**
389
+ * Close all stores
390
+ */
391
+ close(): void {
392
+ for (const store of this._stores.values()) {
393
+ store.close()
394
+ }
395
+ this._stores.clear()
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Create a new memory engine
401
+ */
402
+ export function createEngine(config?: EngineConfig): MemoryEngine {
403
+ return new MemoryEngine(config)
404
+ }
@@ -0,0 +1,8 @@
1
+ // ============================================================================
2
+ // CORE - Main exports
3
+ // ============================================================================
4
+
5
+ export { MemoryEngine, createEngine, type EngineConfig, type StorageMode, type ContextRequest } from './engine.ts'
6
+ export { MemoryStore, createStore, type StoreConfig } from './store.ts'
7
+ export { SmartVectorRetrieval, createRetrieval, type SessionContext } from './retrieval.ts'
8
+ export { Curator, createCurator, type CuratorConfig } from './curator.ts'