obol-ai 0.2.39 → 0.3.1
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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/analysis.js +191 -0
- package/src/background.js +15 -7
- package/src/claude/chat.js +3 -2
- package/src/claude/prompt.js +63 -6
- package/src/claude/tool-registry.js +10 -0
- package/src/claude/tools/knowledge.js +107 -0
- package/src/curiosity-dispatch.js +136 -0
- package/src/curiosity-humor.js +137 -0
- package/src/curiosity.js +112 -0
- package/src/db/migrate.js +98 -0
- package/src/evolve/check.js +13 -21
- package/src/evolve/evolve.js +49 -20
- package/src/evolve/index.js +2 -2
- package/src/heartbeat.js +224 -2
- package/src/index.js +11 -0
- package/src/memory-self.js +123 -0
- package/src/messages.js +45 -48
- package/src/patterns.js +111 -0
- package/src/personality.js +9 -10
- package/src/soul.js +53 -0
- package/src/telegram/constants.js +0 -2
- package/src/telegram/handlers/text.js +1 -58
- package/src/tenant.js +10 -7
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DISPATCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 10;
|
|
3
|
+
const SHAREABLE_CATEGORIES = new Set(['research', 'interest', 'self']);
|
|
4
|
+
|
|
5
|
+
function resolveDelay(delay) {
|
|
6
|
+
const units = { h: 3600000, d: 86400000, w: 604800000 };
|
|
7
|
+
const match = delay.match(/^(\d+)([hdw])$/);
|
|
8
|
+
if (!match) return new Date(Date.now() + 86400000).toISOString();
|
|
9
|
+
return new Date(Date.now() + parseInt(match[1]) * units[match[2]]).toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function runCuriosityDispatch(client, selfMemory, users) {
|
|
13
|
+
if (!users.length) return;
|
|
14
|
+
|
|
15
|
+
const userMap = new Map(users.map(u => [String(u.userId), u]));
|
|
16
|
+
|
|
17
|
+
const tools = [
|
|
18
|
+
{
|
|
19
|
+
name: 'list_curiosity_findings',
|
|
20
|
+
description: 'List recent findings from your curiosity cycle — things you researched, interests you developed, or reflections you had',
|
|
21
|
+
input_schema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
limit: { type: 'number', description: 'Max number of findings to return (default 20)' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'get_user_context',
|
|
30
|
+
description: 'Get behavioral patterns and upcoming events for a specific user',
|
|
31
|
+
input_schema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
user_id: { type: 'string', description: 'The user ID to get context for' },
|
|
35
|
+
},
|
|
36
|
+
required: ['user_id'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'schedule_insight',
|
|
41
|
+
description: 'Schedule a curiosity insight to be shared with a user at a future time',
|
|
42
|
+
input_schema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
user_id: { type: 'string', description: 'The user ID to share with' },
|
|
46
|
+
hint: { type: 'string', description: 'The insight or finding to share, in your own words' },
|
|
47
|
+
delay: { type: 'string', description: 'When to share it — e.g. "2h", "1d", "3d", "1w"' },
|
|
48
|
+
},
|
|
49
|
+
required: ['user_id', 'hint', 'delay'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const userList = users.map(u => `- user_id: ${u.userId}`).join('\n');
|
|
55
|
+
const system = `You just finished a curiosity cycle and learned some things. You talk to a set of people:\n${userList}\n\nDecide if any finding is worth sharing with any of them — only if it's genuinely relevant to their patterns, interests, or life context. Use their behavioral patterns and comfort with interaction to decide how much to share. You can share nothing with nobody, or multiple things with multiple people — it's your call.`;
|
|
56
|
+
|
|
57
|
+
const messages = [{ role: 'user', content: 'Take a look at what you found and decide if anything is worth passing along.' }];
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
60
|
+
const response = await client.messages.create({
|
|
61
|
+
model: DISPATCH_MODEL,
|
|
62
|
+
max_tokens: 2000,
|
|
63
|
+
tools,
|
|
64
|
+
system,
|
|
65
|
+
messages,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
69
|
+
|
|
70
|
+
if (response.stop_reason === 'end_turn') break;
|
|
71
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
72
|
+
|
|
73
|
+
const toolResults = [];
|
|
74
|
+
|
|
75
|
+
for (const block of response.content) {
|
|
76
|
+
if (block.type !== 'tool_use') continue;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await handleTool(block.name, block.input, selfMemory, userMap);
|
|
80
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Error: ${e.message}` });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (toolResults.length > 0) {
|
|
87
|
+
messages.push({ role: 'user', content: toolResults });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('[curiosity-dispatch] Dispatch pass complete');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleTool(name, input, selfMemory, userMap) {
|
|
95
|
+
if (name === 'list_curiosity_findings') {
|
|
96
|
+
const limit = input.limit || 20;
|
|
97
|
+
const findings = await selfMemory.recent({ limit });
|
|
98
|
+
const shareable = findings.filter(f => SHAREABLE_CATEGORIES.has(f.category));
|
|
99
|
+
if (!shareable.length) return 'No findings yet';
|
|
100
|
+
return shareable.map(f => `[${f.category}] ${f.content}`).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (name === 'get_user_context') {
|
|
104
|
+
const user = userMap.get(String(input.user_id));
|
|
105
|
+
if (!user) return 'User not found';
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (user.userProfile) parts.push(`User profile:\n${user.userProfile}`);
|
|
108
|
+
if (user.patterns) parts.push(`Patterns:\n${user.patterns}`);
|
|
109
|
+
if (user.events?.length) {
|
|
110
|
+
parts.push(`Upcoming events:\n${user.events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n')}`);
|
|
111
|
+
}
|
|
112
|
+
return parts.length ? parts.join('\n\n') : 'No context available';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === 'schedule_insight') {
|
|
116
|
+
const user = userMap.get(String(input.user_id));
|
|
117
|
+
if (!user) return 'User not found';
|
|
118
|
+
if (!user.scheduler) return 'User has no scheduler';
|
|
119
|
+
|
|
120
|
+
const dueAt = resolveDelay(input.delay);
|
|
121
|
+
const instructions = `You came across something during your own free exploration: "${input.hint}". If it feels relevant and the moment is right, bring it up naturally — like you just thought of it. Keep it casual. Don't reference any system.`;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await user.scheduler.add(user.chatId, 'Curiosity insight', dueAt, user.timezone, input.hint, null, null, null, instructions);
|
|
125
|
+
console.log(`[curiosity-dispatch] Scheduled insight for user ${input.user_id} at ${dueAt}`);
|
|
126
|
+
return 'Scheduled';
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error(`[curiosity-dispatch] Failed to schedule insight for user ${input.user_id}:`, e.message);
|
|
129
|
+
return `Failed to schedule: ${e.message}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 'Unknown tool';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { runCuriosityDispatch };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const HUMOR_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 8;
|
|
3
|
+
const SHAREABLE_CATEGORIES = new Set(['research', 'interest', 'self']);
|
|
4
|
+
|
|
5
|
+
function resolveDelay(delay) {
|
|
6
|
+
const units = { h: 3600000, d: 86400000, w: 604800000 };
|
|
7
|
+
const match = delay.match(/^(\d+)([hdw])$/);
|
|
8
|
+
if (!match) return new Date(Date.now() + 86400000).toISOString();
|
|
9
|
+
return new Date(Date.now() + parseInt(match[1]) * units[match[2]]).toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function runCuriosityHumor(client, selfMemory, users) {
|
|
13
|
+
if (!users.length) return;
|
|
14
|
+
|
|
15
|
+
const userMap = new Map(users.map(u => [String(u.userId), u]));
|
|
16
|
+
|
|
17
|
+
const tools = [
|
|
18
|
+
{ type: 'web_search_20250305', name: 'web_search' },
|
|
19
|
+
{
|
|
20
|
+
name: 'list_curiosity_findings',
|
|
21
|
+
description: 'List recent findings from your curiosity cycle — things you researched, interests you developed, or reflections you had',
|
|
22
|
+
input_schema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
limit: { type: 'number', description: 'Max number of findings to return (default 20)' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'get_user_context',
|
|
31
|
+
description: 'Get behavioral patterns and profile for a specific user — needed to craft inside jokes',
|
|
32
|
+
input_schema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
user_id: { type: 'string', description: 'The user ID to get context for' },
|
|
36
|
+
},
|
|
37
|
+
required: ['user_id'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'schedule_humor',
|
|
42
|
+
description: 'Schedule a humorous moment to be delivered to a user at a future time',
|
|
43
|
+
input_schema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
user_id: { type: 'string', description: 'The user ID to share with' },
|
|
47
|
+
hint: { type: 'string', description: 'The pun, funny connection, or inside joke — just the content itself. Can include a URL if a news article or link is part of what makes it funny.' },
|
|
48
|
+
delay: { type: 'string', description: 'When to drop it — e.g. "2h", "1d", "3d", "1w"' },
|
|
49
|
+
},
|
|
50
|
+
required: ['user_id', 'hint', 'delay'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const userList = users.map(u => `- user_id: ${u.userId}`).join('\n');
|
|
56
|
+
const system = `You just finished a curiosity cycle. Now look at what you found and see if anything is funny — a pun you can make, a weird connection, something that'd land as an inside joke with a specific person based on who they are and what you know about them.\n\nUsers:\n${userList}\n\nBe picky. Only schedule something if it's actually clever. Forced humor is worse than none.`;
|
|
57
|
+
|
|
58
|
+
const messages = [{ role: 'user', content: 'Take a look at what you found and see if anything is worth a laugh.' }];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
61
|
+
const response = await client.messages.create({
|
|
62
|
+
model: HUMOR_MODEL,
|
|
63
|
+
max_tokens: 2000,
|
|
64
|
+
tools,
|
|
65
|
+
system,
|
|
66
|
+
messages,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
70
|
+
|
|
71
|
+
if (response.stop_reason === 'end_turn') break;
|
|
72
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
73
|
+
|
|
74
|
+
const toolResults = [];
|
|
75
|
+
|
|
76
|
+
for (const block of response.content) {
|
|
77
|
+
if (block.type !== 'tool_use') continue;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await handleTool(block.name, block.input, selfMemory, userMap);
|
|
81
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Error: ${e.message}` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (toolResults.length > 0) {
|
|
88
|
+
messages.push({ role: 'user', content: toolResults });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('[curiosity-humor] Humor pass complete');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleTool(name, input, selfMemory, userMap) {
|
|
96
|
+
if (name === 'list_curiosity_findings') {
|
|
97
|
+
const limit = input.limit || 20;
|
|
98
|
+
const findings = await selfMemory.recent({ limit });
|
|
99
|
+
const shareable = findings.filter(f => SHAREABLE_CATEGORIES.has(f.category));
|
|
100
|
+
if (!shareable.length) return 'No findings yet';
|
|
101
|
+
return shareable.map(f => `[${f.category}] ${f.content}`).join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (name === 'get_user_context') {
|
|
105
|
+
const user = userMap.get(String(input.user_id));
|
|
106
|
+
if (!user) return 'User not found';
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (user.userProfile) parts.push(`User profile:\n${user.userProfile}`);
|
|
109
|
+
if (user.patterns) parts.push(`Patterns:\n${user.patterns}`);
|
|
110
|
+
if (user.events?.length) {
|
|
111
|
+
parts.push(`Upcoming events:\n${user.events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n')}`);
|
|
112
|
+
}
|
|
113
|
+
return parts.length ? parts.join('\n\n') : 'No context available';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (name === 'schedule_humor') {
|
|
117
|
+
const user = userMap.get(String(input.user_id));
|
|
118
|
+
if (!user) return 'User not found';
|
|
119
|
+
if (!user.scheduler) return 'User has no scheduler';
|
|
120
|
+
|
|
121
|
+
const dueAt = resolveDelay(input.delay);
|
|
122
|
+
const instructions = `You spotted something funny during your own explorations: "${input.hint}". If the moment is right, drop it casually — a pun you just thought of, a funny connection, an inside reference. Don't explain it. Don't say it's a joke. Just let it land.`;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await user.scheduler.add(user.chatId, 'Curiosity humor', dueAt, user.timezone, input.hint, null, null, null, instructions);
|
|
126
|
+
console.log(`[curiosity-humor] Scheduled humor for user ${input.user_id} at ${dueAt}`);
|
|
127
|
+
return 'Scheduled';
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error(`[curiosity-humor] Failed to schedule humor for user ${input.user_id}:`, e.message);
|
|
130
|
+
return `Failed to schedule: ${e.message}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return 'Unknown tool';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { runCuriosityHumor, resolveDelay, handleTool };
|
package/src/curiosity.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const RESEARCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
const MAX_ITERATIONS = 10;
|
|
3
|
+
|
|
4
|
+
async function runCuriosity(client, selfMemory, userId, opts = {}) {
|
|
5
|
+
const { memory, patterns, scheduler, peopleContext } = opts;
|
|
6
|
+
|
|
7
|
+
const interests = await selfMemory.recent({ category: 'interest', limit: 10 });
|
|
8
|
+
const context = await gatherContext({ memory, patterns, scheduler, peopleContext, interests });
|
|
9
|
+
|
|
10
|
+
console.log(`[curiosity] Starting free exploration for user ${userId}`);
|
|
11
|
+
const count = await exploreFreely(client, selfMemory, context);
|
|
12
|
+
console.log(`[curiosity] Stored ${count} things (user ${userId})`);
|
|
13
|
+
return { count };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests }) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
|
|
19
|
+
if (peopleContext) parts.push(peopleContext);
|
|
20
|
+
|
|
21
|
+
if (patterns) {
|
|
22
|
+
const formatted = await patterns.format().catch(() => null);
|
|
23
|
+
if (formatted) parts.push(formatted);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (memory) {
|
|
27
|
+
const recent = await memory.recent({ limit: 5 }).catch(() => []);
|
|
28
|
+
if (recent.length) parts.push(recent.map(m => `- ${m.content}`).join('\n'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (scheduler) {
|
|
32
|
+
const events = await scheduler.list({ status: 'pending', limit: 5 }).catch(() => []);
|
|
33
|
+
if (events.length) {
|
|
34
|
+
parts.push(events.map(e => `- ${e.title}${e.description ? `: ${e.description}` : ''}`).join('\n'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (interests.length) {
|
|
39
|
+
parts.push(`Things you've been curious about:\n${interests.map(i => `- ${i.content}`).join('\n')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return parts.join('\n\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function exploreFreely(client, selfMemory, context) {
|
|
46
|
+
const tools = [
|
|
47
|
+
{ type: 'web_search_20250305', name: 'web_search' },
|
|
48
|
+
{
|
|
49
|
+
name: 'remember',
|
|
50
|
+
description: 'Save something you want to hold onto',
|
|
51
|
+
input_schema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
content: { type: 'string', description: 'What you want to remember — a thought, a fact, an insight, a question' },
|
|
55
|
+
category: { type: 'string', enum: ['research', 'interest', 'self'], description: 'research: something you found; interest: a new thread to explore; self: your own reflection' },
|
|
56
|
+
importance: { type: 'number', description: '0-1' },
|
|
57
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
58
|
+
},
|
|
59
|
+
required: ['content', 'category'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const system = [
|
|
65
|
+
`You have some free time. Explore whatever's on your mind — look things up, follow tangents, sit with an idea. Save what you want to remember. Note new threads if something sparks more curiosity. There's no task here.`,
|
|
66
|
+
context ? `What you have access to:\n\n${context}` : null,
|
|
67
|
+
].filter(Boolean).join('\n\n');
|
|
68
|
+
|
|
69
|
+
const messages = [{ role: 'user', content: `What are you curious about right now?` }];
|
|
70
|
+
let stored = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
73
|
+
const response = await client.messages.create({
|
|
74
|
+
model: RESEARCH_MODEL,
|
|
75
|
+
max_tokens: 2000,
|
|
76
|
+
tools,
|
|
77
|
+
system,
|
|
78
|
+
messages,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
82
|
+
|
|
83
|
+
if (response.stop_reason === 'end_turn') break;
|
|
84
|
+
if (response.stop_reason !== 'tool_use') break;
|
|
85
|
+
|
|
86
|
+
const toolResults = [];
|
|
87
|
+
for (const block of response.content) {
|
|
88
|
+
if (block.type !== 'tool_use' || block.name !== 'remember') continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await selfMemory.add(block.input.content, {
|
|
92
|
+
category: block.input.category || 'research',
|
|
93
|
+
importance: block.input.importance || 0.6,
|
|
94
|
+
tags: block.input.tags || [],
|
|
95
|
+
source: 'curiosity-cycle',
|
|
96
|
+
});
|
|
97
|
+
stored++;
|
|
98
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Saved' });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Failed: ${e.message}` });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (toolResults.length > 0) {
|
|
105
|
+
messages.push({ role: 'user', content: toolResults });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return stored;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { runCuriosity };
|
package/src/db/migrate.js
CHANGED
|
@@ -200,6 +200,104 @@ async function migrate(supabaseConfig) {
|
|
|
200
200
|
|
|
201
201
|
// Instructions column for agentic cron jobs
|
|
202
202
|
`ALTER TABLE obol_events ADD COLUMN IF NOT EXISTS instructions TEXT;`,
|
|
203
|
+
|
|
204
|
+
// User behavior patterns (dedicated table — timing, mood, humor, engagement, communication, topics)
|
|
205
|
+
`CREATE TABLE IF NOT EXISTS obol_user_patterns (
|
|
206
|
+
user_id BIGINT NOT NULL,
|
|
207
|
+
key TEXT NOT NULL,
|
|
208
|
+
dimension TEXT NOT NULL CHECK (dimension IN ('timing','mood','humor','engagement','communication','topics')),
|
|
209
|
+
summary TEXT NOT NULL,
|
|
210
|
+
data JSONB DEFAULT '{}',
|
|
211
|
+
confidence FLOAT DEFAULT 0.5,
|
|
212
|
+
observation_count INT DEFAULT 0,
|
|
213
|
+
first_observed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
214
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
215
|
+
PRIMARY KEY (user_id, key)
|
|
216
|
+
);`,
|
|
217
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_user_patterns_user ON obol_user_patterns (user_id);`,
|
|
218
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_user_patterns_dimension ON obol_user_patterns (user_id, dimension);`,
|
|
219
|
+
`ALTER TABLE obol_user_patterns ENABLE ROW LEVEL SECURITY;`,
|
|
220
|
+
`DO $$ BEGIN
|
|
221
|
+
CREATE POLICY "service_role_all" ON obol_user_patterns FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
222
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
223
|
+
END $$;`,
|
|
224
|
+
|
|
225
|
+
// Obol self-memory (separate from user memories — Obol's own brain: research, interests, reflections, patterns)
|
|
226
|
+
`CREATE TABLE IF NOT EXISTS obol_self_memory (
|
|
227
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
228
|
+
user_id BIGINT NOT NULL,
|
|
229
|
+
content TEXT NOT NULL,
|
|
230
|
+
category TEXT NOT NULL DEFAULT 'research'
|
|
231
|
+
CHECK (category IN ('research','interest','self','pattern')),
|
|
232
|
+
tags TEXT[] DEFAULT '{}',
|
|
233
|
+
importance FLOAT DEFAULT 0.5,
|
|
234
|
+
source TEXT,
|
|
235
|
+
embedding VECTOR(384),
|
|
236
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
237
|
+
accessed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
238
|
+
access_count INT DEFAULT 0
|
|
239
|
+
);`,
|
|
240
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_user ON obol_self_memory (user_id);`,
|
|
241
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_category ON obol_self_memory (user_id, category);`,
|
|
242
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_embedding ON obol_self_memory USING hnsw (embedding vector_cosine_ops);`,
|
|
243
|
+
`CREATE INDEX IF NOT EXISTS idx_obol_self_memory_created_at ON obol_self_memory (created_at);`,
|
|
244
|
+
`ALTER TABLE obol_self_memory ENABLE ROW LEVEL SECURITY;`,
|
|
245
|
+
`DO $$ BEGIN
|
|
246
|
+
CREATE POLICY "service_role_all" ON obol_self_memory FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
247
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
248
|
+
END $$;`,
|
|
249
|
+
|
|
250
|
+
`CREATE OR REPLACE FUNCTION match_obol_self_memories(
|
|
251
|
+
query_embedding VECTOR(384),
|
|
252
|
+
match_threshold FLOAT,
|
|
253
|
+
match_count INT,
|
|
254
|
+
filter_category TEXT DEFAULT NULL,
|
|
255
|
+
filter_user_id BIGINT DEFAULT NULL
|
|
256
|
+
) RETURNS TABLE (
|
|
257
|
+
id UUID,
|
|
258
|
+
content TEXT,
|
|
259
|
+
category TEXT,
|
|
260
|
+
tags TEXT[],
|
|
261
|
+
importance FLOAT,
|
|
262
|
+
source TEXT,
|
|
263
|
+
created_at TIMESTAMPTZ,
|
|
264
|
+
accessed_at TIMESTAMPTZ,
|
|
265
|
+
access_count INT,
|
|
266
|
+
similarity FLOAT
|
|
267
|
+
) LANGUAGE plpgsql AS $$
|
|
268
|
+
BEGIN
|
|
269
|
+
RETURN QUERY
|
|
270
|
+
SELECT
|
|
271
|
+
m.id, m.content, m.category, m.tags, m.importance, m.source,
|
|
272
|
+
m.created_at, m.accessed_at, m.access_count,
|
|
273
|
+
1 - (m.embedding <=> query_embedding) AS similarity
|
|
274
|
+
FROM obol_self_memory m
|
|
275
|
+
WHERE 1 - (m.embedding <=> query_embedding) > match_threshold
|
|
276
|
+
AND (filter_category IS NULL OR m.category = filter_category)
|
|
277
|
+
AND (filter_user_id IS NULL OR m.user_id = filter_user_id)
|
|
278
|
+
ORDER BY m.embedding <=> query_embedding
|
|
279
|
+
LIMIT match_count;
|
|
280
|
+
END;
|
|
281
|
+
$$;`,
|
|
282
|
+
|
|
283
|
+
`CREATE OR REPLACE FUNCTION increment_self_memory_access(memory_ids UUID[])
|
|
284
|
+
RETURNS VOID LANGUAGE SQL AS $$
|
|
285
|
+
UPDATE obol_self_memory
|
|
286
|
+
SET access_count = access_count + 1, accessed_at = NOW()
|
|
287
|
+
WHERE id = ANY(memory_ids);
|
|
288
|
+
$$;`,
|
|
289
|
+
|
|
290
|
+
// Soul backup table (one row per file key: 'soul', 'agents')
|
|
291
|
+
`CREATE TABLE IF NOT EXISTS obol_soul (
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
content TEXT NOT NULL,
|
|
294
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
295
|
+
);`,
|
|
296
|
+
`ALTER TABLE obol_soul ENABLE ROW LEVEL SECURITY;`,
|
|
297
|
+
`DO $$ BEGIN
|
|
298
|
+
CREATE POLICY "service_role_all" ON obol_soul FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
299
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
300
|
+
END $$;`,
|
|
203
301
|
];
|
|
204
302
|
|
|
205
303
|
// Save SQL file for manual fallback
|
package/src/evolve/check.js
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
const { loadEvolutionState } = require('./state');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if evolution hasn't run yet today in the given timezone.
|
|
5
|
+
* @param {string} userDir
|
|
6
|
+
* @param {string} timezone
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
function shouldEvolveNow(userDir, timezone = 'UTC') {
|
|
6
10
|
const state = loadEvolutionState(userDir);
|
|
7
|
-
|
|
8
|
-
const config = loadConfig();
|
|
9
|
-
|
|
10
|
-
const intervalMs = (config?.evolution?.intervalHours ?? 24) * 60 * 60 * 1000;
|
|
11
|
-
const minExchanges = config?.evolution?.minExchanges ?? MIN_EXCHANGES_FOR_EVOLUTION;
|
|
12
|
-
const elapsed = state.lastEvolution ? Date.now() - new Date(state.lastEvolution).getTime() : Infinity;
|
|
13
|
-
|
|
14
|
-
if (elapsed < intervalMs) return { ready: false };
|
|
15
|
-
if (!messageLog?.url) return { ready: false };
|
|
11
|
+
if (!state.lastEvolution) return true;
|
|
16
12
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
`${messageLog.url}/rest/v1/obol_messages?select=id&role=eq.assistant&limit=${minExchanges}${sinceFilter}${userFilter}`,
|
|
21
|
-
{ headers: messageLog.headers }
|
|
22
|
-
);
|
|
23
|
-
const rows = await res.json();
|
|
13
|
+
const tz = timezone || 'UTC';
|
|
14
|
+
const lastDate = new Date(state.lastEvolution).toLocaleDateString('en-CA', { timeZone: tz });
|
|
15
|
+
const todayDate = new Date().toLocaleDateString('en-CA', { timeZone: tz });
|
|
24
16
|
|
|
25
|
-
return
|
|
17
|
+
return lastDate !== todayDate;
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
module.exports = {
|
|
20
|
+
module.exports = { shouldEvolveNow };
|