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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Phase 9: Migration & Import Parsers
3
+ *
4
+ * Parsers for converting Clawdbot memory files to OpenClaw Memory database format:
5
+ * - MEMORY.md → memories table
6
+ * - memory/*.md → sessions + messages
7
+ * - TODO.md → tasks table
8
+ * - LEARNINGS.md → learnings table
9
+ */
10
+ export interface ParsedMemory {
11
+ content: string;
12
+ category: string;
13
+ importance: number;
14
+ metadata?: Record<string, any>;
15
+ created_at?: string;
16
+ }
17
+ export interface ParsedSession {
18
+ user_id: string;
19
+ channel?: string;
20
+ started_at: string;
21
+ ended_at?: string;
22
+ summary?: string;
23
+ messages: ParsedMessage[];
24
+ }
25
+ export interface ParsedMessage {
26
+ role: 'user' | 'assistant' | 'system';
27
+ content: string;
28
+ timestamp: string;
29
+ metadata?: Record<string, any>;
30
+ }
31
+ export interface ParsedTask {
32
+ title: string;
33
+ description?: string;
34
+ status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
35
+ priority?: number;
36
+ due_date?: string;
37
+ metadata?: Record<string, any>;
38
+ }
39
+ export interface ParsedLearning {
40
+ category: string;
41
+ trigger: string;
42
+ lesson: string;
43
+ importance: number;
44
+ applied_count?: number;
45
+ created_at?: string;
46
+ }
47
+ /**
48
+ * Parse MEMORY.md into structured memories
49
+ *
50
+ * Expected format:
51
+ * # MEMORY.md
52
+ *
53
+ * ## Category Name
54
+ *
55
+ * - Memory item [importance: 0.9]
56
+ * - Another memory [2024-01-28]
57
+ *
58
+ * Regular paragraphs are also captured as memories.
59
+ */
60
+ export declare function parseMemoryMd(filePath: string): ParsedMemory[];
61
+ /**
62
+ * Parse daily log files (memory/YYYY-MM-DD.md) into sessions and messages
63
+ *
64
+ * Expected format:
65
+ * # 2024-01-28
66
+ *
67
+ * ## Session: Trading Research
68
+ * Started: 09:00
69
+ *
70
+ * **User**: What's the stock price of TSLA?
71
+ *
72
+ * **Assistant**: Tesla is currently trading at $245.
73
+ *
74
+ * **User**: Should I buy?
75
+ *
76
+ * Summary: Discussed TSLA stock price...
77
+ */
78
+ export declare function parseDailyLog(filePath: string, userId?: string): ParsedSession[];
79
+ /**
80
+ * Parse all daily logs in a directory (memory/*.md)
81
+ */
82
+ export declare function parseAllDailyLogs(memoryDir: string, userId?: string): ParsedSession[];
83
+ /**
84
+ * Parse TODO.md into structured tasks
85
+ *
86
+ * Expected format:
87
+ * # TODO
88
+ *
89
+ * ## Priority: High
90
+ * - [ ] Incomplete task
91
+ * - [x] Completed task
92
+ * - [~] Cancelled task
93
+ *
94
+ * ## Category Name
95
+ * - [ ] Task with [due: 2024-02-01]
96
+ */
97
+ export declare function parseTodoMd(filePath: string): ParsedTask[];
98
+ /**
99
+ * Parse LEARNINGS.md into structured learnings
100
+ *
101
+ * Expected format:
102
+ * # LEARNINGS
103
+ *
104
+ * ## Category: Corrections
105
+ *
106
+ * **Trigger**: User said "actually, I prefer Rust"
107
+ * **Lesson**: User prefers Rust over TypeScript
108
+ * **Importance**: 0.8
109
+ *
110
+ * ---
111
+ *
112
+ * ## Category: Errors
113
+ * ...
114
+ */
115
+ export declare function parseLearningsMd(filePath: string): ParsedLearning[];
@@ -0,0 +1,406 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 9: Migration & Import Parsers
4
+ *
5
+ * Parsers for converting Clawdbot memory files to OpenClaw Memory database format:
6
+ * - MEMORY.md → memories table
7
+ * - memory/*.md → sessions + messages
8
+ * - TODO.md → tasks table
9
+ * - LEARNINGS.md → learnings table
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseMemoryMd = parseMemoryMd;
13
+ exports.parseDailyLog = parseDailyLog;
14
+ exports.parseAllDailyLogs = parseAllDailyLogs;
15
+ exports.parseTodoMd = parseTodoMd;
16
+ exports.parseLearningsMd = parseLearningsMd;
17
+ const fs_1 = require("fs");
18
+ const path_1 = require("path");
19
+ // ============ MEMORY.MD PARSER ============
20
+ /**
21
+ * Parse MEMORY.md into structured memories
22
+ *
23
+ * Expected format:
24
+ * # MEMORY.md
25
+ *
26
+ * ## Category Name
27
+ *
28
+ * - Memory item [importance: 0.9]
29
+ * - Another memory [2024-01-28]
30
+ *
31
+ * Regular paragraphs are also captured as memories.
32
+ */
33
+ function parseMemoryMd(filePath) {
34
+ if (!(0, fs_1.existsSync)(filePath)) {
35
+ throw new Error(`File not found: ${filePath}`);
36
+ }
37
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
38
+ const memories = [];
39
+ const lines = content.split('\n');
40
+ let currentCategory = 'general';
41
+ let currentParagraph = '';
42
+ let lineNumber = 0;
43
+ const flushParagraph = () => {
44
+ if (currentParagraph.trim().length > 0) {
45
+ memories.push({
46
+ content: currentParagraph.trim(),
47
+ category: currentCategory,
48
+ importance: 0.6,
49
+ metadata: { source: 'MEMORY.md' }
50
+ });
51
+ currentParagraph = '';
52
+ }
53
+ };
54
+ for (const line of lines) {
55
+ lineNumber++;
56
+ const trimmed = line.trim();
57
+ // Skip title and empty lines at paragraph boundaries
58
+ if (trimmed.startsWith('# ') || trimmed === '') {
59
+ flushParagraph();
60
+ continue;
61
+ }
62
+ // Category header
63
+ if (trimmed.startsWith('## ')) {
64
+ flushParagraph();
65
+ currentCategory = trimmed.slice(3).trim().toLowerCase();
66
+ continue;
67
+ }
68
+ // List item memory
69
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
70
+ flushParagraph();
71
+ let memoryText = trimmed.slice(2).trim();
72
+ let importance = 0.6;
73
+ let createdAt;
74
+ // Extract [importance: X] tags
75
+ const importanceMatch = memoryText.match(/\[importance:\s*([\d.]+)\]/i);
76
+ if (importanceMatch) {
77
+ importance = parseFloat(importanceMatch[1]);
78
+ memoryText = memoryText.replace(importanceMatch[0], '').trim();
79
+ }
80
+ // Extract [YYYY-MM-DD] dates
81
+ const dateMatch = memoryText.match(/\[(\d{4}-\d{2}-\d{2})\]/);
82
+ if (dateMatch) {
83
+ createdAt = dateMatch[1];
84
+ memoryText = memoryText.replace(dateMatch[0], '').trim();
85
+ }
86
+ if (memoryText.length > 0) {
87
+ memories.push({
88
+ content: memoryText,
89
+ category: currentCategory,
90
+ importance,
91
+ created_at: createdAt,
92
+ metadata: { source: 'MEMORY.md', line: lineNumber }
93
+ });
94
+ }
95
+ continue;
96
+ }
97
+ // Regular text - accumulate into paragraph
98
+ if (trimmed.length > 0) {
99
+ currentParagraph += (currentParagraph ? ' ' : '') + trimmed;
100
+ }
101
+ }
102
+ flushParagraph();
103
+ return memories;
104
+ }
105
+ // ============ DAILY LOG PARSER (memory/*.md) ============
106
+ /**
107
+ * Parse daily log files (memory/YYYY-MM-DD.md) into sessions and messages
108
+ *
109
+ * Expected format:
110
+ * # 2024-01-28
111
+ *
112
+ * ## Session: Trading Research
113
+ * Started: 09:00
114
+ *
115
+ * **User**: What's the stock price of TSLA?
116
+ *
117
+ * **Assistant**: Tesla is currently trading at $245.
118
+ *
119
+ * **User**: Should I buy?
120
+ *
121
+ * Summary: Discussed TSLA stock price...
122
+ */
123
+ function parseDailyLog(filePath, userId = 'default') {
124
+ if (!(0, fs_1.existsSync)(filePath)) {
125
+ throw new Error(`File not found: ${filePath}`);
126
+ }
127
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
128
+ const sessions = [];
129
+ const lines = content.split('\n');
130
+ let currentSession = null;
131
+ let currentMessage = '';
132
+ let currentRole = null;
133
+ // Extract date from filename (YYYY-MM-DD.md)
134
+ const dateMatch = filePath.match(/(\d{4}-\d{2}-\d{2})/);
135
+ const fileDate = dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0];
136
+ const flushMessage = () => {
137
+ if (currentMessage.trim() && currentRole && currentSession) {
138
+ currentSession.messages.push({
139
+ role: currentRole,
140
+ content: currentMessage.trim(),
141
+ timestamp: currentSession.started_at
142
+ });
143
+ currentMessage = '';
144
+ currentRole = null;
145
+ }
146
+ };
147
+ const flushSession = () => {
148
+ if (currentSession) {
149
+ sessions.push(currentSession);
150
+ currentSession = null;
151
+ }
152
+ };
153
+ for (const line of lines) {
154
+ const trimmed = line.trim();
155
+ // Session header
156
+ if (trimmed.startsWith('## Session:') || trimmed.startsWith('## ')) {
157
+ flushMessage();
158
+ flushSession();
159
+ const sessionName = trimmed.replace(/^## (Session:?\s*)?/, '').trim();
160
+ currentSession = {
161
+ user_id: userId,
162
+ started_at: `${fileDate}T00:00:00Z`,
163
+ messages: [],
164
+ summary: sessionName
165
+ };
166
+ continue;
167
+ }
168
+ // Started/Ended timestamps
169
+ if (trimmed.startsWith('Started:') && currentSession) {
170
+ const time = trimmed.replace('Started:', '').trim();
171
+ currentSession.started_at = `${fileDate}T${time}:00Z`;
172
+ continue;
173
+ }
174
+ if (trimmed.startsWith('Ended:') && currentSession) {
175
+ const time = trimmed.replace('Ended:', '').trim();
176
+ currentSession.ended_at = `${fileDate}T${time}:00Z`;
177
+ continue;
178
+ }
179
+ // Summary
180
+ if (trimmed.startsWith('Summary:') && currentSession) {
181
+ currentSession.summary = trimmed.replace('Summary:', '').trim();
182
+ continue;
183
+ }
184
+ // Message markers
185
+ if (trimmed.startsWith('**User**:') || trimmed.startsWith('**User**')) {
186
+ flushMessage();
187
+ currentRole = 'user';
188
+ currentMessage = trimmed.replace(/^\*\*User\*\*:?\s*/, '');
189
+ continue;
190
+ }
191
+ if (trimmed.startsWith('**Assistant**:') || trimmed.startsWith('**Assistant**')) {
192
+ flushMessage();
193
+ currentRole = 'assistant';
194
+ currentMessage = trimmed.replace(/^\*\*Assistant\*\*:?\s*/, '');
195
+ continue;
196
+ }
197
+ if (trimmed.startsWith('**System**:') || trimmed.startsWith('**System**')) {
198
+ flushMessage();
199
+ currentRole = 'system';
200
+ currentMessage = trimmed.replace(/^\*\*System\*\*:?\s*/, '');
201
+ continue;
202
+ }
203
+ // Continue accumulating message content
204
+ if (currentRole && trimmed.length > 0) {
205
+ currentMessage += '\n' + trimmed;
206
+ }
207
+ }
208
+ flushMessage();
209
+ flushSession();
210
+ // If no sessions were found but there's content, create a default session
211
+ if (sessions.length === 0 && content.trim().length > 0) {
212
+ sessions.push({
213
+ user_id: userId,
214
+ started_at: `${fileDate}T00:00:00Z`,
215
+ messages: [{
216
+ role: 'system',
217
+ content: content,
218
+ timestamp: `${fileDate}T00:00:00Z`
219
+ }],
220
+ summary: 'Daily log'
221
+ });
222
+ }
223
+ return sessions;
224
+ }
225
+ /**
226
+ * Parse all daily logs in a directory (memory/*.md)
227
+ */
228
+ function parseAllDailyLogs(memoryDir, userId = 'default') {
229
+ if (!(0, fs_1.existsSync)(memoryDir)) {
230
+ throw new Error(`Directory not found: ${memoryDir}`);
231
+ }
232
+ const files = (0, fs_1.readdirSync)(memoryDir)
233
+ .filter(f => f.match(/^\d{4}-\d{2}-\d{2}\.md$/))
234
+ .sort();
235
+ const allSessions = [];
236
+ for (const file of files) {
237
+ const filePath = (0, path_1.join)(memoryDir, file);
238
+ try {
239
+ const sessions = parseDailyLog(filePath, userId);
240
+ allSessions.push(...sessions);
241
+ }
242
+ catch (err) {
243
+ console.error(`⚠️ Failed to parse ${file}:`, err);
244
+ }
245
+ }
246
+ return allSessions;
247
+ }
248
+ // ============ TODO.MD PARSER ============
249
+ /**
250
+ * Parse TODO.md into structured tasks
251
+ *
252
+ * Expected format:
253
+ * # TODO
254
+ *
255
+ * ## Priority: High
256
+ * - [ ] Incomplete task
257
+ * - [x] Completed task
258
+ * - [~] Cancelled task
259
+ *
260
+ * ## Category Name
261
+ * - [ ] Task with [due: 2024-02-01]
262
+ */
263
+ function parseTodoMd(filePath) {
264
+ if (!(0, fs_1.existsSync)(filePath)) {
265
+ throw new Error(`File not found: ${filePath}`);
266
+ }
267
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
268
+ const tasks = [];
269
+ const lines = content.split('\n');
270
+ let currentPriority = 1;
271
+ let currentCategory = 'general';
272
+ for (const line of lines) {
273
+ const trimmed = line.trim();
274
+ // Skip title and empty lines
275
+ if (trimmed.startsWith('# ') || trimmed === '') {
276
+ continue;
277
+ }
278
+ // Category/Section headers
279
+ if (trimmed.startsWith('## ')) {
280
+ const header = trimmed.slice(3).trim();
281
+ // Check for priority indicators
282
+ if (header.toLowerCase().includes('priority')) {
283
+ if (header.toLowerCase().includes('high') || header.toLowerCase().includes('urgent')) {
284
+ currentPriority = 3;
285
+ }
286
+ else if (header.toLowerCase().includes('medium') || header.toLowerCase().includes('normal')) {
287
+ currentPriority = 2;
288
+ }
289
+ else if (header.toLowerCase().includes('low')) {
290
+ currentPriority = 1;
291
+ }
292
+ }
293
+ else {
294
+ currentCategory = header.toLowerCase();
295
+ }
296
+ continue;
297
+ }
298
+ // Task items
299
+ const taskMatch = trimmed.match(/^[-*]\s*\[([ x~])\]\s*(.+)$/i);
300
+ if (taskMatch) {
301
+ const statusChar = taskMatch[1];
302
+ let taskText = taskMatch[2].trim();
303
+ let status = 'pending';
304
+ if (statusChar.toLowerCase() === 'x')
305
+ status = 'completed';
306
+ if (statusChar === '~')
307
+ status = 'cancelled';
308
+ // Extract [due: YYYY-MM-DD] tags
309
+ let dueDate;
310
+ const dueDateMatch = taskText.match(/\[due:\s*(\d{4}-\d{2}-\d{2})\]/i);
311
+ if (dueDateMatch) {
312
+ dueDate = dueDateMatch[1];
313
+ taskText = taskText.replace(dueDateMatch[0], '').trim();
314
+ }
315
+ if (taskText.length > 0) {
316
+ tasks.push({
317
+ title: taskText,
318
+ status,
319
+ priority: currentPriority,
320
+ due_date: dueDate,
321
+ metadata: { source: 'TODO.md', category: currentCategory }
322
+ });
323
+ }
324
+ }
325
+ }
326
+ return tasks;
327
+ }
328
+ // ============ LEARNINGS.MD PARSER ============
329
+ /**
330
+ * Parse LEARNINGS.md into structured learnings
331
+ *
332
+ * Expected format:
333
+ * # LEARNINGS
334
+ *
335
+ * ## Category: Corrections
336
+ *
337
+ * **Trigger**: User said "actually, I prefer Rust"
338
+ * **Lesson**: User prefers Rust over TypeScript
339
+ * **Importance**: 0.8
340
+ *
341
+ * ---
342
+ *
343
+ * ## Category: Errors
344
+ * ...
345
+ */
346
+ function parseLearningsMd(filePath) {
347
+ if (!(0, fs_1.existsSync)(filePath)) {
348
+ throw new Error(`File not found: ${filePath}`);
349
+ }
350
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
351
+ const learnings = [];
352
+ // Split by --- separators
353
+ const blocks = content.split(/\n---+\n/);
354
+ for (const block of blocks) {
355
+ const lines = block.trim().split('\n');
356
+ let category = 'general';
357
+ let trigger = '';
358
+ let lesson = '';
359
+ let importance = 0.5;
360
+ let createdAt;
361
+ for (const line of lines) {
362
+ const trimmed = line.trim();
363
+ // Category header
364
+ if (trimmed.startsWith('## ')) {
365
+ const header = trimmed.slice(3).trim();
366
+ const catMatch = header.match(/Category:\s*(.+)/i);
367
+ if (catMatch) {
368
+ category = catMatch[1].trim().toLowerCase();
369
+ }
370
+ continue;
371
+ }
372
+ // Field lines
373
+ const triggerMatch = trimmed.match(/^\*\*Trigger\*\*:\s*(.+)/i);
374
+ if (triggerMatch) {
375
+ trigger = triggerMatch[1].trim();
376
+ continue;
377
+ }
378
+ const lessonMatch = trimmed.match(/^\*\*Lesson\*\*:\s*(.+)/i);
379
+ if (lessonMatch) {
380
+ lesson = lessonMatch[1].trim();
381
+ continue;
382
+ }
383
+ const importanceMatch = trimmed.match(/^\*\*Importance\*\*:\s*([\d.]+)/i);
384
+ if (importanceMatch) {
385
+ importance = parseFloat(importanceMatch[1]);
386
+ continue;
387
+ }
388
+ const dateMatch = trimmed.match(/^\*\*Date\*\*:\s*(\d{4}-\d{2}-\d{2})/i);
389
+ if (dateMatch) {
390
+ createdAt = dateMatch[1];
391
+ continue;
392
+ }
393
+ }
394
+ // Add if we have at minimum a lesson
395
+ if (lesson.length > 0) {
396
+ learnings.push({
397
+ category,
398
+ trigger: trigger || 'Unknown trigger',
399
+ lesson,
400
+ importance,
401
+ created_at: createdAt
402
+ });
403
+ }
404
+ }
405
+ return learnings;
406
+ }
@@ -0,0 +1,153 @@
1
+ -- OpenClaw Memory - Initial Schema
2
+ -- Run this in your Supabase SQL editor
3
+
4
+ -- Enable vector extension for semantic search
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ -- Sessions: Every conversation gets a session
8
+ CREATE TABLE IF NOT EXISTS sessions (
9
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
10
+ agent_id TEXT NOT NULL,
11
+ user_id TEXT,
12
+ channel TEXT,
13
+ started_at TIMESTAMPTZ DEFAULT NOW(),
14
+ ended_at TIMESTAMPTZ,
15
+ summary TEXT,
16
+ metadata JSONB DEFAULT '{}'
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS sessions_agent_id_idx ON sessions(agent_id);
20
+ CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON sessions(user_id);
21
+ CREATE INDEX IF NOT EXISTS sessions_started_at_idx ON sessions(started_at DESC);
22
+
23
+ -- Messages: Every message in every session
24
+ CREATE TABLE IF NOT EXISTS messages (
25
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
26
+ session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
27
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
28
+ content TEXT NOT NULL,
29
+ created_at TIMESTAMPTZ DEFAULT NOW(),
30
+ token_count INT,
31
+ metadata JSONB DEFAULT '{}'
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS messages_session_id_idx ON messages(session_id);
35
+ CREATE INDEX IF NOT EXISTS messages_created_at_idx ON messages(created_at);
36
+
37
+ -- Memories: Long-term memories extracted from sessions
38
+ CREATE TABLE IF NOT EXISTS memories (
39
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
40
+ agent_id TEXT NOT NULL,
41
+ user_id TEXT,
42
+ category TEXT,
43
+ content TEXT NOT NULL,
44
+ importance FLOAT DEFAULT 0.5 CHECK (importance >= 0 AND importance <= 1),
45
+ source_session_id UUID REFERENCES sessions(id),
46
+ created_at TIMESTAMPTZ DEFAULT NOW(),
47
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
48
+ expires_at TIMESTAMPTZ,
49
+ embedding VECTOR(1536),
50
+ metadata JSONB DEFAULT '{}'
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS memories_agent_id_idx ON memories(agent_id);
54
+ CREATE INDEX IF NOT EXISTS memories_user_id_idx ON memories(user_id);
55
+ CREATE INDEX IF NOT EXISTS memories_category_idx ON memories(category);
56
+ CREATE INDEX IF NOT EXISTS memories_importance_idx ON memories(importance DESC);
57
+ CREATE INDEX IF NOT EXISTS memories_created_at_idx ON memories(created_at DESC);
58
+
59
+ -- Vector similarity index (for semantic search)
60
+ CREATE INDEX IF NOT EXISTS memories_embedding_idx ON memories
61
+ USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
62
+
63
+ -- Entities: People, places, things the agent knows about
64
+ CREATE TABLE IF NOT EXISTS entities (
65
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
66
+ agent_id TEXT NOT NULL,
67
+ entity_type TEXT NOT NULL,
68
+ name TEXT NOT NULL,
69
+ aliases TEXT[],
70
+ description TEXT,
71
+ properties JSONB DEFAULT '{}',
72
+ first_seen_at TIMESTAMPTZ DEFAULT NOW(),
73
+ last_seen_at TIMESTAMPTZ DEFAULT NOW(),
74
+ mention_count INT DEFAULT 1,
75
+ embedding VECTOR(1536),
76
+
77
+ UNIQUE(agent_id, entity_type, name)
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS entities_agent_id_idx ON entities(agent_id);
81
+ CREATE INDEX IF NOT EXISTS entities_type_idx ON entities(entity_type);
82
+ CREATE INDEX IF NOT EXISTS entities_name_idx ON entities(name);
83
+
84
+ -- Tasks: Persistent task tracking
85
+ CREATE TABLE IF NOT EXISTS tasks (
86
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
87
+ agent_id TEXT NOT NULL,
88
+ user_id TEXT,
89
+ title TEXT NOT NULL,
90
+ description TEXT,
91
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'blocked', 'done')),
92
+ priority INT DEFAULT 0,
93
+ due_at TIMESTAMPTZ,
94
+ completed_at TIMESTAMPTZ,
95
+ source_session_id UUID REFERENCES sessions(id),
96
+ parent_task_id UUID REFERENCES tasks(id),
97
+ metadata JSONB DEFAULT '{}',
98
+ created_at TIMESTAMPTZ DEFAULT NOW(),
99
+ updated_at TIMESTAMPTZ DEFAULT NOW()
100
+ );
101
+
102
+ CREATE INDEX IF NOT EXISTS tasks_agent_id_idx ON tasks(agent_id);
103
+ CREATE INDEX IF NOT EXISTS tasks_user_id_idx ON tasks(user_id);
104
+ CREATE INDEX IF NOT EXISTS tasks_status_idx ON tasks(status);
105
+ CREATE INDEX IF NOT EXISTS tasks_priority_idx ON tasks(priority DESC);
106
+ CREATE INDEX IF NOT EXISTS tasks_due_at_idx ON tasks(due_at);
107
+
108
+ -- Learnings: Self-improvement records
109
+ CREATE TABLE IF NOT EXISTS learnings (
110
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
111
+ agent_id TEXT NOT NULL,
112
+ category TEXT NOT NULL CHECK (category IN ('error', 'correction', 'improvement', 'capability_gap')),
113
+ trigger TEXT NOT NULL,
114
+ lesson TEXT NOT NULL,
115
+ action TEXT,
116
+ severity TEXT DEFAULT 'info' CHECK (severity IN ('info', 'warning', 'critical')),
117
+ source_session_id UUID REFERENCES sessions(id),
118
+ applied_count INT DEFAULT 0,
119
+ created_at TIMESTAMPTZ DEFAULT NOW(),
120
+ metadata JSONB DEFAULT '{}'
121
+ );
122
+
123
+ CREATE INDEX IF NOT EXISTS learnings_agent_id_idx ON learnings(agent_id);
124
+ CREATE INDEX IF NOT EXISTS learnings_category_idx ON learnings(category);
125
+ CREATE INDEX IF NOT EXISTS learnings_severity_idx ON learnings(severity);
126
+
127
+ -- Function to update updated_at timestamp
128
+ CREATE OR REPLACE FUNCTION update_updated_at()
129
+ RETURNS TRIGGER AS $$
130
+ BEGIN
131
+ NEW.updated_at = NOW();
132
+ RETURN NEW;
133
+ END;
134
+ $$ LANGUAGE plpgsql;
135
+
136
+ -- Triggers for updated_at
137
+ CREATE TRIGGER memories_updated_at
138
+ BEFORE UPDATE ON memories
139
+ FOR EACH ROW
140
+ EXECUTE FUNCTION update_updated_at();
141
+
142
+ CREATE TRIGGER tasks_updated_at
143
+ BEFORE UPDATE ON tasks
144
+ FOR EACH ROW
145
+ EXECUTE FUNCTION update_updated_at();
146
+
147
+ -- Row Level Security (optional - enable if using Supabase auth)
148
+ -- ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
149
+ -- ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
150
+ -- ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
151
+ -- ALTER TABLE entities ENABLE ROW LEVEL SECURITY;
152
+ -- ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
153
+ -- ALTER TABLE learnings ENABLE ROW LEVEL SECURITY;