supaclaw 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/dist/index.js ADDED
@@ -0,0 +1,2256 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.batchWithErrorHandling = exports.gracefulFallback = exports.withTimeout = exports.safeJsonParse = exports.validateInput = exports.wrapEmbeddingOperation = exports.wrapDatabaseOperation = exports.retry = exports.CircuitBreaker = exports.RateLimitError = exports.ValidationError = exports.EmbeddingError = exports.DatabaseError = exports.OpenClawError = exports.createLoggingMiddleware = exports.createClawdbotIntegration = exports.ClawdbotMemoryIntegration = exports.estimateTokensAccurate = exports.estimateTokens = exports.getBudgetForModel = exports.getContextStats = exports.formatContextWindow = exports.buildContextWindow = exports.createAdaptiveBudget = exports.createContextBudget = exports.OpenClawMemory = void 0;
7
+ const supabase_js_1 = require("@supabase/supabase-js");
8
+ const openai_1 = __importDefault(require("openai"));
9
+ const context_manager_1 = require("./context-manager");
10
+ class OpenClawMemory {
11
+ supabase;
12
+ agentId;
13
+ config;
14
+ openai;
15
+ constructor(config) {
16
+ this.supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseKey);
17
+ this.agentId = config.agentId;
18
+ this.config = config;
19
+ // Initialize OpenAI if API key provided
20
+ if (config.openaiApiKey) {
21
+ this.openai = new openai_1.default({ apiKey: config.openaiApiKey });
22
+ }
23
+ }
24
+ /**
25
+ * Generate embedding for text using configured provider
26
+ */
27
+ async generateEmbedding(text) {
28
+ if (!this.config.embeddingProvider || this.config.embeddingProvider === 'none') {
29
+ return null;
30
+ }
31
+ if (this.config.embeddingProvider === 'openai') {
32
+ if (!this.openai) {
33
+ throw new Error('OpenAI API key not provided');
34
+ }
35
+ const model = this.config.embeddingModel || 'text-embedding-3-small';
36
+ const response = await this.openai.embeddings.create({
37
+ model,
38
+ input: text,
39
+ });
40
+ return response.data[0].embedding;
41
+ }
42
+ // TODO: Add Voyage AI support
43
+ if (this.config.embeddingProvider === 'voyage') {
44
+ throw new Error('Voyage AI embeddings not yet implemented');
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Calculate cosine similarity between two vectors
50
+ */
51
+ cosineSimilarity(a, b) {
52
+ if (a.length !== b.length) {
53
+ throw new Error('Vectors must have the same length');
54
+ }
55
+ let dotProduct = 0;
56
+ let normA = 0;
57
+ let normB = 0;
58
+ for (let i = 0; i < a.length; i++) {
59
+ dotProduct += a[i] * b[i];
60
+ normA += a[i] * a[i];
61
+ normB += b[i] * b[i];
62
+ }
63
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
64
+ if (magnitude === 0)
65
+ return 0;
66
+ return dotProduct / magnitude;
67
+ }
68
+ /**
69
+ * Initialize database tables (run once)
70
+ */
71
+ async initialize() {
72
+ // Tables are created via migration SQL files
73
+ // This checks if tables exist
74
+ const { error } = await this.supabase
75
+ .from('sessions')
76
+ .select('id')
77
+ .limit(1);
78
+ if (error && error.code === '42P01') {
79
+ throw new Error('Tables not found. Run migrations first: npx openclaw-memory migrate');
80
+ }
81
+ }
82
+ // ============ SESSIONS ============
83
+ /**
84
+ * Start a new conversation session
85
+ */
86
+ async startSession(opts = {}) {
87
+ const { data, error } = await this.supabase
88
+ .from('sessions')
89
+ .insert({
90
+ agent_id: this.agentId,
91
+ user_id: opts.userId,
92
+ channel: opts.channel,
93
+ metadata: opts.metadata || {}
94
+ })
95
+ .select()
96
+ .single();
97
+ if (error)
98
+ throw error;
99
+ return data;
100
+ }
101
+ /**
102
+ * End a session with optional summary
103
+ */
104
+ async endSession(sessionId, opts = {}) {
105
+ let summary = opts.summary;
106
+ // Auto-generate summary if requested
107
+ if (opts.autoSummarize && !summary && this.openai) {
108
+ summary = await this.generateSessionSummary(sessionId);
109
+ }
110
+ const { data, error } = await this.supabase
111
+ .from('sessions')
112
+ .update({
113
+ ended_at: new Date().toISOString(),
114
+ summary
115
+ })
116
+ .eq('id', sessionId)
117
+ .select()
118
+ .single();
119
+ if (error)
120
+ throw error;
121
+ return data;
122
+ }
123
+ /**
124
+ * Generate an AI summary of a session
125
+ */
126
+ async generateSessionSummary(sessionId) {
127
+ if (!this.openai) {
128
+ throw new Error('OpenAI client required for auto-summarization');
129
+ }
130
+ const messages = await this.getMessages(sessionId);
131
+ if (messages.length === 0) {
132
+ return 'Empty session';
133
+ }
134
+ const conversation = messages
135
+ .map(m => `${m.role}: ${m.content}`)
136
+ .join('\n');
137
+ const response = await this.openai.chat.completions.create({
138
+ model: 'gpt-4o-mini',
139
+ messages: [
140
+ {
141
+ role: 'system',
142
+ content: 'Summarize this conversation in 2-3 sentences. Focus on key topics, decisions, and outcomes.'
143
+ },
144
+ {
145
+ role: 'user',
146
+ content: conversation
147
+ }
148
+ ],
149
+ max_tokens: 200
150
+ });
151
+ return response.choices[0]?.message?.content || 'Summary generation failed';
152
+ }
153
+ /**
154
+ * Resume a session (useful for continuing interrupted conversations)
155
+ */
156
+ async resumeSession(sessionId) {
157
+ const session = await this.getSession(sessionId);
158
+ if (!session) {
159
+ throw new Error(`Session ${sessionId} not found`);
160
+ }
161
+ const messages = await this.getMessages(sessionId);
162
+ // Build context summary
163
+ const contextParts = [];
164
+ if (session.summary) {
165
+ contextParts.push(`Previous summary: ${session.summary}`);
166
+ }
167
+ contextParts.push(`Message count: ${messages.length}`);
168
+ const lastMessages = messages.slice(-5);
169
+ if (lastMessages.length > 0) {
170
+ contextParts.push('Recent messages:');
171
+ lastMessages.forEach(m => {
172
+ contextParts.push(` ${m.role}: ${m.content.substring(0, 100)}...`);
173
+ });
174
+ }
175
+ return {
176
+ session,
177
+ messages,
178
+ context: contextParts.join('\n')
179
+ };
180
+ }
181
+ /**
182
+ * Search sessions by date range
183
+ */
184
+ async searchSessions(opts = {}) {
185
+ let query = this.supabase
186
+ .from('sessions')
187
+ .select()
188
+ .eq('agent_id', this.agentId)
189
+ .order('started_at', { ascending: false });
190
+ if (opts.userId) {
191
+ query = query.eq('user_id', opts.userId);
192
+ }
193
+ if (opts.channel) {
194
+ query = query.eq('channel', opts.channel);
195
+ }
196
+ if (opts.startDate) {
197
+ query = query.gte('started_at', opts.startDate);
198
+ }
199
+ if (opts.endDate) {
200
+ query = query.lte('started_at', opts.endDate);
201
+ }
202
+ query = query.range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 50) - 1);
203
+ const { data, error } = await query;
204
+ if (error)
205
+ throw error;
206
+ return data || [];
207
+ }
208
+ /**
209
+ * Export a session to markdown
210
+ */
211
+ async exportSessionToMarkdown(sessionId) {
212
+ const session = await this.getSession(sessionId);
213
+ if (!session) {
214
+ throw new Error(`Session ${sessionId} not found`);
215
+ }
216
+ const messages = await this.getMessages(sessionId);
217
+ const lines = [
218
+ `# Session ${session.id}`,
219
+ '',
220
+ `**Started:** ${new Date(session.started_at).toLocaleString()}`,
221
+ session.ended_at ? `**Ended:** ${new Date(session.ended_at).toLocaleString()}` : '**Status:** Active',
222
+ session.user_id ? `**User:** ${session.user_id}` : '',
223
+ session.channel ? `**Channel:** ${session.channel}` : '',
224
+ ''
225
+ ];
226
+ if (session.summary) {
227
+ lines.push(`## Summary`, '', session.summary, '');
228
+ }
229
+ lines.push(`## Messages (${messages.length})`, '');
230
+ messages.forEach(msg => {
231
+ const time = new Date(msg.created_at).toLocaleTimeString();
232
+ lines.push(`### ${msg.role} (${time})`);
233
+ lines.push('');
234
+ lines.push(msg.content);
235
+ lines.push('');
236
+ });
237
+ return lines.filter(Boolean).join('\n');
238
+ }
239
+ /**
240
+ * Import a session from markdown
241
+ */
242
+ async importSessionFromMarkdown(markdown, opts = {}) {
243
+ // Simple parser - expects format from exportSessionToMarkdown
244
+ const lines = markdown.split('\n');
245
+ // Start new session
246
+ const session = await this.startSession({
247
+ userId: opts.userId,
248
+ channel: opts.channel
249
+ });
250
+ // Parse messages (simple state machine)
251
+ let currentRole = 'user';
252
+ let currentContent = [];
253
+ for (const line of lines) {
254
+ const roleMatch = line.match(/^### (user|assistant|system|tool)/i);
255
+ if (roleMatch) {
256
+ // Save previous message if exists
257
+ if (currentContent.length > 0) {
258
+ await this.addMessage(session.id, {
259
+ role: currentRole,
260
+ content: currentContent.join('\n').trim()
261
+ });
262
+ currentContent = [];
263
+ }
264
+ currentRole = roleMatch[1].toLowerCase();
265
+ }
266
+ else if (line.startsWith('##') || line.startsWith('**')) {
267
+ // Skip headers and metadata
268
+ continue;
269
+ }
270
+ else {
271
+ currentContent.push(line);
272
+ }
273
+ }
274
+ // Save last message
275
+ if (currentContent.length > 0) {
276
+ await this.addMessage(session.id, {
277
+ role: currentRole,
278
+ content: currentContent.join('\n').trim()
279
+ });
280
+ }
281
+ return session;
282
+ }
283
+ /**
284
+ * Extract memories from a session
285
+ */
286
+ async extractMemoriesFromSession(sessionId, opts = {}) {
287
+ const messages = await this.getMessages(sessionId);
288
+ const memories = [];
289
+ if (opts.autoExtract && this.openai) {
290
+ // Use AI to extract key learnings
291
+ const conversation = messages
292
+ .map(m => `${m.role}: ${m.content}`)
293
+ .join('\n');
294
+ const response = await this.openai.chat.completions.create({
295
+ model: 'gpt-4o-mini',
296
+ messages: [
297
+ {
298
+ role: 'system',
299
+ content: `Extract key facts, decisions, and learnings from this conversation.
300
+ Return as JSON array: [{"content": "...", "category": "fact|decision|preference|learning", "importance": 0-1}]`
301
+ },
302
+ {
303
+ role: 'user',
304
+ content: conversation
305
+ }
306
+ ],
307
+ response_format: { type: 'json_object' }
308
+ });
309
+ const result = JSON.parse(response.choices[0]?.message?.content || '{"items":[]}');
310
+ const items = result.items || result.memories || [];
311
+ for (const item of items) {
312
+ if (item.importance >= (opts.minImportance || 0.5)) {
313
+ const memory = await this.remember({
314
+ content: item.content,
315
+ category: item.category,
316
+ importance: item.importance,
317
+ sessionId
318
+ });
319
+ memories.push(memory);
320
+ }
321
+ }
322
+ }
323
+ return memories;
324
+ }
325
+ /**
326
+ * Count tokens in a session
327
+ */
328
+ async countSessionTokens(sessionId) {
329
+ const messages = await this.getMessages(sessionId);
330
+ let totalTokens = 0;
331
+ for (const msg of messages) {
332
+ if (msg.token_count) {
333
+ totalTokens += msg.token_count;
334
+ }
335
+ else {
336
+ // Rough estimation: 1 token ≈ 4 characters
337
+ totalTokens += Math.ceil(msg.content.length / 4);
338
+ }
339
+ }
340
+ return {
341
+ totalTokens,
342
+ messageCount: messages.length,
343
+ averageTokensPerMessage: messages.length > 0
344
+ ? Math.round(totalTokens / messages.length)
345
+ : 0
346
+ };
347
+ }
348
+ /**
349
+ * Get a session by ID
350
+ */
351
+ async getSession(sessionId) {
352
+ const { data, error } = await this.supabase
353
+ .from('sessions')
354
+ .select()
355
+ .eq('id', sessionId)
356
+ .single();
357
+ if (error && error.code !== 'PGRST116')
358
+ throw error;
359
+ return data;
360
+ }
361
+ /**
362
+ * Get recent sessions
363
+ */
364
+ async getRecentSessions(opts = {}) {
365
+ let query = this.supabase
366
+ .from('sessions')
367
+ .select()
368
+ .eq('agent_id', this.agentId)
369
+ .order('started_at', { ascending: false })
370
+ .limit(opts.limit || 10);
371
+ if (opts.userId) {
372
+ query = query.eq('user_id', opts.userId);
373
+ }
374
+ const { data, error } = await query;
375
+ if (error)
376
+ throw error;
377
+ return data || [];
378
+ }
379
+ // ============ MESSAGES ============
380
+ /**
381
+ * Add a message to a session
382
+ */
383
+ async addMessage(sessionId, message) {
384
+ const { data, error } = await this.supabase
385
+ .from('messages')
386
+ .insert({
387
+ session_id: sessionId,
388
+ role: message.role,
389
+ content: message.content,
390
+ token_count: message.tokenCount,
391
+ metadata: message.metadata || {}
392
+ })
393
+ .select()
394
+ .single();
395
+ if (error)
396
+ throw error;
397
+ return data;
398
+ }
399
+ /**
400
+ * Get messages from a session
401
+ */
402
+ async getMessages(sessionId, opts = {}) {
403
+ const { data, error } = await this.supabase
404
+ .from('messages')
405
+ .select()
406
+ .eq('session_id', sessionId)
407
+ .order('created_at', { ascending: true })
408
+ .range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 100) - 1);
409
+ if (error)
410
+ throw error;
411
+ return data || [];
412
+ }
413
+ // ============ MEMORIES ============
414
+ /**
415
+ * Store a long-term memory with semantic embedding
416
+ */
417
+ async remember(memory) {
418
+ // Generate embedding if provider configured
419
+ const embedding = await this.generateEmbedding(memory.content);
420
+ const { data, error } = await this.supabase
421
+ .from('memories')
422
+ .insert({
423
+ agent_id: this.agentId,
424
+ user_id: memory.userId,
425
+ category: memory.category,
426
+ content: memory.content,
427
+ importance: memory.importance ?? 0.5,
428
+ source_session_id: memory.sessionId,
429
+ expires_at: memory.expiresAt,
430
+ embedding,
431
+ metadata: memory.metadata || {}
432
+ })
433
+ .select()
434
+ .single();
435
+ if (error)
436
+ throw error;
437
+ return data;
438
+ }
439
+ /**
440
+ * Search memories using vector similarity (semantic search)
441
+ */
442
+ async recall(query, opts = {}) {
443
+ // Generate query embedding for semantic search
444
+ const queryEmbedding = await this.generateEmbedding(query);
445
+ if (queryEmbedding) {
446
+ // Use pgvector for semantic search
447
+ const { data, error } = await this.supabase.rpc('match_memories', {
448
+ query_embedding: queryEmbedding,
449
+ match_threshold: opts.minSimilarity ?? 0.7,
450
+ match_count: opts.limit || 10,
451
+ p_agent_id: this.agentId,
452
+ p_user_id: opts.userId,
453
+ p_category: opts.category,
454
+ p_min_importance: opts.minImportance
455
+ });
456
+ if (error)
457
+ throw error;
458
+ return data || [];
459
+ }
460
+ // Fallback to text search when no embeddings available
461
+ let q = this.supabase
462
+ .from('memories')
463
+ .select()
464
+ .eq('agent_id', this.agentId)
465
+ .order('importance', { ascending: false })
466
+ .order('created_at', { ascending: false })
467
+ .limit(opts.limit || 10);
468
+ if (opts.userId) {
469
+ q = q.or(`user_id.eq.${opts.userId},user_id.is.null`);
470
+ }
471
+ if (opts.category) {
472
+ q = q.eq('category', opts.category);
473
+ }
474
+ if (opts.minImportance) {
475
+ q = q.gte('importance', opts.minImportance);
476
+ }
477
+ // Text search filter
478
+ q = q.ilike('content', `%${query}%`);
479
+ const { data, error } = await q;
480
+ if (error)
481
+ throw error;
482
+ return data || [];
483
+ }
484
+ /**
485
+ * Hybrid search: combines semantic similarity and keyword matching
486
+ * Returns deduplicated results sorted by relevance score
487
+ */
488
+ async hybridRecall(query, opts = {}) {
489
+ const vectorWeight = opts.vectorWeight ?? 0.7;
490
+ const keywordWeight = opts.keywordWeight ?? 0.3;
491
+ // Generate query embedding
492
+ const queryEmbedding = await this.generateEmbedding(query);
493
+ if (queryEmbedding) {
494
+ // Use hybrid search RPC function
495
+ const { data, error } = await this.supabase.rpc('hybrid_search_memories', {
496
+ query_embedding: queryEmbedding,
497
+ query_text: query,
498
+ vector_weight: vectorWeight,
499
+ keyword_weight: keywordWeight,
500
+ match_count: opts.limit || 10,
501
+ p_agent_id: this.agentId,
502
+ p_user_id: opts.userId,
503
+ p_category: opts.category,
504
+ p_min_importance: opts.minImportance
505
+ });
506
+ if (error)
507
+ throw error;
508
+ return data || [];
509
+ }
510
+ // Fallback to regular recall if no embeddings
511
+ return this.recall(query, opts);
512
+ }
513
+ /**
514
+ * Delete a memory
515
+ */
516
+ async forget(memoryId) {
517
+ const { error } = await this.supabase
518
+ .from('memories')
519
+ .delete()
520
+ .eq('id', memoryId);
521
+ if (error)
522
+ throw error;
523
+ }
524
+ /**
525
+ * Get all memories (paginated)
526
+ */
527
+ async getMemories(opts = {}) {
528
+ let query = this.supabase
529
+ .from('memories')
530
+ .select()
531
+ .eq('agent_id', this.agentId)
532
+ .order('created_at', { ascending: false })
533
+ .range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 50) - 1);
534
+ if (opts.userId) {
535
+ query = query.eq('user_id', opts.userId);
536
+ }
537
+ if (opts.category) {
538
+ query = query.eq('category', opts.category);
539
+ }
540
+ const { data, error } = await query;
541
+ if (error)
542
+ throw error;
543
+ return data || [];
544
+ }
545
+ /**
546
+ * Find memories similar to an existing memory
547
+ * Useful for context expansion and deduplication
548
+ */
549
+ async findSimilarMemories(memoryId, opts = {}) {
550
+ const { data, error } = await this.supabase.rpc('find_similar_memories', {
551
+ memory_id: memoryId,
552
+ match_threshold: opts.minSimilarity ?? 0.8,
553
+ match_count: opts.limit || 5
554
+ });
555
+ if (error)
556
+ throw error;
557
+ return data || [];
558
+ }
559
+ // ============ TASKS ============
560
+ /**
561
+ * Create a task
562
+ */
563
+ async createTask(task) {
564
+ const { data, error } = await this.supabase
565
+ .from('tasks')
566
+ .insert({
567
+ agent_id: this.agentId,
568
+ user_id: task.userId,
569
+ title: task.title,
570
+ description: task.description,
571
+ priority: task.priority ?? 0,
572
+ due_at: task.dueAt,
573
+ parent_task_id: task.parentTaskId,
574
+ metadata: task.metadata || {}
575
+ })
576
+ .select()
577
+ .single();
578
+ if (error)
579
+ throw error;
580
+ return data;
581
+ }
582
+ /**
583
+ * Update a task
584
+ */
585
+ async updateTask(taskId, updates) {
586
+ const updateData = {
587
+ updated_at: new Date().toISOString()
588
+ };
589
+ if (updates.title)
590
+ updateData.title = updates.title;
591
+ if (updates.description)
592
+ updateData.description = updates.description;
593
+ if (updates.status) {
594
+ updateData.status = updates.status;
595
+ if (updates.status === 'done') {
596
+ updateData.completed_at = new Date().toISOString();
597
+ }
598
+ }
599
+ if (updates.priority !== undefined)
600
+ updateData.priority = updates.priority;
601
+ if (updates.dueAt)
602
+ updateData.due_at = updates.dueAt;
603
+ if (updates.metadata)
604
+ updateData.metadata = updates.metadata;
605
+ const { data, error } = await this.supabase
606
+ .from('tasks')
607
+ .update(updateData)
608
+ .eq('id', taskId)
609
+ .select()
610
+ .single();
611
+ if (error)
612
+ throw error;
613
+ return data;
614
+ }
615
+ /**
616
+ * Get tasks
617
+ */
618
+ async getTasks(opts = {}) {
619
+ let query = this.supabase
620
+ .from('tasks')
621
+ .select()
622
+ .eq('agent_id', this.agentId)
623
+ .order('priority', { ascending: false })
624
+ .order('created_at', { ascending: false })
625
+ .limit(opts.limit || 50);
626
+ if (opts.status) {
627
+ query = query.eq('status', opts.status);
628
+ }
629
+ if (opts.userId) {
630
+ query = query.eq('user_id', opts.userId);
631
+ }
632
+ const { data, error } = await query;
633
+ if (error)
634
+ throw error;
635
+ return data || [];
636
+ }
637
+ /**
638
+ * Delete a task
639
+ */
640
+ async deleteTask(taskId) {
641
+ const { error } = await this.supabase
642
+ .from('tasks')
643
+ .delete()
644
+ .eq('id', taskId);
645
+ if (error)
646
+ throw error;
647
+ }
648
+ /**
649
+ * Get subtasks of a parent task
650
+ */
651
+ async getSubtasks(parentTaskId) {
652
+ const { data, error } = await this.supabase
653
+ .from('tasks')
654
+ .select()
655
+ .eq('parent_task_id', parentTaskId)
656
+ .order('priority', { ascending: false })
657
+ .order('created_at', { ascending: false });
658
+ if (error)
659
+ throw error;
660
+ return data || [];
661
+ }
662
+ /**
663
+ * Get task with all its subtasks (hierarchical)
664
+ */
665
+ async getTaskWithSubtasks(taskId) {
666
+ const task = await this.supabase
667
+ .from('tasks')
668
+ .select()
669
+ .eq('id', taskId)
670
+ .single();
671
+ if (task.error)
672
+ throw task.error;
673
+ const subtasks = await this.getSubtasks(taskId);
674
+ return {
675
+ task: task.data,
676
+ subtasks
677
+ };
678
+ }
679
+ /**
680
+ * Get upcoming tasks (due soon)
681
+ */
682
+ async getUpcomingTasks(opts = {}) {
683
+ const now = new Date();
684
+ const future = new Date(now.getTime() + (opts.hoursAhead || 24) * 60 * 60 * 1000);
685
+ let query = this.supabase
686
+ .from('tasks')
687
+ .select()
688
+ .eq('agent_id', this.agentId)
689
+ .neq('status', 'done')
690
+ .not('due_at', 'is', null)
691
+ .gte('due_at', now.toISOString())
692
+ .lte('due_at', future.toISOString())
693
+ .order('due_at', { ascending: true });
694
+ if (opts.userId) {
695
+ query = query.eq('user_id', opts.userId);
696
+ }
697
+ const { data, error } = await query;
698
+ if (error)
699
+ throw error;
700
+ return data || [];
701
+ }
702
+ // ============ LEARNINGS ============
703
+ /**
704
+ * Record a learning
705
+ */
706
+ async learn(learning) {
707
+ const { data, error } = await this.supabase
708
+ .from('learnings')
709
+ .insert({
710
+ agent_id: this.agentId,
711
+ category: learning.category,
712
+ trigger: learning.trigger,
713
+ lesson: learning.lesson,
714
+ action: learning.action,
715
+ severity: learning.severity ?? 'info',
716
+ source_session_id: learning.sessionId,
717
+ metadata: learning.metadata || {}
718
+ })
719
+ .select()
720
+ .single();
721
+ if (error)
722
+ throw error;
723
+ return data;
724
+ }
725
+ /**
726
+ * Get learnings
727
+ */
728
+ async getLearnings(opts = {}) {
729
+ let query = this.supabase
730
+ .from('learnings')
731
+ .select()
732
+ .eq('agent_id', this.agentId)
733
+ .order('created_at', { ascending: false })
734
+ .limit(opts.limit || 50);
735
+ if (opts.category) {
736
+ query = query.eq('category', opts.category);
737
+ }
738
+ if (opts.severity) {
739
+ query = query.eq('severity', opts.severity);
740
+ }
741
+ const { data, error } = await query;
742
+ if (error)
743
+ throw error;
744
+ return data || [];
745
+ }
746
+ /**
747
+ * Search learnings by topic for context
748
+ */
749
+ async searchLearnings(query, opts = {}) {
750
+ const { data, error } = await this.supabase
751
+ .from('learnings')
752
+ .select()
753
+ .eq('agent_id', this.agentId)
754
+ .or(`trigger.ilike.%${query}%,lesson.ilike.%${query}%,action.ilike.%${query}%`)
755
+ .order('created_at', { ascending: false })
756
+ .limit(opts.limit || 10);
757
+ if (error)
758
+ throw error;
759
+ return data || [];
760
+ }
761
+ /**
762
+ * Mark a learning as applied (increments applied_count)
763
+ */
764
+ async applyLearning(learningId) {
765
+ const { data, error } = await this.supabase.rpc('increment_learning_applied', {
766
+ learning_id: learningId
767
+ });
768
+ if (error) {
769
+ // Fallback if RPC doesn't exist
770
+ const learning = await this.supabase
771
+ .from('learnings')
772
+ .select()
773
+ .eq('id', learningId)
774
+ .single();
775
+ if (learning.error)
776
+ throw learning.error;
777
+ const updated = await this.supabase
778
+ .from('learnings')
779
+ .update({ applied_count: (learning.data.applied_count || 0) + 1 })
780
+ .eq('id', learningId)
781
+ .select()
782
+ .single();
783
+ if (updated.error)
784
+ throw updated.error;
785
+ return updated.data;
786
+ }
787
+ return data;
788
+ }
789
+ // ============ TASK DEPENDENCIES ============
790
+ /**
791
+ * Add a task dependency (taskId depends on dependsOnTaskId)
792
+ */
793
+ async addTaskDependency(taskId, dependsOnTaskId) {
794
+ // Store in metadata
795
+ const task = await this.supabase
796
+ .from('tasks')
797
+ .select()
798
+ .eq('id', taskId)
799
+ .single();
800
+ if (task.error)
801
+ throw task.error;
802
+ const dependencies = task.data.metadata?.dependencies || [];
803
+ if (!dependencies.includes(dependsOnTaskId)) {
804
+ dependencies.push(dependsOnTaskId);
805
+ }
806
+ await this.supabase
807
+ .from('tasks')
808
+ .update({
809
+ metadata: {
810
+ ...task.data.metadata,
811
+ dependencies
812
+ }
813
+ })
814
+ .eq('id', taskId);
815
+ }
816
+ /**
817
+ * Remove a task dependency
818
+ */
819
+ async removeTaskDependency(taskId, dependsOnTaskId) {
820
+ const task = await this.supabase
821
+ .from('tasks')
822
+ .select()
823
+ .eq('id', taskId)
824
+ .single();
825
+ if (task.error)
826
+ throw task.error;
827
+ const dependencies = task.data.metadata?.dependencies || [];
828
+ const filtered = dependencies.filter(id => id !== dependsOnTaskId);
829
+ await this.supabase
830
+ .from('tasks')
831
+ .update({
832
+ metadata: {
833
+ ...task.data.metadata,
834
+ dependencies: filtered
835
+ }
836
+ })
837
+ .eq('id', taskId);
838
+ }
839
+ /**
840
+ * Get task dependencies
841
+ */
842
+ async getTaskDependencies(taskId) {
843
+ const task = await this.supabase
844
+ .from('tasks')
845
+ .select()
846
+ .eq('id', taskId)
847
+ .single();
848
+ if (task.error)
849
+ throw task.error;
850
+ const dependencyIds = task.data.metadata?.dependencies || [];
851
+ if (dependencyIds.length === 0)
852
+ return [];
853
+ const { data, error } = await this.supabase
854
+ .from('tasks')
855
+ .select()
856
+ .in('id', dependencyIds);
857
+ if (error)
858
+ throw error;
859
+ return data || [];
860
+ }
861
+ /**
862
+ * Check if a task is blocked by uncompleted dependencies
863
+ */
864
+ async isTaskBlocked(taskId) {
865
+ const dependencies = await this.getTaskDependencies(taskId);
866
+ return dependencies.some(dep => dep.status !== 'done');
867
+ }
868
+ /**
869
+ * Get tasks that are ready to start (no blocking dependencies)
870
+ */
871
+ async getReadyTasks(opts = {}) {
872
+ const tasks = await this.getTasks({ status: 'pending', userId: opts.userId });
873
+ const ready = [];
874
+ for (const task of tasks) {
875
+ const blocked = await this.isTaskBlocked(task.id);
876
+ if (!blocked) {
877
+ ready.push(task);
878
+ }
879
+ }
880
+ return ready;
881
+ }
882
+ // ============ TASK TEMPLATES ============
883
+ /**
884
+ * Create a task template
885
+ */
886
+ async createTaskTemplate(template) {
887
+ // Store as a special task with metadata flag
888
+ const { data, error } = await this.supabase
889
+ .from('tasks')
890
+ .insert({
891
+ agent_id: this.agentId,
892
+ title: `[TEMPLATE] ${template.name}`,
893
+ description: template.description,
894
+ status: 'pending',
895
+ priority: -1, // Templates have negative priority
896
+ metadata: {
897
+ is_template: true,
898
+ template_data: template,
899
+ ...template.metadata
900
+ }
901
+ })
902
+ .select()
903
+ .single();
904
+ if (error)
905
+ throw error;
906
+ return { id: data.id };
907
+ }
908
+ /**
909
+ * Get all task templates
910
+ */
911
+ async getTaskTemplates() {
912
+ const { data, error } = await this.supabase
913
+ .from('tasks')
914
+ .select()
915
+ .eq('agent_id', this.agentId)
916
+ .eq('metadata->>is_template', 'true');
917
+ if (error)
918
+ throw error;
919
+ return (data || []).map(task => ({
920
+ id: task.id,
921
+ name: task.title.replace('[TEMPLATE] ', ''),
922
+ description: task.description,
923
+ tasks: task.metadata?.template_data?.tasks || []
924
+ }));
925
+ }
926
+ /**
927
+ * Apply a task template (create all tasks from template)
928
+ */
929
+ async applyTaskTemplate(templateId, opts = {}) {
930
+ const template = await this.supabase
931
+ .from('tasks')
932
+ .select()
933
+ .eq('id', templateId)
934
+ .single();
935
+ if (template.error)
936
+ throw template.error;
937
+ const templateData = template.data.metadata?.template_data;
938
+ if (!templateData?.tasks) {
939
+ throw new Error('Invalid template data');
940
+ }
941
+ const createdTasks = [];
942
+ const taskIdMap = new Map(); // template index -> created task id
943
+ // Create all tasks first
944
+ for (let i = 0; i < templateData.tasks.length; i++) {
945
+ const taskDef = templateData.tasks[i];
946
+ let dueAt;
947
+ if (opts.startDate && taskDef.estimatedDuration) {
948
+ // Calculate due date based on start date + duration
949
+ const start = new Date(opts.startDate);
950
+ const durationHours = parseInt(taskDef.estimatedDuration) || 24;
951
+ dueAt = new Date(start.getTime() + durationHours * 60 * 60 * 1000).toISOString();
952
+ }
953
+ const task = await this.createTask({
954
+ title: taskDef.title,
955
+ description: taskDef.description,
956
+ priority: taskDef.priority || 0,
957
+ dueAt,
958
+ userId: opts.userId,
959
+ metadata: {
960
+ from_template: templateId,
961
+ template_index: i,
962
+ ...opts.metadata
963
+ }
964
+ });
965
+ createdTasks.push(task);
966
+ taskIdMap.set(i, task.id);
967
+ }
968
+ // Now add dependencies
969
+ for (let i = 0; i < templateData.tasks.length; i++) {
970
+ const taskDef = templateData.tasks[i];
971
+ if (taskDef.dependencies && taskDef.dependencies.length > 0) {
972
+ const taskId = taskIdMap.get(i);
973
+ if (taskId) {
974
+ for (const depIndex of taskDef.dependencies) {
975
+ const depId = taskIdMap.get(depIndex);
976
+ if (depId) {
977
+ await this.addTaskDependency(taskId, depId);
978
+ }
979
+ }
980
+ }
981
+ }
982
+ }
983
+ return createdTasks;
984
+ }
985
+ // ============ TASK REMINDERS ============
986
+ /**
987
+ * Get tasks that need reminders (due soon but not done)
988
+ */
989
+ async getTasksNeedingReminders(opts = {}) {
990
+ const tasks = await this.getUpcomingTasks({
991
+ userId: opts.userId,
992
+ hoursAhead: opts.hoursAhead || 24
993
+ });
994
+ const now = Date.now();
995
+ return tasks.map(task => ({
996
+ ...task,
997
+ timeUntilDue: task.due_at ? new Date(task.due_at).getTime() - now : 0
998
+ })).filter(task => task.timeUntilDue > 0);
999
+ }
1000
+ /**
1001
+ * Format task reminder message
1002
+ */
1003
+ formatTaskReminder(task, timeUntilDue) {
1004
+ const hours = Math.floor(timeUntilDue / (60 * 60 * 1000));
1005
+ const minutes = Math.floor((timeUntilDue % (60 * 60 * 1000)) / (60 * 1000));
1006
+ let timeStr = '';
1007
+ if (hours > 0) {
1008
+ timeStr = `${hours}h ${minutes}m`;
1009
+ }
1010
+ else {
1011
+ timeStr = `${minutes}m`;
1012
+ }
1013
+ return `⏰ Task reminder: "${task.title}" is due in ${timeStr}\n${task.description || ''}`;
1014
+ }
1015
+ // ============ LEARNING PATTERN DETECTION ============
1016
+ /**
1017
+ * Detect patterns in learnings (common categories, triggers, lessons)
1018
+ */
1019
+ async detectLearningPatterns() {
1020
+ const learnings = await this.getLearnings({ limit: 1000 });
1021
+ // Category distribution
1022
+ const categoryMap = new Map();
1023
+ learnings.forEach(l => {
1024
+ categoryMap.set(l.category, (categoryMap.get(l.category) || 0) + 1);
1025
+ });
1026
+ const commonCategories = Array.from(categoryMap.entries())
1027
+ .map(([category, count]) => ({ category, count }))
1028
+ .sort((a, b) => b.count - a.count);
1029
+ // Trigger patterns (extract common words)
1030
+ const triggerWords = new Map();
1031
+ learnings.forEach(l => {
1032
+ const words = l.trigger.toLowerCase().split(/\s+/)
1033
+ .filter(w => w.length > 4); // Only words longer than 4 chars
1034
+ words.forEach(word => {
1035
+ triggerWords.set(word, (triggerWords.get(word) || 0) + 1);
1036
+ });
1037
+ });
1038
+ const commonTriggers = Array.from(triggerWords.entries())
1039
+ .map(([pattern, count]) => ({ pattern, count }))
1040
+ .sort((a, b) => b.count - a.count)
1041
+ .slice(0, 10);
1042
+ // Recent trends by week
1043
+ const weekMap = new Map();
1044
+ learnings.forEach(l => {
1045
+ const date = new Date(l.created_at);
1046
+ const weekStart = new Date(date);
1047
+ weekStart.setDate(date.getDate() - date.getDay());
1048
+ const weekKey = weekStart.toISOString().split('T')[0];
1049
+ const existing = weekMap.get(weekKey) || { count: 0, severities: [] };
1050
+ existing.count++;
1051
+ existing.severities.push(l.severity);
1052
+ weekMap.set(weekKey, existing);
1053
+ });
1054
+ const recentTrends = Array.from(weekMap.entries())
1055
+ .map(([week, data]) => ({
1056
+ week,
1057
+ count: data.count,
1058
+ severity: data.severities.filter(s => s === 'critical').length > 0
1059
+ ? 'critical'
1060
+ : data.severities.filter(s => s === 'warning').length > 0
1061
+ ? 'warning'
1062
+ : 'info'
1063
+ }))
1064
+ .sort((a, b) => b.week.localeCompare(a.week))
1065
+ .slice(0, 8);
1066
+ // Top applied lessons
1067
+ const topLessons = learnings
1068
+ .filter(l => l.applied_count > 0)
1069
+ .sort((a, b) => b.applied_count - a.applied_count)
1070
+ .slice(0, 10)
1071
+ .map(l => ({
1072
+ lesson: l.lesson,
1073
+ applied: l.applied_count,
1074
+ id: l.id
1075
+ }));
1076
+ return {
1077
+ commonCategories,
1078
+ commonTriggers,
1079
+ recentTrends,
1080
+ topLessons
1081
+ };
1082
+ }
1083
+ /**
1084
+ * Get learning recommendations based on current context
1085
+ */
1086
+ async getLearningRecommendations(context, limit = 5) {
1087
+ const learnings = await this.searchLearnings(context, { limit: limit * 2 });
1088
+ // Score and rank by relevance + application count
1089
+ const scored = learnings.map(l => ({
1090
+ learning: l,
1091
+ score: (l.applied_count || 0) * 0.3 + // Boost frequently applied learnings
1092
+ (l.severity === 'critical' ? 1.5 : l.severity === 'warning' ? 1.2 : 1.0)
1093
+ }));
1094
+ return scored
1095
+ .sort((a, b) => b.score - a.score)
1096
+ .slice(0, limit)
1097
+ .map(s => s.learning);
1098
+ }
1099
+ // ============ LEARNING SIMILARITY SEARCH ============
1100
+ /**
1101
+ * Find similar learnings using embeddings
1102
+ */
1103
+ async findSimilarLearnings(learningId, opts = {}) {
1104
+ const learning = await this.supabase
1105
+ .from('learnings')
1106
+ .select()
1107
+ .eq('id', learningId)
1108
+ .single();
1109
+ if (learning.error)
1110
+ throw learning.error;
1111
+ // Generate embedding for the learning
1112
+ const text = `${learning.data.trigger} ${learning.data.lesson} ${learning.data.action || ''}`;
1113
+ const embedding = await this.generateEmbedding(text);
1114
+ if (!embedding) {
1115
+ throw new Error('Failed to generate embedding for learning');
1116
+ }
1117
+ // Store embedding in metadata for future use
1118
+ await this.supabase
1119
+ .from('learnings')
1120
+ .update({
1121
+ metadata: {
1122
+ ...learning.data.metadata,
1123
+ embedding
1124
+ }
1125
+ })
1126
+ .eq('id', learningId);
1127
+ // Search for similar learnings
1128
+ const { data, error } = await this.supabase
1129
+ .from('learnings')
1130
+ .select()
1131
+ .eq('agent_id', this.agentId)
1132
+ .neq('id', learningId);
1133
+ if (error)
1134
+ throw error;
1135
+ // Calculate similarities
1136
+ const similarities = [];
1137
+ for (const l of data || []) {
1138
+ // Get or generate embedding
1139
+ const lText = `${l.trigger} ${l.lesson} ${l.action || ''}`;
1140
+ let lEmbedding;
1141
+ if (l.metadata?.embedding && Array.isArray(l.metadata.embedding)) {
1142
+ lEmbedding = l.metadata.embedding;
1143
+ }
1144
+ else {
1145
+ // Generate embedding on the fly
1146
+ lEmbedding = await this.generateEmbedding(lText);
1147
+ if (lEmbedding) {
1148
+ // Cache it
1149
+ await this.supabase
1150
+ .from('learnings')
1151
+ .update({
1152
+ metadata: {
1153
+ ...l.metadata,
1154
+ embedding: lEmbedding
1155
+ }
1156
+ })
1157
+ .eq('id', l.id);
1158
+ }
1159
+ }
1160
+ if (lEmbedding !== null && lEmbedding.length > 0) {
1161
+ const similarity = this.cosineSimilarity(embedding, lEmbedding);
1162
+ similarities.push({ ...l, similarity });
1163
+ }
1164
+ }
1165
+ return similarities
1166
+ .filter(s => s.similarity >= (opts.threshold || 0.7))
1167
+ .sort((a, b) => b.similarity - a.similarity)
1168
+ .slice(0, opts.limit || 5);
1169
+ }
1170
+ // ============ LEARNING EXPORT/REPORT ============
1171
+ /**
1172
+ * Export learnings to markdown report
1173
+ */
1174
+ async exportLearningsReport(opts = {}) {
1175
+ let learnings = await this.getLearnings({
1176
+ category: opts.category,
1177
+ severity: opts.severity,
1178
+ limit: 1000
1179
+ });
1180
+ if (opts.since) {
1181
+ const sinceDate = new Date(opts.since);
1182
+ learnings = learnings.filter(l => new Date(l.created_at) >= sinceDate);
1183
+ }
1184
+ const patterns = await this.detectLearningPatterns();
1185
+ let report = `# Learning Report\n\n`;
1186
+ report += `**Generated:** ${new Date().toISOString()}\n`;
1187
+ report += `**Total Learnings:** ${learnings.length}\n\n`;
1188
+ // Patterns section
1189
+ report += `## Patterns\n\n`;
1190
+ report += `### Categories\n`;
1191
+ patterns.commonCategories.forEach(c => {
1192
+ report += `- ${c.category}: ${c.count}\n`;
1193
+ });
1194
+ report += `\n### Common Triggers\n`;
1195
+ patterns.commonTriggers.forEach(t => {
1196
+ report += `- "${t.pattern}": ${t.count} occurrences\n`;
1197
+ });
1198
+ report += `\n### Top Applied Lessons\n`;
1199
+ patterns.topLessons.forEach(l => {
1200
+ report += `- "${l.lesson}" (applied ${l.applied} times)\n`;
1201
+ });
1202
+ // Individual learnings by category
1203
+ report += `\n## All Learnings\n\n`;
1204
+ const byCategory = new Map();
1205
+ learnings.forEach(l => {
1206
+ const cat = byCategory.get(l.category) || [];
1207
+ cat.push(l);
1208
+ byCategory.set(l.category, cat);
1209
+ });
1210
+ byCategory.forEach((items, category) => {
1211
+ report += `### ${category.toUpperCase()}\n\n`;
1212
+ items.forEach(l => {
1213
+ report += `**[${l.severity.toUpperCase()}]** ${l.trigger}\n`;
1214
+ report += `- Lesson: ${l.lesson}\n`;
1215
+ if (l.action) {
1216
+ report += `- Action: ${l.action}\n`;
1217
+ }
1218
+ report += `- Applied: ${l.applied_count} times\n`;
1219
+ report += `- Created: ${new Date(l.created_at).toLocaleDateString()}\n\n`;
1220
+ });
1221
+ });
1222
+ return report;
1223
+ }
1224
+ /**
1225
+ * Export learnings to JSON
1226
+ */
1227
+ async exportLearningsJSON(opts = {}) {
1228
+ let learnings = await this.getLearnings({
1229
+ category: opts.category,
1230
+ severity: opts.severity,
1231
+ limit: 1000
1232
+ });
1233
+ if (opts.since) {
1234
+ const sinceDate = new Date(opts.since);
1235
+ learnings = learnings.filter(l => new Date(l.created_at) >= sinceDate);
1236
+ }
1237
+ const patterns = await this.detectLearningPatterns();
1238
+ return {
1239
+ generated: new Date().toISOString(),
1240
+ total: learnings.length,
1241
+ patterns,
1242
+ learnings
1243
+ };
1244
+ }
1245
+ // ============ ENTITIES ============
1246
+ /**
1247
+ * Extract entities from text using AI
1248
+ */
1249
+ async extractEntities(text, opts = {}) {
1250
+ if (!this.openai) {
1251
+ throw new Error('OpenAI client required for entity extraction');
1252
+ }
1253
+ const response = await this.openai.chat.completions.create({
1254
+ model: 'gpt-4o-mini',
1255
+ messages: [
1256
+ {
1257
+ role: 'system',
1258
+ content: `Extract named entities from the text. Return JSON array of entities.
1259
+ Each entity should have: type (person|place|organization|product|concept), name, description.
1260
+ Focus on important entities that should be remembered.
1261
+ Format: {"entities": [{"type": "...", "name": "...", "description": "..."}]}`
1262
+ },
1263
+ {
1264
+ role: 'user',
1265
+ content: text
1266
+ }
1267
+ ],
1268
+ response_format: { type: 'json_object' }
1269
+ });
1270
+ const result = JSON.parse(response.choices[0]?.message?.content || '{"entities":[]}');
1271
+ const extractedEntities = result.entities || [];
1272
+ const entities = [];
1273
+ for (const e of extractedEntities) {
1274
+ // Check if entity already exists (by name, case-insensitive)
1275
+ const existing = await this.findEntity(e.name);
1276
+ if (existing) {
1277
+ // Update existing entity
1278
+ const updated = await this.updateEntity(existing.id, {
1279
+ description: e.description,
1280
+ lastSeenAt: new Date().toISOString()
1281
+ });
1282
+ entities.push(updated);
1283
+ }
1284
+ else {
1285
+ // Create new entity
1286
+ const entity = await this.createEntity({
1287
+ entityType: e.type,
1288
+ name: e.name,
1289
+ description: e.description
1290
+ });
1291
+ entities.push(entity);
1292
+ }
1293
+ }
1294
+ return entities;
1295
+ }
1296
+ /**
1297
+ * Create an entity
1298
+ */
1299
+ async createEntity(entity) {
1300
+ const { data, error } = await this.supabase
1301
+ .from('entities')
1302
+ .insert({
1303
+ agent_id: this.agentId,
1304
+ entity_type: entity.entityType,
1305
+ name: entity.name,
1306
+ aliases: entity.aliases || [],
1307
+ description: entity.description,
1308
+ properties: entity.properties || {},
1309
+ first_seen_at: new Date().toISOString(),
1310
+ last_seen_at: new Date().toISOString(),
1311
+ mention_count: 1
1312
+ })
1313
+ .select()
1314
+ .single();
1315
+ if (error)
1316
+ throw error;
1317
+ return data;
1318
+ }
1319
+ /**
1320
+ * Update an entity
1321
+ */
1322
+ async updateEntity(entityId, updates) {
1323
+ const updateData = {};
1324
+ if (updates.name)
1325
+ updateData.name = updates.name;
1326
+ if (updates.aliases)
1327
+ updateData.aliases = updates.aliases;
1328
+ if (updates.description)
1329
+ updateData.description = updates.description;
1330
+ if (updates.properties)
1331
+ updateData.properties = updates.properties;
1332
+ if (updates.lastSeenAt)
1333
+ updateData.last_seen_at = updates.lastSeenAt;
1334
+ // Increment mention count
1335
+ const entity = await this.supabase
1336
+ .from('entities')
1337
+ .select()
1338
+ .eq('id', entityId)
1339
+ .single();
1340
+ if (entity.error)
1341
+ throw entity.error;
1342
+ updateData.mention_count = (entity.data.mention_count || 0) + 1;
1343
+ const { data, error } = await this.supabase
1344
+ .from('entities')
1345
+ .update(updateData)
1346
+ .eq('id', entityId)
1347
+ .select()
1348
+ .single();
1349
+ if (error)
1350
+ throw error;
1351
+ return data;
1352
+ }
1353
+ /**
1354
+ * Find an entity by name or alias
1355
+ */
1356
+ async findEntity(nameOrAlias) {
1357
+ const { data, error } = await this.supabase
1358
+ .from('entities')
1359
+ .select()
1360
+ .eq('agent_id', this.agentId)
1361
+ .or(`name.ilike.${nameOrAlias},aliases.cs.{${nameOrAlias}}`)
1362
+ .limit(1)
1363
+ .single();
1364
+ if (error && error.code !== 'PGRST116')
1365
+ throw error;
1366
+ return data;
1367
+ }
1368
+ /**
1369
+ * Search entities
1370
+ */
1371
+ async searchEntities(opts = {}) {
1372
+ let query = this.supabase
1373
+ .from('entities')
1374
+ .select()
1375
+ .eq('agent_id', this.agentId)
1376
+ .order('mention_count', { ascending: false })
1377
+ .order('last_seen_at', { ascending: false })
1378
+ .limit(opts.limit || 20);
1379
+ if (opts.entityType) {
1380
+ query = query.eq('entity_type', opts.entityType);
1381
+ }
1382
+ if (opts.query) {
1383
+ query = query.or(`name.ilike.%${opts.query}%,description.ilike.%${opts.query}%`);
1384
+ }
1385
+ const { data, error } = await query;
1386
+ if (error)
1387
+ throw error;
1388
+ return data || [];
1389
+ }
1390
+ /**
1391
+ * Merge two entities (deduplication)
1392
+ */
1393
+ async mergeEntities(primaryId, duplicateId) {
1394
+ // Get both entities
1395
+ const [primary, duplicate] = await Promise.all([
1396
+ this.supabase.from('entities').select().eq('id', primaryId).single(),
1397
+ this.supabase.from('entities').select().eq('id', duplicateId).single()
1398
+ ]);
1399
+ if (primary.error)
1400
+ throw primary.error;
1401
+ if (duplicate.error)
1402
+ throw duplicate.error;
1403
+ // Merge aliases
1404
+ const mergedAliases = [
1405
+ ...(primary.data.aliases || []),
1406
+ duplicate.data.name,
1407
+ ...(duplicate.data.aliases || [])
1408
+ ].filter((v, i, a) => a.indexOf(v) === i); // Deduplicate
1409
+ // Merge properties
1410
+ const mergedProperties = {
1411
+ ...duplicate.data.properties,
1412
+ ...primary.data.properties
1413
+ };
1414
+ // Update primary entity
1415
+ const { data, error } = await this.supabase
1416
+ .from('entities')
1417
+ .update({
1418
+ aliases: mergedAliases,
1419
+ properties: mergedProperties,
1420
+ mention_count: primary.data.mention_count + duplicate.data.mention_count,
1421
+ first_seen_at: new Date(Math.min(new Date(primary.data.first_seen_at).getTime(), new Date(duplicate.data.first_seen_at).getTime())).toISOString(),
1422
+ last_seen_at: new Date(Math.max(new Date(primary.data.last_seen_at).getTime(), new Date(duplicate.data.last_seen_at).getTime())).toISOString()
1423
+ })
1424
+ .eq('id', primaryId)
1425
+ .select()
1426
+ .single();
1427
+ if (error)
1428
+ throw error;
1429
+ // Delete duplicate
1430
+ await this.supabase.from('entities').delete().eq('id', duplicateId);
1431
+ return data;
1432
+ }
1433
+ /**
1434
+ * Create or update a relationship between entities
1435
+ */
1436
+ async createEntityRelationship(rel) {
1437
+ // Check if relationship already exists
1438
+ const { data: existing, error: checkError } = await this.supabase
1439
+ .from('entity_relationships')
1440
+ .select()
1441
+ .eq('agent_id', this.agentId)
1442
+ .eq('source_entity_id', rel.sourceEntityId)
1443
+ .eq('target_entity_id', rel.targetEntityId)
1444
+ .eq('relationship_type', rel.relationshipType)
1445
+ .single();
1446
+ if (existing && !checkError) {
1447
+ // Update existing relationship
1448
+ const { data, error } = await this.supabase.rpc('increment_relationship_mentions', {
1449
+ rel_id: existing.id
1450
+ });
1451
+ if (error) {
1452
+ // Fallback if RPC doesn't exist
1453
+ const updated = await this.supabase
1454
+ .from('entity_relationships')
1455
+ .update({
1456
+ mention_count: existing.mention_count + 1,
1457
+ last_seen_at: new Date().toISOString(),
1458
+ confidence: Math.min(1.0, existing.confidence + 0.1), // Increase confidence with mentions
1459
+ properties: { ...existing.properties, ...rel.properties }
1460
+ })
1461
+ .eq('id', existing.id)
1462
+ .select()
1463
+ .single();
1464
+ if (updated.error)
1465
+ throw updated.error;
1466
+ return updated.data;
1467
+ }
1468
+ return data;
1469
+ }
1470
+ // Create new relationship
1471
+ const { data, error } = await this.supabase
1472
+ .from('entity_relationships')
1473
+ .insert({
1474
+ agent_id: this.agentId,
1475
+ source_entity_id: rel.sourceEntityId,
1476
+ target_entity_id: rel.targetEntityId,
1477
+ relationship_type: rel.relationshipType,
1478
+ properties: rel.properties || {},
1479
+ confidence: rel.confidence ?? 0.5,
1480
+ source_session_id: rel.sessionId,
1481
+ metadata: rel.metadata || {}
1482
+ })
1483
+ .select()
1484
+ .single();
1485
+ if (error)
1486
+ throw error;
1487
+ return data;
1488
+ }
1489
+ /**
1490
+ * Get relationships for an entity
1491
+ */
1492
+ async getEntityRelationships(entityId, opts = {}) {
1493
+ const direction = opts.direction || 'both';
1494
+ const minConfidence = opts.minConfidence ?? 0.3;
1495
+ const limit = opts.limit || 50;
1496
+ const results = [];
1497
+ // Get outgoing relationships
1498
+ if (direction === 'outgoing' || direction === 'both') {
1499
+ let query = this.supabase
1500
+ .from('entity_relationships')
1501
+ .select('*, target:entities!target_entity_id(*)')
1502
+ .eq('source_entity_id', entityId)
1503
+ .gte('confidence', minConfidence)
1504
+ .order('mention_count', { ascending: false })
1505
+ .limit(limit);
1506
+ if (opts.relationshipType) {
1507
+ query = query.eq('relationship_type', opts.relationshipType);
1508
+ }
1509
+ const { data, error } = await query;
1510
+ if (error)
1511
+ throw error;
1512
+ if (data) {
1513
+ for (const row of data) {
1514
+ const { target, ...relationship } = row;
1515
+ results.push({
1516
+ relationship: relationship,
1517
+ relatedEntity: target,
1518
+ direction: 'outgoing'
1519
+ });
1520
+ }
1521
+ }
1522
+ }
1523
+ // Get incoming relationships
1524
+ if (direction === 'incoming' || direction === 'both') {
1525
+ let query = this.supabase
1526
+ .from('entity_relationships')
1527
+ .select('*, source:entities!source_entity_id(*)')
1528
+ .eq('target_entity_id', entityId)
1529
+ .gte('confidence', minConfidence)
1530
+ .order('mention_count', { ascending: false })
1531
+ .limit(limit);
1532
+ if (opts.relationshipType) {
1533
+ query = query.eq('relationship_type', opts.relationshipType);
1534
+ }
1535
+ const { data, error } = await query;
1536
+ if (error)
1537
+ throw error;
1538
+ if (data) {
1539
+ for (const row of data) {
1540
+ const { source, ...relationship } = row;
1541
+ results.push({
1542
+ relationship: relationship,
1543
+ relatedEntity: source,
1544
+ direction: 'incoming'
1545
+ });
1546
+ }
1547
+ }
1548
+ }
1549
+ return results;
1550
+ }
1551
+ /**
1552
+ * Find related entities through graph traversal
1553
+ */
1554
+ async findRelatedEntities(entityId, opts = {}) {
1555
+ const { data, error } = await this.supabase.rpc('find_related_entities', {
1556
+ entity_id: entityId,
1557
+ max_depth: opts.maxDepth || 2,
1558
+ min_confidence: opts.minConfidence ?? 0.5
1559
+ });
1560
+ if (error)
1561
+ throw error;
1562
+ return data || [];
1563
+ }
1564
+ /**
1565
+ * Get entity network statistics
1566
+ */
1567
+ async getEntityNetworkStats() {
1568
+ const { data, error } = await this.supabase.rpc('get_entity_network_stats', {
1569
+ agent: this.agentId
1570
+ });
1571
+ if (error)
1572
+ throw error;
1573
+ const stats = data?.[0];
1574
+ if (!stats) {
1575
+ return {
1576
+ totalEntities: 0,
1577
+ totalRelationships: 0,
1578
+ avgConnectionsPerEntity: 0
1579
+ };
1580
+ }
1581
+ return {
1582
+ totalEntities: Number(stats.total_entities),
1583
+ totalRelationships: Number(stats.total_relationships),
1584
+ avgConnectionsPerEntity: Number(stats.avg_connections_per_entity) || 0,
1585
+ mostConnectedEntity: stats.most_connected_entity_id ? {
1586
+ id: stats.most_connected_entity_id,
1587
+ name: stats.most_connected_entity_name,
1588
+ connectionCount: Number(stats.connection_count)
1589
+ } : undefined
1590
+ };
1591
+ }
1592
+ /**
1593
+ * Extract entities and relationships from text using AI
1594
+ */
1595
+ async extractEntitiesWithRelationships(text, opts = {}) {
1596
+ if (!this.openai) {
1597
+ throw new Error('OpenAI client required for entity extraction');
1598
+ }
1599
+ const response = await this.openai.chat.completions.create({
1600
+ model: 'gpt-4o-mini',
1601
+ messages: [
1602
+ {
1603
+ role: 'system',
1604
+ content: `Extract named entities and their relationships from the text.
1605
+
1606
+ Return JSON with this structure:
1607
+ {
1608
+ "entities": [
1609
+ {"type": "person|place|organization|product|concept", "name": "...", "description": "..."}
1610
+ ],
1611
+ "relationships": [
1612
+ {"source": "entity name", "target": "entity name", "type": "works_at|knows|created|located_in|etc", "confidence": 0.0-1.0}
1613
+ ]
1614
+ }
1615
+
1616
+ Focus on important entities and clear relationships. Use standard relationship types when possible.`
1617
+ },
1618
+ {
1619
+ role: 'user',
1620
+ content: text
1621
+ }
1622
+ ],
1623
+ response_format: { type: 'json_object' }
1624
+ });
1625
+ const result = JSON.parse(response.choices[0]?.message?.content || '{"entities":[],"relationships":[]}');
1626
+ const extractedEntities = result.entities || [];
1627
+ const extractedRelationships = result.relationships || [];
1628
+ // First, create/update all entities
1629
+ const entityMap = new Map();
1630
+ for (const e of extractedEntities) {
1631
+ const existing = await this.findEntity(e.name);
1632
+ let entity;
1633
+ if (existing) {
1634
+ entity = await this.updateEntity(existing.id, {
1635
+ description: e.description,
1636
+ lastSeenAt: new Date().toISOString()
1637
+ });
1638
+ }
1639
+ else {
1640
+ entity = await this.createEntity({
1641
+ entityType: e.type,
1642
+ name: e.name,
1643
+ description: e.description
1644
+ });
1645
+ }
1646
+ entityMap.set(e.name.toLowerCase(), entity);
1647
+ }
1648
+ // Then, create relationships
1649
+ const relationships = [];
1650
+ for (const r of extractedRelationships) {
1651
+ const sourceEntity = entityMap.get(r.source.toLowerCase());
1652
+ const targetEntity = entityMap.get(r.target.toLowerCase());
1653
+ if (sourceEntity && targetEntity) {
1654
+ const relationship = await this.createEntityRelationship({
1655
+ sourceEntityId: sourceEntity.id,
1656
+ targetEntityId: targetEntity.id,
1657
+ relationshipType: r.type,
1658
+ confidence: r.confidence || 0.7,
1659
+ sessionId: opts.sessionId
1660
+ });
1661
+ relationships.push(relationship);
1662
+ }
1663
+ }
1664
+ return {
1665
+ entities: Array.from(entityMap.values()),
1666
+ relationships
1667
+ };
1668
+ }
1669
+ /**
1670
+ * Delete a relationship
1671
+ */
1672
+ async deleteEntityRelationship(relationshipId) {
1673
+ const { error } = await this.supabase
1674
+ .from('entity_relationships')
1675
+ .delete()
1676
+ .eq('id', relationshipId);
1677
+ if (error)
1678
+ throw error;
1679
+ }
1680
+ /**
1681
+ * Search relationships
1682
+ */
1683
+ async searchRelationships(opts = {}) {
1684
+ let query = this.supabase
1685
+ .from('entity_relationships')
1686
+ .select()
1687
+ .eq('agent_id', this.agentId)
1688
+ .order('mention_count', { ascending: false })
1689
+ .order('confidence', { ascending: false })
1690
+ .limit(opts.limit || 50);
1691
+ if (opts.relationshipType) {
1692
+ query = query.eq('relationship_type', opts.relationshipType);
1693
+ }
1694
+ if (opts.minConfidence) {
1695
+ query = query.gte('confidence', opts.minConfidence);
1696
+ }
1697
+ const { data, error } = await query;
1698
+ if (error)
1699
+ throw error;
1700
+ return data || [];
1701
+ }
1702
+ // ============ CONTEXT ============
1703
+ /**
1704
+ * Get relevant context for a query
1705
+ * Combines memories, recent messages, and entities
1706
+ */
1707
+ async getContext(query, opts = {}) {
1708
+ // Get relevant memories
1709
+ const memories = await this.recall(query, {
1710
+ userId: opts.userId,
1711
+ limit: opts.maxMemories || 5
1712
+ });
1713
+ // Get recent messages from current session
1714
+ let recentMessages = [];
1715
+ if (opts.sessionId) {
1716
+ recentMessages = await this.getMessages(opts.sessionId, {
1717
+ limit: opts.maxMessages || 20
1718
+ });
1719
+ }
1720
+ // Build context summary
1721
+ const memoryText = memories
1722
+ .map(m => `- ${m.content}`)
1723
+ .join('\n');
1724
+ const summary = memories.length > 0
1725
+ ? `Relevant memories:\n${memoryText}`
1726
+ : 'No relevant memories found.';
1727
+ return { memories, recentMessages, summary };
1728
+ }
1729
+ // ============ CONTEXT WINDOW MANAGEMENT ============
1730
+ /**
1731
+ * Build an optimized context window with token budgeting
1732
+ * Implements smart context selection and lost-in-middle mitigation
1733
+ */
1734
+ async buildOptimizedContext(opts) {
1735
+ const { query, sessionId, userId, modelContextSize, model, useLostInMiddleFix = true, recencyWeight, importanceWeight, customBudget } = opts;
1736
+ // Fetch relevant data
1737
+ const [messages, memories, learnings, entities] = await Promise.all([
1738
+ sessionId ? this.getMessages(sessionId) : Promise.resolve([]),
1739
+ this.recall(query, { userId, limit: 50 }),
1740
+ this.searchLearnings(query, { limit: 20 }),
1741
+ this.searchEntities({ query, limit: 15 })
1742
+ ]);
1743
+ // Determine budget
1744
+ let budget;
1745
+ if (customBudget) {
1746
+ budget = customBudget;
1747
+ }
1748
+ else if (model) {
1749
+ budget = (0, context_manager_1.getBudgetForModel)(model);
1750
+ }
1751
+ else if (modelContextSize) {
1752
+ budget = (0, context_manager_1.createContextBudget)({ modelContextSize });
1753
+ }
1754
+ else {
1755
+ // Adaptive budget based on available content
1756
+ budget = (0, context_manager_1.createAdaptiveBudget)({
1757
+ messageCount: messages.length,
1758
+ memoryCount: memories.length,
1759
+ learningCount: learnings.length,
1760
+ entityCount: entities.length
1761
+ });
1762
+ }
1763
+ // Build context window
1764
+ const window = (0, context_manager_1.buildContextWindow)({
1765
+ messages,
1766
+ memories,
1767
+ learnings,
1768
+ entities,
1769
+ budget,
1770
+ useLostInMiddleFix,
1771
+ recencyWeight,
1772
+ importanceWeight
1773
+ });
1774
+ // Format for prompt
1775
+ const formatted = (0, context_manager_1.formatContextWindow)(window, {
1776
+ groupByType: true,
1777
+ includeMetadata: false
1778
+ });
1779
+ // Get stats
1780
+ const stats = (0, context_manager_1.getContextStats)(window);
1781
+ return { window, formatted, stats };
1782
+ }
1783
+ /**
1784
+ * Get smart context with automatic budget management
1785
+ * Simplified version of buildOptimizedContext for common use cases
1786
+ */
1787
+ async getSmartContext(query, opts = {}) {
1788
+ const result = await this.buildOptimizedContext({
1789
+ query,
1790
+ sessionId: opts.sessionId,
1791
+ userId: opts.userId,
1792
+ model: opts.model || 'default'
1793
+ });
1794
+ return result.formatted;
1795
+ }
1796
+ /**
1797
+ * Estimate token usage for a session
1798
+ */
1799
+ async estimateSessionTokenUsage(sessionId) {
1800
+ const stats = await this.countSessionTokens(sessionId);
1801
+ // Get memories from this session
1802
+ const { data, error } = await this.supabase
1803
+ .from('memories')
1804
+ .select()
1805
+ .eq('source_session_id', sessionId);
1806
+ const memoryTokens = (data || []).reduce((sum, mem) => {
1807
+ return sum + (mem.content.length / 4); // Rough estimate
1808
+ }, 0);
1809
+ const total = stats.totalTokens + memoryTokens;
1810
+ // Determine context size needed
1811
+ let contextSize = '4k';
1812
+ if (total > 4000)
1813
+ contextSize = '8k';
1814
+ if (total > 8000)
1815
+ contextSize = '16k';
1816
+ if (total > 16000)
1817
+ contextSize = '32k';
1818
+ if (total > 32000)
1819
+ contextSize = '64k';
1820
+ if (total > 64000)
1821
+ contextSize = '128k';
1822
+ if (total > 128000)
1823
+ contextSize = '200k';
1824
+ return {
1825
+ messages: stats.totalTokens,
1826
+ memories: Math.round(memoryTokens),
1827
+ total: Math.round(total),
1828
+ contextSize
1829
+ };
1830
+ }
1831
+ /**
1832
+ * Test context window with different budgets
1833
+ * Useful for optimization and debugging
1834
+ */
1835
+ async testContextBudgets(query, opts = {}) {
1836
+ const models = opts.models || ['gpt-3.5-turbo', 'gpt-4-turbo', 'claude-3.5-sonnet'];
1837
+ const results = [];
1838
+ for (const model of models) {
1839
+ const { window, stats } = await this.buildOptimizedContext({
1840
+ query,
1841
+ sessionId: opts.sessionId,
1842
+ userId: opts.userId,
1843
+ model
1844
+ });
1845
+ results.push({
1846
+ model,
1847
+ budget: window.budget,
1848
+ stats
1849
+ });
1850
+ }
1851
+ return results;
1852
+ }
1853
+ // ============ MEMORY LIFECYCLE MANAGEMENT ============
1854
+ /**
1855
+ * Apply importance decay to memories
1856
+ * Reduces importance over time to prevent old memories from dominating
1857
+ */
1858
+ async decayMemoryImportance(opts = {}) {
1859
+ const decayRate = opts.decayRate ?? 0.1;
1860
+ const minImportance = opts.minImportance ?? 0.1;
1861
+ const olderThanDays = opts.olderThanDays ?? 7;
1862
+ // Get memories to decay
1863
+ const cutoffDate = new Date();
1864
+ cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
1865
+ let query = this.supabase
1866
+ .from('memories')
1867
+ .select()
1868
+ .eq('agent_id', this.agentId)
1869
+ .lt('updated_at', cutoffDate.toISOString())
1870
+ .gt('importance', minImportance);
1871
+ if (opts.userId) {
1872
+ query = query.eq('user_id', opts.userId);
1873
+ }
1874
+ const { data: memories, error } = await query;
1875
+ if (error)
1876
+ throw error;
1877
+ if (!memories || memories.length === 0) {
1878
+ return { updated: 0, avgDecay: 0 };
1879
+ }
1880
+ // Apply decay
1881
+ let totalDecay = 0;
1882
+ const updates = memories.map(mem => {
1883
+ const newImportance = Math.max(minImportance, mem.importance * (1 - decayRate));
1884
+ totalDecay += (mem.importance - newImportance);
1885
+ return {
1886
+ id: mem.id,
1887
+ importance: newImportance,
1888
+ metadata: {
1889
+ ...mem.metadata,
1890
+ last_decay: new Date().toISOString(),
1891
+ decay_count: (mem.metadata?.decay_count || 0) + 1
1892
+ }
1893
+ };
1894
+ });
1895
+ // Batch update
1896
+ for (const update of updates) {
1897
+ await this.supabase
1898
+ .from('memories')
1899
+ .update({
1900
+ importance: update.importance,
1901
+ metadata: update.metadata,
1902
+ updated_at: new Date().toISOString()
1903
+ })
1904
+ .eq('id', update.id);
1905
+ }
1906
+ return {
1907
+ updated: memories.length,
1908
+ avgDecay: totalDecay / memories.length
1909
+ };
1910
+ }
1911
+ /**
1912
+ * Consolidate similar memories
1913
+ * Merge duplicate/similar memories to reduce clutter
1914
+ */
1915
+ async consolidateMemories(opts = {}) {
1916
+ const threshold = opts.similarityThreshold ?? 0.9;
1917
+ const limit = opts.limit ?? 100;
1918
+ // Get memories with embeddings
1919
+ let query = this.supabase
1920
+ .from('memories')
1921
+ .select()
1922
+ .eq('agent_id', this.agentId)
1923
+ .not('embedding', 'is', null)
1924
+ .order('created_at', { ascending: false })
1925
+ .limit(limit);
1926
+ if (opts.userId) {
1927
+ query = query.eq('user_id', opts.userId);
1928
+ }
1929
+ if (opts.category) {
1930
+ query = query.eq('category', opts.category);
1931
+ }
1932
+ const { data: memories, error } = await query;
1933
+ if (error)
1934
+ throw error;
1935
+ if (!memories || memories.length < 2) {
1936
+ return { merged: 0, kept: memories?.length || 0 };
1937
+ }
1938
+ // Find similar pairs
1939
+ const toMerge = [];
1940
+ for (let i = 0; i < memories.length; i++) {
1941
+ for (let j = i + 1; j < memories.length; j++) {
1942
+ const similarity = this.cosineSimilarity(memories[i].embedding, memories[j].embedding);
1943
+ if (similarity >= threshold) {
1944
+ // Keep the more important/recent one
1945
+ const [keep, merge] = memories[i].importance >= memories[j].importance
1946
+ ? [memories[i], memories[j]]
1947
+ : [memories[j], memories[i]];
1948
+ toMerge.push({ keep, merge, similarity });
1949
+ }
1950
+ }
1951
+ }
1952
+ // Merge memories
1953
+ let mergedCount = 0;
1954
+ for (const { keep, merge, similarity } of toMerge) {
1955
+ // Combine content
1956
+ const combinedContent = `${keep.content}\n\n[Merged similar memory (similarity: ${similarity.toFixed(2)})]:\n${merge.content}`;
1957
+ // Update importance (weighted average)
1958
+ const combinedImportance = (keep.importance + merge.importance) / 2;
1959
+ // Update metadata
1960
+ const combinedMetadata = {
1961
+ ...keep.metadata,
1962
+ merged_from: [
1963
+ ...(keep.metadata?.merged_from || []),
1964
+ merge.id
1965
+ ],
1966
+ merge_count: (keep.metadata?.merge_count || 0) + 1,
1967
+ last_merged: new Date().toISOString()
1968
+ };
1969
+ // Update keep
1970
+ await this.supabase
1971
+ .from('memories')
1972
+ .update({
1973
+ content: combinedContent,
1974
+ importance: combinedImportance,
1975
+ metadata: combinedMetadata,
1976
+ updated_at: new Date().toISOString()
1977
+ })
1978
+ .eq('id', keep.id);
1979
+ // Delete merge
1980
+ await this.supabase
1981
+ .from('memories')
1982
+ .delete()
1983
+ .eq('id', merge.id);
1984
+ mergedCount++;
1985
+ }
1986
+ return {
1987
+ merged: mergedCount,
1988
+ kept: memories.length - mergedCount
1989
+ };
1990
+ }
1991
+ /**
1992
+ * Version a memory (create historical snapshot)
1993
+ */
1994
+ async versionMemory(memoryId) {
1995
+ // Get current memory
1996
+ const { data: memory, error } = await this.supabase
1997
+ .from('memories')
1998
+ .select()
1999
+ .eq('id', memoryId)
2000
+ .single();
2001
+ if (error)
2002
+ throw error;
2003
+ // Store version in metadata
2004
+ const versions = memory.metadata?.versions || [];
2005
+ versions.push({
2006
+ timestamp: new Date().toISOString(),
2007
+ content: memory.content,
2008
+ importance: memory.importance
2009
+ });
2010
+ const versionId = `v${versions.length}`;
2011
+ // Update metadata
2012
+ await this.supabase
2013
+ .from('memories')
2014
+ .update({
2015
+ metadata: {
2016
+ ...memory.metadata,
2017
+ versions,
2018
+ current_version: versionId
2019
+ }
2020
+ })
2021
+ .eq('id', memoryId);
2022
+ return { memory, versionId };
2023
+ }
2024
+ /**
2025
+ * Get memory version history
2026
+ */
2027
+ async getMemoryVersions(memoryId) {
2028
+ const { data: memory, error } = await this.supabase
2029
+ .from('memories')
2030
+ .select()
2031
+ .eq('id', memoryId)
2032
+ .single();
2033
+ if (error)
2034
+ throw error;
2035
+ const versions = memory.metadata?.versions || [];
2036
+ return versions.map((v, i) => ({
2037
+ version: `v${i + 1}`,
2038
+ ...v
2039
+ }));
2040
+ }
2041
+ /**
2042
+ * Tag memories for organization
2043
+ */
2044
+ async tagMemory(memoryId, tags) {
2045
+ const { data: memory, error: fetchError } = await this.supabase
2046
+ .from('memories')
2047
+ .select()
2048
+ .eq('id', memoryId)
2049
+ .single();
2050
+ if (fetchError)
2051
+ throw fetchError;
2052
+ const existingTags = memory.metadata?.tags || [];
2053
+ const newTags = Array.from(new Set([...existingTags, ...tags]));
2054
+ const { data, error } = await this.supabase
2055
+ .from('memories')
2056
+ .update({
2057
+ metadata: {
2058
+ ...memory.metadata,
2059
+ tags: newTags
2060
+ },
2061
+ updated_at: new Date().toISOString()
2062
+ })
2063
+ .eq('id', memoryId)
2064
+ .select()
2065
+ .single();
2066
+ if (error)
2067
+ throw error;
2068
+ return data;
2069
+ }
2070
+ /**
2071
+ * Remove tags from memory
2072
+ */
2073
+ async untagMemory(memoryId, tags) {
2074
+ const { data: memory, error: fetchError } = await this.supabase
2075
+ .from('memories')
2076
+ .select()
2077
+ .eq('id', memoryId)
2078
+ .single();
2079
+ if (fetchError)
2080
+ throw fetchError;
2081
+ const existingTags = memory.metadata?.tags || [];
2082
+ const newTags = existingTags.filter(t => !tags.includes(t));
2083
+ const { data, error } = await this.supabase
2084
+ .from('memories')
2085
+ .update({
2086
+ metadata: {
2087
+ ...memory.metadata,
2088
+ tags: newTags
2089
+ },
2090
+ updated_at: new Date().toISOString()
2091
+ })
2092
+ .eq('id', memoryId)
2093
+ .select()
2094
+ .single();
2095
+ if (error)
2096
+ throw error;
2097
+ return data;
2098
+ }
2099
+ /**
2100
+ * Search memories by tags
2101
+ */
2102
+ async searchMemoriesByTags(tags, opts = {}) {
2103
+ let query = this.supabase
2104
+ .from('memories')
2105
+ .select()
2106
+ .eq('agent_id', this.agentId);
2107
+ if (opts.userId) {
2108
+ query = query.eq('user_id', opts.userId);
2109
+ }
2110
+ const { data, error } = await query;
2111
+ if (error)
2112
+ throw error;
2113
+ // Filter by tags
2114
+ const filtered = (data || []).filter(mem => {
2115
+ const memTags = mem.metadata?.tags || [];
2116
+ if (opts.matchAll) {
2117
+ return tags.every(tag => memTags.includes(tag));
2118
+ }
2119
+ else {
2120
+ return tags.some(tag => memTags.includes(tag));
2121
+ }
2122
+ });
2123
+ return filtered.slice(0, opts.limit || 50);
2124
+ }
2125
+ /**
2126
+ * Auto-cleanup old sessions
2127
+ * Archive or delete sessions older than a threshold
2128
+ */
2129
+ async cleanupOldSessions(opts = {}) {
2130
+ const olderThanDays = opts.olderThanDays ?? 90;
2131
+ const action = opts.action ?? 'archive';
2132
+ const keepSummaries = opts.keepSummaries ?? true;
2133
+ const cutoffDate = new Date();
2134
+ cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
2135
+ let query = this.supabase
2136
+ .from('sessions')
2137
+ .select()
2138
+ .eq('agent_id', this.agentId)
2139
+ .lt('started_at', cutoffDate.toISOString());
2140
+ if (opts.userId) {
2141
+ query = query.eq('user_id', opts.userId);
2142
+ }
2143
+ if (keepSummaries) {
2144
+ query = query.is('summary', null);
2145
+ }
2146
+ const { data: sessions, error } = await query;
2147
+ if (error)
2148
+ throw error;
2149
+ if (!sessions || sessions.length === 0) {
2150
+ return action === 'delete' ? { deleted: 0 } : { archived: 0 };
2151
+ }
2152
+ if (action === 'delete') {
2153
+ // Delete sessions and their messages
2154
+ for (const session of sessions) {
2155
+ // Delete messages first
2156
+ await this.supabase
2157
+ .from('messages')
2158
+ .delete()
2159
+ .eq('session_id', session.id);
2160
+ // Delete session
2161
+ await this.supabase
2162
+ .from('sessions')
2163
+ .delete()
2164
+ .eq('id', session.id);
2165
+ }
2166
+ return { deleted: sessions.length };
2167
+ }
2168
+ else {
2169
+ // Archive by marking in metadata
2170
+ for (const session of sessions) {
2171
+ await this.supabase
2172
+ .from('sessions')
2173
+ .update({
2174
+ metadata: {
2175
+ ...session.metadata,
2176
+ archived: true,
2177
+ archived_at: new Date().toISOString()
2178
+ }
2179
+ })
2180
+ .eq('id', session.id);
2181
+ }
2182
+ return { archived: sessions.length };
2183
+ }
2184
+ }
2185
+ /**
2186
+ * Get cleanup statistics
2187
+ */
2188
+ async getCleanupStats() {
2189
+ const { data: sessions, error: sessError } = await this.supabase
2190
+ .from('sessions')
2191
+ .select()
2192
+ .eq('agent_id', this.agentId);
2193
+ if (sessError)
2194
+ throw sessError;
2195
+ const totalSessions = sessions?.length || 0;
2196
+ const archivedSessions = sessions?.filter(s => s.metadata?.archived).length || 0;
2197
+ const cutoffDate = new Date();
2198
+ cutoffDate.setDate(cutoffDate.getDate() - 90);
2199
+ const oldSessions = sessions?.filter(s => new Date(s.started_at) < cutoffDate).length || 0;
2200
+ const { data: messages, error: msgError } = await this.supabase
2201
+ .from('messages')
2202
+ .select()
2203
+ .in('session_id', sessions?.map(s => s.id) || []);
2204
+ if (msgError)
2205
+ throw msgError;
2206
+ const totalMessages = messages?.length || 0;
2207
+ // Find orphaned messages (messages without sessions)
2208
+ const { data: allMessages, error: allMsgError } = await this.supabase
2209
+ .from('messages')
2210
+ .select('id, session_id');
2211
+ if (allMsgError)
2212
+ throw allMsgError;
2213
+ const sessionIds = new Set(sessions?.map(s => s.id) || []);
2214
+ const orphanedMessages = allMessages?.filter(m => !sessionIds.has(m.session_id)).length || 0;
2215
+ return {
2216
+ totalSessions,
2217
+ archivedSessions,
2218
+ oldSessions,
2219
+ totalMessages,
2220
+ orphanedMessages
2221
+ };
2222
+ }
2223
+ }
2224
+ exports.OpenClawMemory = OpenClawMemory;
2225
+ // Re-export context manager utilities
2226
+ var context_manager_2 = require("./context-manager");
2227
+ Object.defineProperty(exports, "createContextBudget", { enumerable: true, get: function () { return context_manager_2.createContextBudget; } });
2228
+ Object.defineProperty(exports, "createAdaptiveBudget", { enumerable: true, get: function () { return context_manager_2.createAdaptiveBudget; } });
2229
+ Object.defineProperty(exports, "buildContextWindow", { enumerable: true, get: function () { return context_manager_2.buildContextWindow; } });
2230
+ Object.defineProperty(exports, "formatContextWindow", { enumerable: true, get: function () { return context_manager_2.formatContextWindow; } });
2231
+ Object.defineProperty(exports, "getContextStats", { enumerable: true, get: function () { return context_manager_2.getContextStats; } });
2232
+ Object.defineProperty(exports, "getBudgetForModel", { enumerable: true, get: function () { return context_manager_2.getBudgetForModel; } });
2233
+ Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return context_manager_2.estimateTokens; } });
2234
+ Object.defineProperty(exports, "estimateTokensAccurate", { enumerable: true, get: function () { return context_manager_2.estimateTokensAccurate; } });
2235
+ // Export Clawdbot integration
2236
+ var clawdbot_integration_1 = require("./clawdbot-integration");
2237
+ Object.defineProperty(exports, "ClawdbotMemoryIntegration", { enumerable: true, get: function () { return clawdbot_integration_1.ClawdbotMemoryIntegration; } });
2238
+ Object.defineProperty(exports, "createClawdbotIntegration", { enumerable: true, get: function () { return clawdbot_integration_1.createClawdbotIntegration; } });
2239
+ Object.defineProperty(exports, "createLoggingMiddleware", { enumerable: true, get: function () { return clawdbot_integration_1.createLoggingMiddleware; } });
2240
+ // Export error handling utilities
2241
+ var error_handling_1 = require("./error-handling");
2242
+ Object.defineProperty(exports, "OpenClawError", { enumerable: true, get: function () { return error_handling_1.OpenClawError; } });
2243
+ Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function () { return error_handling_1.DatabaseError; } });
2244
+ Object.defineProperty(exports, "EmbeddingError", { enumerable: true, get: function () { return error_handling_1.EmbeddingError; } });
2245
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return error_handling_1.ValidationError; } });
2246
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return error_handling_1.RateLimitError; } });
2247
+ Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return error_handling_1.CircuitBreaker; } });
2248
+ Object.defineProperty(exports, "retry", { enumerable: true, get: function () { return error_handling_1.retry; } });
2249
+ Object.defineProperty(exports, "wrapDatabaseOperation", { enumerable: true, get: function () { return error_handling_1.wrapDatabaseOperation; } });
2250
+ Object.defineProperty(exports, "wrapEmbeddingOperation", { enumerable: true, get: function () { return error_handling_1.wrapEmbeddingOperation; } });
2251
+ Object.defineProperty(exports, "validateInput", { enumerable: true, get: function () { return error_handling_1.validateInput; } });
2252
+ Object.defineProperty(exports, "safeJsonParse", { enumerable: true, get: function () { return error_handling_1.safeJsonParse; } });
2253
+ Object.defineProperty(exports, "withTimeout", { enumerable: true, get: function () { return error_handling_1.withTimeout; } });
2254
+ Object.defineProperty(exports, "gracefulFallback", { enumerable: true, get: function () { return error_handling_1.gracefulFallback; } });
2255
+ Object.defineProperty(exports, "batchWithErrorHandling", { enumerable: true, get: function () { return error_handling_1.batchWithErrorHandling; } });
2256
+ exports.default = OpenClawMemory;