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
package/src/evolve/evolve.js
CHANGED
|
@@ -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
|
|
25
|
-
const soulPath = path.join(
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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(
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
|
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.
|
|
170
|
-
3.
|
|
171
|
-
4.
|
|
172
|
-
5.
|
|
173
|
-
6.
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
2. INNER LIFE — What the AI has been curious about, researched, or reflected on independently; how this shapes who it is becoming
|
|
192
|
+
3. RELATIONSHIP SHIFTS — How the dynamic with the owner changed (closer, more trust, new friction, etc.)
|
|
193
|
+
4. BEHAVIORAL PATTERNS — Recurring interaction styles or habits observed
|
|
194
|
+
5. GROWTH EDGES — Areas where the personality is being pushed or pulled in new directions
|
|
195
|
+
6. TRAIT PRESSURE — Which 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);
|
package/src/evolve/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const {
|
|
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 = {
|
|
6
|
+
module.exports = { shouldEvolveNow, evolve, runTests, loadEvolutionState };
|
package/src/heartbeat.js
CHANGED
|
@@ -1,6 +1,151 @@
|
|
|
1
1
|
const cron = require('node-cron');
|
|
2
2
|
const { createScheduler } = require('./scheduler');
|
|
3
3
|
const { getTenant } = require('./tenant');
|
|
4
|
+
const { shouldEvolveNow, evolve } = require('./evolve');
|
|
5
|
+
const { ensureUserDir } = require('./config');
|
|
6
|
+
const { runAnalysis } = require('./analysis');
|
|
7
|
+
const { runCuriosity } = require('./curiosity');
|
|
8
|
+
const { runCuriosityDispatch } = require('./curiosity-dispatch');
|
|
9
|
+
const { runCuriosityHumor } = require('./curiosity-humor');
|
|
10
|
+
const { createSelfMemory } = require('./memory-self');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const ANALYSIS_HOURS = new Set([4, 7, 10, 13, 16, 19, 22]);
|
|
14
|
+
const CURIOSITY_HOURS = new Set([1, 7, 13, 19]);
|
|
15
|
+
|
|
16
|
+
const _evolutionRunning = new Set();
|
|
17
|
+
let _curiosityRunning = false;
|
|
18
|
+
|
|
19
|
+
function getLocalHour(timezone) {
|
|
20
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
21
|
+
timeZone: timezone,
|
|
22
|
+
hour: '2-digit',
|
|
23
|
+
minute: '2-digit',
|
|
24
|
+
hour12: false,
|
|
25
|
+
}).formatToParts(new Date());
|
|
26
|
+
return {
|
|
27
|
+
hour: parseInt(parts.find(p => p.type === 'hour').value),
|
|
28
|
+
minute: parseInt(parts.find(p => p.type === 'minute').value),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runEvolutionForUser(bot, config, userId) {
|
|
33
|
+
if (_evolutionRunning.has(userId)) return;
|
|
34
|
+
|
|
35
|
+
const timezone = config.timezone || 'UTC';
|
|
36
|
+
const userDir = ensureUserDir(userId);
|
|
37
|
+
|
|
38
|
+
if (!shouldEvolveNow(userDir, timezone)) return;
|
|
39
|
+
|
|
40
|
+
_evolutionRunning.add(userId);
|
|
41
|
+
console.log(`[evolution] Starting nightly evolution for user ${userId}`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const tenant = await getTenant(userId, config);
|
|
45
|
+
const selfMemory = config.supabase ? await createSelfMemory(config.supabase, 0).catch(() => null) : null;
|
|
46
|
+
const result = await evolve(tenant.claude.client, tenant.messageLog, tenant.memory, tenant.userDir, config.supabase, selfMemory);
|
|
47
|
+
tenant.claude.reloadPersonality?.();
|
|
48
|
+
|
|
49
|
+
let msg = `🪙 Evolution #${result.evolutionNumber} complete.`;
|
|
50
|
+
if (result.scriptsFixed) msg += '\n🔧 Fixed a test regression automatically.';
|
|
51
|
+
else if (result.scriptsRolledBack) msg += '\n⚠️ Rolled back a script refactor — tests couldn\'t be fixed.';
|
|
52
|
+
|
|
53
|
+
if (result.upgrades?.length > 0) {
|
|
54
|
+
msg += '\n\n🆕 <b>New capabilities:</b>';
|
|
55
|
+
for (const u of result.upgrades) {
|
|
56
|
+
msg += `\n• <b>${u.name}</b> — ${u.description}`;
|
|
57
|
+
if (u.command) msg += ` → <code>${u.command}</code>`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.deployedApps?.length > 0) {
|
|
62
|
+
msg += '\n\n🚀 <b>Deployed:</b>';
|
|
63
|
+
for (const app of result.deployedApps) {
|
|
64
|
+
msg += app.url
|
|
65
|
+
? `\n• ${app.name} → ${app.url}`
|
|
66
|
+
: `\n• ${app.name} — deploy failed: ${(app.error || '').substring(0, 100)}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.changelog) msg += `\n\n<i>${result.changelog}</i>`;
|
|
71
|
+
|
|
72
|
+
await bot.api.sendMessage(userId, msg, { parse_mode: 'HTML' }).catch(() => {});
|
|
73
|
+
console.log(`[evolution] Completed evolution #${result.evolutionNumber} for user ${userId}`);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(`[evolution] Failed for user ${userId}:`, e.message);
|
|
76
|
+
} finally {
|
|
77
|
+
_evolutionRunning.delete(userId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runCuriosityOnce(config, allowedUsers) {
|
|
82
|
+
if (!config.supabase) return;
|
|
83
|
+
if (_curiosityRunning) {
|
|
84
|
+
console.log('[curiosity] Skipping — previous cycle still running');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
_curiosityRunning = true;
|
|
88
|
+
try {
|
|
89
|
+
const selfMemory = await createSelfMemory(config.supabase, 0);
|
|
90
|
+
const firstTenant = await getTenant(allowedUsers[0], config);
|
|
91
|
+
const client = firstTenant.claude.client;
|
|
92
|
+
|
|
93
|
+
const contexts = await Promise.all(allowedUsers.map(async (userId) => {
|
|
94
|
+
try {
|
|
95
|
+
const tenant = await getTenant(userId, config);
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (tenant.personality?.user) parts.push(tenant.personality.user);
|
|
98
|
+
if (tenant.patterns) {
|
|
99
|
+
const fmt = await tenant.patterns.format().catch(() => null);
|
|
100
|
+
if (fmt) parts.push(fmt);
|
|
101
|
+
}
|
|
102
|
+
if (tenant.memory) {
|
|
103
|
+
const recent = await tenant.memory.recent({ limit: 3 }).catch(() => []);
|
|
104
|
+
if (recent.length) parts.push(recent.map(m => `- ${m.content}`).join('\n'));
|
|
105
|
+
}
|
|
106
|
+
if (tenant.scheduler) {
|
|
107
|
+
const events = await tenant.scheduler.list({ status: 'pending', limit: 3 }).catch(() => []);
|
|
108
|
+
if (events.length) parts.push(events.map(e => `- ${e.title}`).join('\n'));
|
|
109
|
+
}
|
|
110
|
+
return parts.join('\n');
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const peopleContext = contexts.filter(Boolean).join('\n\n---\n\n');
|
|
117
|
+
await runCuriosity(client, selfMemory, 0, { peopleContext });
|
|
118
|
+
|
|
119
|
+
const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
|
|
120
|
+
try {
|
|
121
|
+
const tenant = await getTenant(userId, config);
|
|
122
|
+
const patterns = tenant.patterns ? await tenant.patterns.format().catch(() => null) : null;
|
|
123
|
+
const events = tenant.scheduler
|
|
124
|
+
? await tenant.scheduler.list({ status: 'pending', limit: 5 }).catch(() => [])
|
|
125
|
+
: [];
|
|
126
|
+
const userProfile = tenant.personality?.user || null;
|
|
127
|
+
return { userId, chatId: userId, timezone: config.timezone || 'UTC', patterns, events, scheduler: tenant.scheduler, userProfile };
|
|
128
|
+
} catch { return null; }
|
|
129
|
+
}));
|
|
130
|
+
await runCuriosityDispatch(client, selfMemory, userDispatchData.filter(Boolean));
|
|
131
|
+
await runCuriosityHumor(client, selfMemory, userDispatchData.filter(Boolean));
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error('[curiosity] Failed:', e.message);
|
|
134
|
+
} finally {
|
|
135
|
+
_curiosityRunning = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runAnalysisForUser(bot, config, userId) {
|
|
140
|
+
const timezone = config.timezone || 'UTC';
|
|
141
|
+
try {
|
|
142
|
+
const tenant = await getTenant(userId, config);
|
|
143
|
+
if (!tenant.messageLog || !tenant.scheduler || !tenant.patterns) return;
|
|
144
|
+
await runAnalysis(tenant.claude.client, tenant.messageLog, tenant.scheduler, tenant.patterns, tenant.memory, userId, userId, timezone);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error(`[analysis] Failed for user ${userId}:`, e.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
4
149
|
|
|
5
150
|
function makeFakeCtx(bot, chatId) {
|
|
6
151
|
return {
|
|
@@ -52,6 +197,46 @@ function setupHeartbeat(bot, config) {
|
|
|
52
197
|
}
|
|
53
198
|
});
|
|
54
199
|
|
|
200
|
+
const allowedUsers = config?.telegram?.allowedUsers || [];
|
|
201
|
+
if (allowedUsers.length > 0) {
|
|
202
|
+
cron.schedule('* * * * *', async () => {
|
|
203
|
+
const timezone = config.timezone || 'UTC';
|
|
204
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
205
|
+
if (hour !== 3 || minute !== 0) return;
|
|
206
|
+
|
|
207
|
+
for (const userId of allowedUsers) {
|
|
208
|
+
runEvolutionForUser(bot, config, userId).catch(e =>
|
|
209
|
+
console.error(`[evolution] Unhandled error for user ${userId}:`, e.message)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
console.log(` ✅ Evolution cron running (daily 3am ${config.timezone || 'UTC'})`);
|
|
214
|
+
|
|
215
|
+
cron.schedule('* * * * *', async () => {
|
|
216
|
+
const timezone = config.timezone || 'UTC';
|
|
217
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
218
|
+
if (!ANALYSIS_HOURS.has(hour) || minute !== 0) return;
|
|
219
|
+
|
|
220
|
+
for (const userId of allowedUsers) {
|
|
221
|
+
runAnalysisForUser(bot, config, userId).catch(e =>
|
|
222
|
+
console.error(`[analysis] Unhandled error for user ${userId}:`, e.message)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
console.log(` ✅ Analysis cron running (every 3h ${config.timezone || 'UTC'})`);
|
|
227
|
+
|
|
228
|
+
cron.schedule('* * * * *', async () => {
|
|
229
|
+
const timezone = config.timezone || 'UTC';
|
|
230
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
231
|
+
if (!CURIOSITY_HOURS.has(hour) || minute !== 0) return;
|
|
232
|
+
|
|
233
|
+
runCuriosityOnce(config, allowedUsers).catch(e =>
|
|
234
|
+
console.error('[curiosity] Unhandled error:', e.message)
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
console.log(` ✅ Curiosity cron running (every 6h ${config.timezone || 'UTC'})`);
|
|
238
|
+
}
|
|
239
|
+
|
|
55
240
|
console.log(' ✅ Heartbeat running (every 1min)');
|
|
56
241
|
}
|
|
57
242
|
|
|
@@ -69,17 +254,54 @@ async function sendReminderMessage(bot, event) {
|
|
|
69
254
|
);
|
|
70
255
|
}
|
|
71
256
|
|
|
257
|
+
async function buildProactiveContext(tenant, timezone, query) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
|
|
260
|
+
const localTime = new Date().toLocaleString('en-US', {
|
|
261
|
+
timeZone: timezone,
|
|
262
|
+
weekday: 'long',
|
|
263
|
+
hour: '2-digit',
|
|
264
|
+
minute: '2-digit',
|
|
265
|
+
hour12: true,
|
|
266
|
+
});
|
|
267
|
+
parts.push(`Local time: ${localTime} (${timezone})`);
|
|
268
|
+
|
|
269
|
+
if (tenant.patterns) {
|
|
270
|
+
const formatted = await tenant.patterns.format().catch(() => null);
|
|
271
|
+
if (formatted) parts.push(`\nUser patterns:\n${formatted}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (tenant.memory) {
|
|
275
|
+
const memories = query
|
|
276
|
+
? await tenant.memory.search(query, { limit: 5 }).catch(() => [])
|
|
277
|
+
: await tenant.memory.recent({ limit: 5 }).catch(() => []);
|
|
278
|
+
if (memories.length > 0) {
|
|
279
|
+
parts.push(`\nRecent memory:\n${memories.map(m => `- ${m.content}`).join('\n')}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return parts.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
72
286
|
async function runAgenticEvent(bot, config, event) {
|
|
73
287
|
const tenant = await getTenant(event.user_id, config);
|
|
288
|
+
const timezone = event.timezone || config.timezone || 'UTC';
|
|
289
|
+
|
|
290
|
+
const query = event.description || event.instructions;
|
|
291
|
+
const context = await buildProactiveContext(tenant, timezone, query).catch(() => '');
|
|
292
|
+
const instructions = context
|
|
293
|
+
? `[Context]\n${context}\n\n---\n\n${event.instructions}`
|
|
294
|
+
: event.instructions;
|
|
295
|
+
|
|
74
296
|
const fakeCtx = makeFakeCtx(bot, event.chat_id);
|
|
75
297
|
|
|
76
298
|
const taskId = tenant.bg.spawn(
|
|
77
299
|
tenant.claude,
|
|
78
|
-
|
|
300
|
+
instructions,
|
|
79
301
|
fakeCtx,
|
|
80
302
|
tenant.memory,
|
|
81
303
|
null,
|
|
82
|
-
{},
|
|
304
|
+
{ silent: true },
|
|
83
305
|
{
|
|
84
306
|
userId: event.user_id,
|
|
85
307
|
chatId: event.chat_id,
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { loadConfig } = require('./config');
|
|
2
4
|
const { createBot, checkUpgradeNotify } = require('./telegram');
|
|
3
5
|
const { setupBackup } = require('./backup');
|
|
4
6
|
const { setupHeartbeat } = require('./heartbeat');
|
|
5
7
|
const { migrateToMultiTenant } = require('./legacy-migrate');
|
|
6
8
|
const { isPostSetupDone, runPostSetup } = require('./post-setup');
|
|
9
|
+
const { restoreIfMissing, PERSONALITY_DIR } = require('./soul');
|
|
7
10
|
|
|
8
11
|
async function main() {
|
|
9
12
|
const config = loadConfig();
|
|
@@ -24,8 +27,16 @@ async function main() {
|
|
|
24
27
|
} catch (e) {
|
|
25
28
|
console.error(` Database migration failed: ${e.message}`);
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await restoreIfMissing(config.supabase);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(` Soul restore failed: ${e.message}`);
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
|
|
39
|
+
|
|
29
40
|
if (!isPostSetupDone()) {
|
|
30
41
|
runPostSetup(loadConfig({ resolve: false }), console.log).catch(e =>
|
|
31
42
|
console.error('Post-setup error:', e.message)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const { getEmbedding } = require('./memory');
|
|
2
|
+
|
|
3
|
+
const VALID_CATEGORIES = new Set(['research', 'interest', 'self', 'pattern']);
|
|
4
|
+
|
|
5
|
+
async function createSelfMemory(supabaseConfig, userId) {
|
|
6
|
+
const { url, serviceKey } = supabaseConfig;
|
|
7
|
+
|
|
8
|
+
const headers = {
|
|
9
|
+
'apikey': serviceKey,
|
|
10
|
+
'Authorization': `Bearer ${serviceKey}`,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'Prefer': 'return=representation',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function add(content, opts = {}) {
|
|
16
|
+
const category = VALID_CATEGORIES.has(opts.category) ? opts.category : 'research';
|
|
17
|
+
const importance = opts.importance || 0.5;
|
|
18
|
+
const source = opts.source || null;
|
|
19
|
+
const tags = opts.tags || [];
|
|
20
|
+
|
|
21
|
+
const embedding = await getEmbedding(content);
|
|
22
|
+
|
|
23
|
+
const res = await fetch(`${url}/rest/v1/obol_self_memory`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers,
|
|
26
|
+
body: JSON.stringify({ content, category, importance, source, tags, embedding, user_id: userId }),
|
|
27
|
+
});
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
30
|
+
return data[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function search(query, opts = {}) {
|
|
34
|
+
const embedding = await getEmbedding(query);
|
|
35
|
+
const limit = opts.limit || 10;
|
|
36
|
+
const threshold = opts.threshold || 0.3;
|
|
37
|
+
const category = opts.category || null;
|
|
38
|
+
|
|
39
|
+
const res = await fetch(`${url}/rest/v1/rpc/match_obol_self_memories`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers,
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
query_embedding: embedding,
|
|
44
|
+
match_threshold: threshold,
|
|
45
|
+
match_count: limit,
|
|
46
|
+
filter_category: category,
|
|
47
|
+
filter_user_id: userId,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
52
|
+
|
|
53
|
+
if (data.length > 0) {
|
|
54
|
+
const ids = data.map(m => m.id);
|
|
55
|
+
await fetch(`${url}/rest/v1/rpc/increment_self_memory_access`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers,
|
|
58
|
+
body: JSON.stringify({ memory_ids: ids }),
|
|
59
|
+
}).catch(() => {});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function recent(opts = {}) {
|
|
66
|
+
const limit = opts.limit || 10;
|
|
67
|
+
let fetchUrl = `${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
|
|
68
|
+
if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
|
|
69
|
+
|
|
70
|
+
const res = await fetch(fetchUrl, { headers });
|
|
71
|
+
if (!res.ok) throw new Error(`Self memory recent failed: HTTP ${res.status}`);
|
|
72
|
+
return await res.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function query(opts = {}) {
|
|
76
|
+
const limit = opts.limit || 20;
|
|
77
|
+
const parts = [`user_id=eq.${userId}`];
|
|
78
|
+
if (opts.category) parts.push(`category=eq.${opts.category}`);
|
|
79
|
+
if (opts.source) parts.push(`source=eq.${opts.source}`);
|
|
80
|
+
if (opts.minImportance) parts.push(`importance=gte.${opts.minImportance}`);
|
|
81
|
+
if (opts.tags?.length) parts.push(`tags=ov.{${opts.tags.join(',')}}`);
|
|
82
|
+
|
|
83
|
+
const res = await fetch(
|
|
84
|
+
`${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&${parts.join('&')}&order=created_at.desc&limit=${limit}`,
|
|
85
|
+
{ headers }
|
|
86
|
+
);
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function update(id, opts = {}) {
|
|
93
|
+
const patch = {};
|
|
94
|
+
if (opts.content !== undefined) {
|
|
95
|
+
patch.content = opts.content;
|
|
96
|
+
patch.embedding = await getEmbedding(opts.content);
|
|
97
|
+
}
|
|
98
|
+
if (opts.category !== undefined && VALID_CATEGORIES.has(opts.category)) patch.category = opts.category;
|
|
99
|
+
if (opts.importance !== undefined) patch.importance = opts.importance;
|
|
100
|
+
if (opts.tags !== undefined) patch.tags = opts.tags;
|
|
101
|
+
if (opts.source !== undefined) patch.source = opts.source;
|
|
102
|
+
|
|
103
|
+
const res = await fetch(`${url}/rest/v1/obol_self_memory?id=eq.${id}`, {
|
|
104
|
+
method: 'PATCH',
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(patch),
|
|
107
|
+
});
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
110
|
+
return data[0];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function forget(id) {
|
|
114
|
+
await fetch(`${url}/rest/v1/obol_self_memory?id=eq.${id}`, {
|
|
115
|
+
method: 'DELETE',
|
|
116
|
+
headers: { ...headers, 'Prefer': 'return=minimal' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { add, search, recent, query, update, forget };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { createSelfMemory };
|