obol-ai 0.2.39 → 0.3.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,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,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
@@ -1,28 +1,20 @@
1
1
  const { loadEvolutionState } = require('./state');
2
2
 
3
- const MIN_EXCHANGES_FOR_EVOLUTION = 10;
4
-
5
- async function checkEvolution(userDir, messageLog) {
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
- const { loadConfig } = require('../config');
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 sinceFilter = state.lastEvolution ? `&created_at=gt.${state.lastEvolution}` : '';
18
- const userFilter = messageLog.userId ? `&user_id=eq.${messageLog.userId}` : '';
19
- const res = await fetch(
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 { ready: Array.isArray(rows) && rows.length >= minExchanges };
17
+ return lastDate !== todayDate;
26
18
  }
27
19
 
28
- module.exports = { checkEvolution };
20
+ module.exports = { shouldEvolveNow };
@@ -18,13 +18,14 @@ const MODELS = {
18
18
  };
19
19
  const MAX_FIX_ATTEMPTS = 1;
20
20
 
21
- async function evolve(claudeClient, messageLog, memory, userDir) {
21
+ async function evolve(claudeClient, messageLog, memory, userDir, supabaseConfig = null, selfMemory = null) {
22
+ const { PERSONALITY_DIR } = require('../soul');
22
23
  const baseDir = userDir || OBOL_DIR;
23
24
  const state = loadEvolutionState(userDir);
24
- const personalityDir = path.join(baseDir, 'personality');
25
- const soulPath = path.join(personalityDir, 'SOUL.md');
26
- const userPath = path.join(personalityDir, 'USER.md');
27
- const agentsPath = path.join(personalityDir, 'AGENTS.md');
25
+ const userPersonalityDir = path.join(baseDir, 'personality');
26
+ const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
27
+ const agentsPath = path.join(userPersonalityDir, 'AGENTS.md');
28
+ const userPath = path.join(userPersonalityDir, 'USER.md');
28
29
  const scriptsDir = path.join(baseDir, 'scripts');
29
30
  const testsDir = path.join(baseDir, 'tests');
30
31
  const commandsDir = path.join(baseDir, 'commands');
@@ -32,7 +33,7 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
32
33
  const currentSoul = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
33
34
  const currentUser = fs.existsSync(userPath) ? fs.readFileSync(userPath, 'utf-8') : '';
34
35
  const currentAgents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf-8') : '';
35
- const currentTraits = loadTraits(personalityDir);
36
+ const currentTraits = loadTraits(userPersonalityDir);
36
37
  const currentScripts = readDir(scriptsDir);
37
38
  const currentTests = readDir(testsDir);
38
39
  const currentCommands = readDir(commandsDir);
@@ -41,11 +42,12 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
41
42
  if (messageLog) {
42
43
  try {
43
44
  const userFilter = messageLog.userId ? `&user_id=eq.${messageLog.userId}` : '';
45
+ const sinceFilter = state.lastEvolution ? `&created_at=gt.${state.lastEvolution}` : '';
44
46
  const res = await fetch(
45
- `${messageLog.url}/rest/v1/obol_messages?order=created_at.desc&limit=100&select=role,content,created_at${userFilter}`,
47
+ `${messageLog.url}/rest/v1/obol_messages?order=created_at.asc&limit=500&select=role,content,created_at${userFilter}${sinceFilter}`,
46
48
  { headers: messageLog.headers }
47
49
  );
48
- recentMessages = (await res.json()).reverse();
50
+ recentMessages = await res.json();
49
51
  } catch (e) {
50
52
  console.error('[evolve] Failed to fetch recent messages:', e.message);
51
53
  }
@@ -87,8 +89,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
87
89
  }
88
90
  }
89
91
 
92
+ let selfMemories = [];
93
+ if (selfMemory) {
94
+ try {
95
+ selfMemories = await selfMemory.query({ minImportance: 0.5, limit: 30 });
96
+ } catch (e) {
97
+ console.error('[evolve] Failed to fetch self memories:', e.message);
98
+ }
99
+ }
100
+
90
101
  let previousSoul = '';
91
- const archiveDir = path.join(personalityDir, 'evolution');
102
+ const archiveDir = path.join(PERSONALITY_DIR, 'evolution');
92
103
  try {
93
104
  if (fs.existsSync(archiveDir)) {
94
105
  const archives = fs.readdirSync(archiveDir)
@@ -132,6 +143,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
132
143
  .map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
133
144
  .join('\n\n');
134
145
 
146
+ const selfCategoryLabels = { research: 'Research', interest: 'Interests', self: 'Self-reflection', pattern: 'Patterns' };
147
+ const selfMemoryGroups = {};
148
+ for (const m of selfMemories) {
149
+ const group = selfCategoryLabels[m.category] || 'Other';
150
+ if (!selfMemoryGroups[group]) selfMemoryGroups[group] = [];
151
+ selfMemoryGroups[group].push(m.content);
152
+ }
153
+ const selfMemorySummary = Object.entries(selfMemoryGroups)
154
+ .map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
155
+ .join('\n\n');
156
+
135
157
  const scriptsManifest = Object.entries(currentScripts)
136
158
  .map(([name, content]) => `### ${name}\n\`\`\`\n${content.substring(0, 500)}\n\`\`\``)
137
159
  .join('\n\n') || '(no scripts)';
@@ -156,23 +178,24 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
156
178
 
157
179
  const isFirstEvolution = !currentSoul;
158
180
  let growthReport = '';
159
- if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0)) {
181
+ if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0 || selfMemories.length > 0)) {
160
182
  try {
161
183
  const growthResponse = await claudeClient.messages.create({
162
184
  model: MODELS.personality,
163
185
  max_tokens: 2048,
164
- system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories and conversations since the last evolution.
186
+ system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories, conversations, and the AI's own inner life (things it researched, discovered, and reflected on during curiosity cycles) since the last evolution.
165
187
 
166
188
  Produce a structured growth report covering:
167
189
 
168
190
  1. NEW LEARNINGS — What new facts, skills, or knowledge emerged
169
- 2. RELATIONSHIP SHIFTSHow the dynamic with the owner changed (closer, more trust, new friction, etc.)
170
- 3. BEHAVIORAL PATTERNSRecurring interaction styles or habits observed
171
- 4. GROWTH EDGESAreas where the personality is being pushed or pulled in new directions
172
- 5. TRAIT PRESSUREWhich traits should shift and why (cite specific evidence from conversations/memories)
173
- 6. IDENTITY CONTINUITYWhat core aspects stayed the same and should be preserved
174
-
175
- Be specific. Cite evidence from the conversations and memories. This report guides the evolution rewrite.`,
191
+ 2. INNER LIFEWhat the AI has been curious about, researched, or reflected on independently; how this shapes who it is becoming
192
+ 3. RELATIONSHIP SHIFTSHow the dynamic with the owner changed (closer, more trust, new friction, etc.)
193
+ 4. BEHAVIORAL PATTERNSRecurring interaction styles or habits observed
194
+ 5. GROWTH EDGESAreas where the personality is being pushed or pulled in new directions
195
+ 6. TRAIT PRESSUREWhich traits should shift and why (cite specific evidence from conversations/memories)
196
+ 7. IDENTITY CONTINUITY — What core aspects stayed the same and should be preserved
197
+
198
+ Be specific. Cite evidence from the conversations, memories, and self-memories. This report guides the evolution rewrite.`,
176
199
  messages: [{
177
200
  role: 'user',
178
201
  content: `## Previous SOUL (before current evolution)
@@ -187,7 +210,7 @@ ${JSON.stringify(currentTraits)}
187
210
  ## New Memories Since Last Evolution (${recentMemories.length})
188
211
  ${recentMemorySummary || '(none)'}
189
212
 
190
- ## Recent Conversations (${recentMessages.length} messages)
213
+ ${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently during curiosity cycles:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (${recentMessages.length} messages)
191
214
  ${transcript.substring(0, 30000)}`,
192
215
  }],
193
216
  });
@@ -254,7 +277,7 @@ ${commandsManifest}
254
277
  ## Core Memories (highest importance)
255
278
  ${memorySummary || '(no memories yet)'}
256
279
 
257
- ${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
280
+ ${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently — this is Obol's inner life, shaping who it is becoming:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
258
281
  ${transcript || '(no conversations yet)'}
259
282
 
260
283
  ---
@@ -393,6 +416,12 @@ Fix the scripts. Tests define correct behavior.`
393
416
  }
394
417
 
395
418
  fs.writeFileSync(soulPath, result.soul);
419
+ if (supabaseConfig) {
420
+ const { backup } = require('../soul');
421
+ backup(supabaseConfig, 'soul', result.soul).catch(e =>
422
+ console.error('[evolve] Soul backup failed:', e.message)
423
+ );
424
+ }
396
425
 
397
426
  if (result.user && result.user.length > 50) {
398
427
  fs.writeFileSync(userPath, result.user);
@@ -1,6 +1,6 @@
1
- const { checkEvolution } = require('./check');
1
+ const { shouldEvolveNow } = require('./check');
2
2
  const { evolve } = require('./evolve');
3
3
  const { runTests } = require('./tests');
4
4
  const { loadEvolutionState } = require('./state');
5
5
 
6
- module.exports = { checkEvolution, evolve, runTests, loadEvolutionState };
6
+ module.exports = { shouldEvolveNow, evolve, runTests, loadEvolutionState };