@rlabs-inc/memory 0.3.5 → 0.3.6

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/src/core/store.ts CHANGED
@@ -6,21 +6,25 @@
6
6
  import { createDatabase, type Database, type PersistentCollection } from '@rlabs-inc/fsdb'
7
7
  import { homedir } from 'os'
8
8
  import { join } from 'path'
9
- import type {
10
- CuratedMemory,
11
- StoredMemory,
12
- SessionSummary,
13
- ProjectSnapshot,
9
+ import {
10
+ type CuratedMemory,
11
+ type StoredMemory,
12
+ type SessionSummary,
13
+ type ProjectSnapshot,
14
+ V2_DEFAULTS,
14
15
  } from '../types/memory.ts'
15
16
  import {
16
17
  memorySchema,
17
18
  sessionSummarySchema,
18
19
  projectSnapshotSchema,
19
20
  sessionSchema,
21
+ managementLogSchema,
22
+ MEMORY_SCHEMA_VERSION,
20
23
  type MemorySchema,
21
24
  type SessionSummarySchema,
22
25
  type ProjectSnapshotSchema,
23
26
  type SessionSchema,
27
+ type ManagementLogSchema,
24
28
  } from '../types/schema.ts'
25
29
 
26
30
  /**
@@ -34,6 +38,13 @@ export interface StoreConfig {
34
38
  */
35
39
  basePath?: string
36
40
 
41
+ /**
42
+ * Path for global memories (shared across all projects)
43
+ * Default: ~/.local/share/memory/global
44
+ * Global memories are ALWAYS stored centrally, even in local mode
45
+ */
46
+ globalPath?: string
47
+
37
48
  /**
38
49
  * Whether to watch for file changes
39
50
  * Default: false
@@ -52,20 +63,343 @@ interface ProjectDB {
52
63
  sessions: PersistentCollection<typeof sessionSchema>
53
64
  }
54
65
 
66
+ /**
67
+ * Global database with collections (shared across all projects)
68
+ */
69
+ interface GlobalDB {
70
+ db: Database
71
+ memories: PersistentCollection<typeof memorySchema>
72
+ managementLogs: PersistentCollection<typeof managementLogSchema>
73
+ }
74
+
75
+ /**
76
+ * Personal primer structure
77
+ */
78
+ export interface PersonalPrimer {
79
+ content: string
80
+ updated: number // timestamp
81
+ }
82
+
83
+ /**
84
+ * Special ID for the personal primer record
85
+ */
86
+ const PERSONAL_PRIMER_ID = 'personal-primer'
87
+
88
+ /**
89
+ * Default central path for global memories
90
+ * Global memories are ALWAYS stored here, even in local mode
91
+ */
92
+ const DEFAULT_GLOBAL_PATH = join(homedir(), '.local', 'share', 'memory', 'global')
93
+
55
94
  /**
56
95
  * MemoryStore - Manages per-project fsDB instances
57
96
  */
58
97
  export class MemoryStore {
59
98
  private _config: Required<StoreConfig>
60
99
  private _projects = new Map<string, ProjectDB>()
100
+ private _global: GlobalDB | null = null
61
101
 
62
102
  constructor(config: StoreConfig = {}) {
63
103
  this._config = {
64
104
  basePath: config.basePath ?? join(homedir(), '.local', 'share', 'memory'),
105
+ // Global path is ALWAYS central, never local
106
+ globalPath: config.globalPath ?? DEFAULT_GLOBAL_PATH,
65
107
  watchFiles: config.watchFiles ?? false,
66
108
  }
67
109
  }
68
110
 
111
+ // ================================================================
112
+ // GLOBAL DATABASE OPERATIONS
113
+ // ================================================================
114
+
115
+ /**
116
+ * Get or create the global database (shared across all projects)
117
+ * Global is ALWAYS in central location, even when using local mode for projects
118
+ */
119
+ async getGlobal(): Promise<GlobalDB> {
120
+ if (this._global) {
121
+ return this._global
122
+ }
123
+
124
+ // Use the configured global path (always central)
125
+ const globalPath = this._config.globalPath
126
+ console.log(`🌐 [DEBUG] Initializing global database at ${globalPath}`)
127
+
128
+ const db = createDatabase({
129
+ name: 'global',
130
+ basePath: globalPath,
131
+ })
132
+
133
+ // Global memories collection (personal, philosophy, preferences, general breakthroughs)
134
+ const memories = db.collection('memories', {
135
+ schema: memorySchema,
136
+ contentColumn: 'content',
137
+ autoSave: true,
138
+ watchFiles: this._config.watchFiles,
139
+ })
140
+
141
+ // Management log collection (tracks management agent activity)
142
+ const managementLogs = db.collection('management-logs', {
143
+ schema: managementLogSchema,
144
+ contentColumn: 'summary',
145
+ autoSave: true,
146
+ watchFiles: this._config.watchFiles,
147
+ })
148
+
149
+ await Promise.all([memories.load(), managementLogs.load()])
150
+
151
+ this._global = { db, memories, managementLogs }
152
+ return this._global
153
+ }
154
+
155
+ /**
156
+ * Get all global memories
157
+ */
158
+ async getGlobalMemories(): Promise<StoredMemory[]> {
159
+ const { memories } = await this.getGlobal()
160
+
161
+ return memories.all().map(record => ({
162
+ id: record.id,
163
+ content: record.content,
164
+ reasoning: record.reasoning,
165
+ importance_weight: record.importance_weight,
166
+ confidence_score: record.confidence_score,
167
+ context_type: record.context_type as StoredMemory['context_type'],
168
+ temporal_relevance: record.temporal_relevance as StoredMemory['temporal_relevance'],
169
+ knowledge_domain: record.knowledge_domain as StoredMemory['knowledge_domain'],
170
+ emotional_resonance: record.emotional_resonance as StoredMemory['emotional_resonance'],
171
+ action_required: record.action_required,
172
+ problem_solution_pair: record.problem_solution_pair,
173
+ semantic_tags: record.semantic_tags,
174
+ trigger_phrases: record.trigger_phrases,
175
+ question_types: record.question_types,
176
+ session_id: record.session_id,
177
+ project_id: 'global',
178
+ embedding: record.embedding ?? undefined,
179
+ created_at: record.created,
180
+ updated_at: record.updated,
181
+ stale: record.stale,
182
+ }))
183
+ }
184
+
185
+ /**
186
+ * Store a global memory (personal, philosophy, preference, etc.)
187
+ * Global memories are ALWAYS scope: 'global' and have their own type defaults
188
+ */
189
+ async storeGlobalMemory(
190
+ sessionId: string,
191
+ memory: CuratedMemory,
192
+ embedding?: Float32Array | number[],
193
+ sessionNumber?: number
194
+ ): Promise<string> {
195
+ const { memories } = await this.getGlobal()
196
+
197
+ // Get type-specific defaults (personal, philosophy, preference tend to be eternal)
198
+ const contextType = memory.context_type ?? 'personal'
199
+ const typeDefaults = V2_DEFAULTS.typeDefaults[contextType] ?? V2_DEFAULTS.typeDefaults.personal
200
+
201
+ const id = memories.insert({
202
+ // Core v1 fields
203
+ content: memory.content,
204
+ reasoning: memory.reasoning,
205
+ importance_weight: memory.importance_weight,
206
+ confidence_score: memory.confidence_score,
207
+ context_type: memory.context_type,
208
+ temporal_relevance: memory.temporal_relevance,
209
+ knowledge_domain: memory.knowledge_domain,
210
+ emotional_resonance: memory.emotional_resonance,
211
+ action_required: memory.action_required,
212
+ problem_solution_pair: memory.problem_solution_pair,
213
+ semantic_tags: memory.semantic_tags,
214
+ trigger_phrases: memory.trigger_phrases,
215
+ question_types: memory.question_types,
216
+ session_id: sessionId,
217
+ project_id: 'global',
218
+ embedding: embedding
219
+ ? (embedding instanceof Float32Array ? embedding : new Float32Array(embedding))
220
+ : null,
221
+
222
+ // v2 lifecycle fields - global memories are always scope: 'global'
223
+ status: V2_DEFAULTS.fallback.status,
224
+ scope: 'global', // Always global for global memories
225
+ temporal_class: memory.temporal_class ?? typeDefaults?.temporal_class ?? 'eternal',
226
+ fade_rate: typeDefaults?.fade_rate ?? 0, // Global memories typically don't fade
227
+ session_created: sessionNumber ?? 0,
228
+ session_updated: sessionNumber ?? 0,
229
+ sessions_since_surfaced: 0,
230
+ domain: memory.domain ?? null,
231
+ feature: memory.feature ?? null,
232
+ related_files: memory.related_files ?? [],
233
+ awaiting_implementation: memory.awaiting_implementation ?? false,
234
+ awaiting_decision: memory.awaiting_decision ?? false,
235
+ retrieval_weight: memory.importance_weight,
236
+ exclude_from_retrieval: false,
237
+ schema_version: MEMORY_SCHEMA_VERSION,
238
+
239
+ // Initialize empty relationship fields
240
+ supersedes: null,
241
+ superseded_by: null,
242
+ related_to: [],
243
+ resolves: [],
244
+ resolved_by: null,
245
+ parent_id: null,
246
+ child_ids: [],
247
+ blocked_by: null,
248
+ blocks: [],
249
+ })
250
+
251
+ return id
252
+ }
253
+
254
+ // ================================================================
255
+ // PERSONAL PRIMER OPERATIONS
256
+ // ================================================================
257
+
258
+ /**
259
+ * Get the personal primer content
260
+ * Returns null if no primer exists yet (grows organically with personal memories)
261
+ */
262
+ async getPersonalPrimer(): Promise<PersonalPrimer | null> {
263
+ const { memories } = await this.getGlobal()
264
+
265
+ // Personal primer is stored as a special memory record
266
+ const primer = memories.get(PERSONAL_PRIMER_ID)
267
+ if (!primer) {
268
+ return null
269
+ }
270
+
271
+ return {
272
+ content: primer.content,
273
+ updated: primer.updated,
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Update the personal primer
279
+ * Creates it if it doesn't exist
280
+ */
281
+ async setPersonalPrimer(content: string): Promise<void> {
282
+ const { memories } = await this.getGlobal()
283
+
284
+ const existing = memories.get(PERSONAL_PRIMER_ID)
285
+ if (existing) {
286
+ memories.update(PERSONAL_PRIMER_ID, { content })
287
+ } else {
288
+ // Create the primer as a special memory record
289
+ memories.insert({
290
+ id: PERSONAL_PRIMER_ID,
291
+ content,
292
+ reasoning: 'Personal relationship context injected at session start',
293
+ importance_weight: 1.0,
294
+ confidence_score: 1.0,
295
+ context_type: 'personal',
296
+ temporal_relevance: 'persistent',
297
+ knowledge_domain: 'personal',
298
+ emotional_resonance: 'neutral',
299
+ action_required: false,
300
+ problem_solution_pair: false,
301
+ semantic_tags: ['personal', 'primer', 'relationship'],
302
+ trigger_phrases: [],
303
+ question_types: [],
304
+ session_id: 'system',
305
+ project_id: 'global',
306
+ embedding: null,
307
+ })
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Check if personal memories are enabled
313
+ * For now, always returns true. Later can be configured.
314
+ */
315
+ isPersonalMemoriesEnabled(): boolean {
316
+ // TODO: Read from config file if needed
317
+ return true
318
+ }
319
+
320
+ // ================================================================
321
+ // MANAGEMENT LOG OPERATIONS
322
+ // ================================================================
323
+
324
+ /**
325
+ * Store a management log entry
326
+ * Stores complete data with no truncation
327
+ */
328
+ async storeManagementLog(entry: {
329
+ projectId: string
330
+ sessionNumber: number
331
+ memoriesProcessed: number
332
+ supersededCount: number
333
+ resolvedCount: number
334
+ linkedCount: number
335
+ primerUpdated: boolean
336
+ success: boolean
337
+ durationMs: number
338
+ summary: string
339
+ fullReport?: string
340
+ error?: string
341
+ details?: Record<string, any>
342
+ }): Promise<string> {
343
+ const { managementLogs } = await this.getGlobal()
344
+
345
+ const id = managementLogs.insert({
346
+ project_id: entry.projectId,
347
+ session_number: entry.sessionNumber,
348
+ memories_processed: entry.memoriesProcessed,
349
+ superseded_count: entry.supersededCount,
350
+ resolved_count: entry.resolvedCount,
351
+ linked_count: entry.linkedCount,
352
+ primer_updated: entry.primerUpdated,
353
+ success: entry.success,
354
+ duration_ms: entry.durationMs,
355
+ summary: entry.summary,
356
+ full_report: entry.fullReport ?? '',
357
+ error: entry.error ?? '',
358
+ details: entry.details ? JSON.stringify(entry.details) : '',
359
+ })
360
+
361
+ return id
362
+ }
363
+
364
+ /**
365
+ * Get recent management logs
366
+ */
367
+ async getManagementLogs(limit: number = 10): Promise<Array<{
368
+ id: string
369
+ projectId: string
370
+ sessionNumber: number
371
+ memoriesProcessed: number
372
+ supersededCount: number
373
+ resolvedCount: number
374
+ linkedCount: number
375
+ primerUpdated: boolean
376
+ success: boolean
377
+ durationMs: number
378
+ summary: string
379
+ createdAt: number
380
+ }>> {
381
+ const { managementLogs } = await this.getGlobal()
382
+
383
+ return managementLogs
384
+ .all()
385
+ .sort((a, b) => b.created - a.created)
386
+ .slice(0, limit)
387
+ .map(record => ({
388
+ id: record.id,
389
+ projectId: record.project_id,
390
+ sessionNumber: record.session_number,
391
+ memoriesProcessed: record.memories_processed,
392
+ supersededCount: record.superseded_count,
393
+ resolvedCount: record.resolved_count,
394
+ linkedCount: record.linked_count,
395
+ primerUpdated: record.primer_updated,
396
+ success: record.success,
397
+ durationMs: record.duration_ms,
398
+ summary: record.summary,
399
+ createdAt: record.created,
400
+ }))
401
+ }
402
+
69
403
  /**
70
404
  * Get or create database for a project
71
405
  */
@@ -131,17 +465,23 @@ export class MemoryStore {
131
465
  // ================================================================
132
466
 
133
467
  /**
134
- * Store a curated memory
468
+ * Store a curated memory with v2 lifecycle fields
135
469
  */
136
470
  async storeMemory(
137
471
  projectId: string,
138
472
  sessionId: string,
139
473
  memory: CuratedMemory,
140
- embedding?: Float32Array | number[]
474
+ embedding?: Float32Array | number[],
475
+ sessionNumber?: number
141
476
  ): Promise<string> {
142
477
  const { memories } = await this.getProject(projectId)
143
478
 
479
+ // Get type-specific defaults
480
+ const contextType = memory.context_type ?? 'general'
481
+ const typeDefaults = V2_DEFAULTS.typeDefaults[contextType] ?? V2_DEFAULTS.typeDefaults.technical
482
+
144
483
  const id = memories.insert({
484
+ // Core v1 fields
145
485
  content: memory.content,
146
486
  reasoning: memory.reasoning,
147
487
  importance_weight: memory.importance_weight,
@@ -160,6 +500,34 @@ export class MemoryStore {
160
500
  embedding: embedding
161
501
  ? (embedding instanceof Float32Array ? embedding : new Float32Array(embedding))
162
502
  : null,
503
+
504
+ // v2 lifecycle fields - use curator-provided values or smart defaults
505
+ status: V2_DEFAULTS.fallback.status,
506
+ scope: memory.scope ?? typeDefaults?.scope ?? V2_DEFAULTS.fallback.scope,
507
+ temporal_class: memory.temporal_class ?? typeDefaults?.temporal_class ?? V2_DEFAULTS.fallback.temporal_class,
508
+ fade_rate: typeDefaults?.fade_rate ?? V2_DEFAULTS.fallback.fade_rate,
509
+ session_created: sessionNumber ?? 0,
510
+ session_updated: sessionNumber ?? 0,
511
+ sessions_since_surfaced: 0,
512
+ domain: memory.domain ?? null,
513
+ feature: memory.feature ?? null,
514
+ related_files: memory.related_files ?? [],
515
+ awaiting_implementation: memory.awaiting_implementation ?? false,
516
+ awaiting_decision: memory.awaiting_decision ?? false,
517
+ retrieval_weight: memory.importance_weight, // Start with importance as retrieval weight
518
+ exclude_from_retrieval: false,
519
+ schema_version: MEMORY_SCHEMA_VERSION,
520
+
521
+ // Initialize empty relationship fields
522
+ supersedes: null,
523
+ superseded_by: null,
524
+ related_to: [],
525
+ resolves: [],
526
+ resolved_by: null,
527
+ parent_id: null,
528
+ child_ids: [],
529
+ blocked_by: null,
530
+ blocks: [],
163
531
  })
164
532
 
165
533
  return id
@@ -498,13 +866,20 @@ export class MemoryStore {
498
866
  }
499
867
 
500
868
  /**
501
- * Close all project databases
869
+ * Close all project databases (including global)
502
870
  */
503
871
  close(): void {
872
+ // Close project databases
504
873
  for (const projectDB of this._projects.values()) {
505
874
  projectDB.db.close()
506
875
  }
507
876
  this._projects.clear()
877
+
878
+ // Close global database
879
+ if (this._global) {
880
+ this._global.db.close()
881
+ this._global = null
882
+ }
508
883
  }
509
884
  }
510
885
 
@@ -6,6 +6,7 @@
6
6
  import { MemoryEngine, createEngine, type EngineConfig } from '../core/engine.ts'
7
7
  import { Curator, createCurator, type CuratorConfig } from '../core/curator.ts'
8
8
  import { EmbeddingGenerator, createEmbeddings } from '../core/embeddings.ts'
9
+ import { Manager, createManager, type ManagerConfig } from '../core/manager.ts'
9
10
  import type { CurationTrigger } from '../types/memory.ts'
10
11
  import { logger } from '../utils/logger.ts'
11
12
 
@@ -16,6 +17,21 @@ export interface ServerConfig extends EngineConfig {
16
17
  port?: number
17
18
  host?: string
18
19
  curator?: CuratorConfig
20
+ manager?: ManagerConfig
21
+
22
+ /**
23
+ * Enable the management agent (convenience shortcut)
24
+ * When false, memories are stored but not organized/linked asynchronously
25
+ * Default: true
26
+ */
27
+ managerEnabled?: boolean
28
+
29
+ /**
30
+ * Enable personal memories extraction and storage (convenience shortcut)
31
+ * When false, personal/relationship memories are not extracted or surfaced
32
+ * Default: true
33
+ */
34
+ personalMemoriesEnabled?: boolean
19
35
  }
20
36
 
21
37
  /**
@@ -55,6 +71,9 @@ export async function createServer(config: ServerConfig = {}) {
55
71
  port = 8765,
56
72
  host = 'localhost',
57
73
  curator: curatorConfig,
74
+ manager: managerConfig,
75
+ managerEnabled,
76
+ personalMemoriesEnabled,
58
77
  ...engineConfig
59
78
  } = config
60
79
 
@@ -63,12 +82,27 @@ export async function createServer(config: ServerConfig = {}) {
63
82
  logger.info('Initializing embedding model (this may take a moment on first run)...')
64
83
  await embeddings.initialize()
65
84
 
66
- // Create engine with embedder
85
+ // Merge top-level convenience options with nested configs
86
+ const finalCuratorConfig: CuratorConfig = {
87
+ ...curatorConfig,
88
+ // Top-level option overrides nested if set
89
+ personalMemoriesEnabled: personalMemoriesEnabled ?? curatorConfig?.personalMemoriesEnabled,
90
+ }
91
+
92
+ const finalManagerConfig: ManagerConfig = {
93
+ ...managerConfig,
94
+ // Top-level option overrides nested if set
95
+ enabled: managerEnabled ?? managerConfig?.enabled,
96
+ }
97
+
98
+ // Create engine with embedder and personalMemoriesEnabled flag
67
99
  const engine = createEngine({
68
100
  ...engineConfig,
69
101
  embedder: embeddings.createEmbedder(),
102
+ personalMemoriesEnabled: finalCuratorConfig.personalMemoriesEnabled,
70
103
  })
71
- const curator = createCurator(curatorConfig)
104
+ const curator = createCurator(finalCuratorConfig)
105
+ const manager = createManager(finalManagerConfig)
72
106
 
73
107
  const server = Bun.serve({
74
108
  port,
@@ -180,6 +214,61 @@ export async function createServer(config: ServerConfig = {}) {
180
214
 
181
215
  logger.logCurationComplete(result.memories.length, result.session_summary)
182
216
  logger.logCuratedMemories(result.memories)
217
+
218
+ // Fire and forget - spawn management agent to update/organize memories
219
+ const sessionNumber = await engine.getSessionNumber(body.project_id, body.project_path)
220
+ // Get resolved storage paths from engine config (runtime values, not hardcoded)
221
+ const storagePaths = engine.getStoragePaths(body.project_id, body.project_path)
222
+
223
+ setImmediate(async () => {
224
+ try {
225
+ logger.logManagementStart(result.memories.length)
226
+ const startTime = Date.now()
227
+
228
+ const managementResult = await manager.manageWithCLI(
229
+ body.project_id,
230
+ sessionNumber,
231
+ result,
232
+ storagePaths
233
+ )
234
+
235
+ logger.logManagementComplete({
236
+ success: managementResult.success,
237
+ superseded: managementResult.superseded || undefined,
238
+ resolved: managementResult.resolved || undefined,
239
+ linked: managementResult.linked || undefined,
240
+ filesRead: managementResult.filesRead || undefined,
241
+ filesWritten: managementResult.filesWritten || undefined,
242
+ primerUpdated: managementResult.primerUpdated,
243
+ actions: managementResult.actions,
244
+ fullReport: managementResult.fullReport,
245
+ error: managementResult.error,
246
+ })
247
+
248
+ // Store management log with full action history (no truncation)
249
+ await engine.storeManagementLog({
250
+ projectId: body.project_id,
251
+ sessionNumber,
252
+ memoriesProcessed: result.memories.length,
253
+ supersededCount: managementResult.superseded,
254
+ resolvedCount: managementResult.resolved,
255
+ linkedCount: managementResult.linked,
256
+ primerUpdated: managementResult.primerUpdated,
257
+ success: managementResult.success,
258
+ durationMs: Date.now() - startTime,
259
+ summary: managementResult.summary,
260
+ fullReport: managementResult.fullReport,
261
+ error: managementResult.error,
262
+ details: {
263
+ actions: managementResult.actions,
264
+ filesRead: managementResult.filesRead,
265
+ filesWritten: managementResult.filesWritten,
266
+ },
267
+ })
268
+ } catch (error) {
269
+ logger.error(`Management failed: ${error}`)
270
+ }
271
+ })
183
272
  } else {
184
273
  logger.logCurationComplete(0)
185
274
  }
@@ -228,6 +317,7 @@ export async function createServer(config: ServerConfig = {}) {
228
317
  server,
229
318
  engine,
230
319
  curator,
320
+ manager,
231
321
  embeddings,
232
322
  stop: () => server.stop(),
233
323
  }
@@ -240,10 +330,20 @@ if (import.meta.main) {
240
330
  const storageMode = (process.env.MEMORY_STORAGE_MODE ?? 'central') as 'central' | 'local'
241
331
  const apiKey = process.env.ANTHROPIC_API_KEY
242
332
 
243
- await createServer({
244
- port,
245
- host,
246
- storageMode,
247
- curator: { apiKey },
248
- })
333
+ // Feature toggles (default: enabled)
334
+ // Set to '0' or 'false' to disable
335
+ const managerEnabled = !['0', 'false'].includes(process.env.MEMORY_MANAGER_ENABLED?.toLowerCase() ?? '')
336
+ const personalMemoriesEnabled = !['0', 'false'].includes(process.env.MEMORY_PERSONAL_ENABLED?.toLowerCase() ?? '')
337
+
338
+ // Wrap in async IIFE for CJS compatibility
339
+ void (async () => {
340
+ await createServer({
341
+ port,
342
+ host,
343
+ storageMode,
344
+ managerEnabled,
345
+ personalMemoriesEnabled,
346
+ curator: { apiKey },
347
+ })
348
+ })()
249
349
  }