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.
- package/CHANGELOG.md +15 -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.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 +222 -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/heartbeat.js
CHANGED
|
@@ -1,6 +1,149 @@
|
|
|
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 { createSelfMemory } = require('./memory-self');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const ANALYSIS_HOURS = new Set([4, 7, 10, 13, 16, 19, 22]);
|
|
13
|
+
const CURIOSITY_HOURS = new Set([1, 7, 13, 19]);
|
|
14
|
+
|
|
15
|
+
const _evolutionRunning = new Set();
|
|
16
|
+
let _curiosityRunning = false;
|
|
17
|
+
|
|
18
|
+
function getLocalHour(timezone) {
|
|
19
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
20
|
+
timeZone: timezone,
|
|
21
|
+
hour: '2-digit',
|
|
22
|
+
minute: '2-digit',
|
|
23
|
+
hour12: false,
|
|
24
|
+
}).formatToParts(new Date());
|
|
25
|
+
return {
|
|
26
|
+
hour: parseInt(parts.find(p => p.type === 'hour').value),
|
|
27
|
+
minute: parseInt(parts.find(p => p.type === 'minute').value),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runEvolutionForUser(bot, config, userId) {
|
|
32
|
+
if (_evolutionRunning.has(userId)) return;
|
|
33
|
+
|
|
34
|
+
const timezone = config.timezone || 'UTC';
|
|
35
|
+
const userDir = ensureUserDir(userId);
|
|
36
|
+
|
|
37
|
+
if (!shouldEvolveNow(userDir, timezone)) return;
|
|
38
|
+
|
|
39
|
+
_evolutionRunning.add(userId);
|
|
40
|
+
console.log(`[evolution] Starting nightly evolution for user ${userId}`);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const tenant = await getTenant(userId, config);
|
|
44
|
+
const selfMemory = config.supabase ? await createSelfMemory(config.supabase, 0).catch(() => null) : null;
|
|
45
|
+
const result = await evolve(tenant.claude.client, tenant.messageLog, tenant.memory, tenant.userDir, config.supabase, selfMemory);
|
|
46
|
+
tenant.claude.reloadPersonality?.();
|
|
47
|
+
|
|
48
|
+
let msg = `šŖ Evolution #${result.evolutionNumber} complete.`;
|
|
49
|
+
if (result.scriptsFixed) msg += '\nš§ Fixed a test regression automatically.';
|
|
50
|
+
else if (result.scriptsRolledBack) msg += '\nā ļø Rolled back a script refactor ā tests couldn\'t be fixed.';
|
|
51
|
+
|
|
52
|
+
if (result.upgrades?.length > 0) {
|
|
53
|
+
msg += '\n\nš <b>New capabilities:</b>';
|
|
54
|
+
for (const u of result.upgrades) {
|
|
55
|
+
msg += `\n⢠<b>${u.name}</b> ā ${u.description}`;
|
|
56
|
+
if (u.command) msg += ` ā <code>${u.command}</code>`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (result.deployedApps?.length > 0) {
|
|
61
|
+
msg += '\n\nš <b>Deployed:</b>';
|
|
62
|
+
for (const app of result.deployedApps) {
|
|
63
|
+
msg += app.url
|
|
64
|
+
? `\n⢠${app.name} ā ${app.url}`
|
|
65
|
+
: `\n⢠${app.name} ā deploy failed: ${(app.error || '').substring(0, 100)}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (result.changelog) msg += `\n\n<i>${result.changelog}</i>`;
|
|
70
|
+
|
|
71
|
+
await bot.api.sendMessage(userId, msg, { parse_mode: 'HTML' }).catch(() => {});
|
|
72
|
+
console.log(`[evolution] Completed evolution #${result.evolutionNumber} for user ${userId}`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(`[evolution] Failed for user ${userId}:`, e.message);
|
|
75
|
+
} finally {
|
|
76
|
+
_evolutionRunning.delete(userId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runCuriosityOnce(config, allowedUsers) {
|
|
81
|
+
if (!config.supabase) return;
|
|
82
|
+
if (_curiosityRunning) {
|
|
83
|
+
console.log('[curiosity] Skipping ā previous cycle still running');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
_curiosityRunning = true;
|
|
87
|
+
try {
|
|
88
|
+
const selfMemory = await createSelfMemory(config.supabase, 0);
|
|
89
|
+
const firstTenant = await getTenant(allowedUsers[0], config);
|
|
90
|
+
const client = firstTenant.claude.client;
|
|
91
|
+
|
|
92
|
+
const contexts = await Promise.all(allowedUsers.map(async (userId) => {
|
|
93
|
+
try {
|
|
94
|
+
const tenant = await getTenant(userId, config);
|
|
95
|
+
const parts = [];
|
|
96
|
+
if (tenant.personality?.user) parts.push(tenant.personality.user);
|
|
97
|
+
if (tenant.patterns) {
|
|
98
|
+
const fmt = await tenant.patterns.format().catch(() => null);
|
|
99
|
+
if (fmt) parts.push(fmt);
|
|
100
|
+
}
|
|
101
|
+
if (tenant.memory) {
|
|
102
|
+
const recent = await tenant.memory.recent({ limit: 3 }).catch(() => []);
|
|
103
|
+
if (recent.length) parts.push(recent.map(m => `- ${m.content}`).join('\n'));
|
|
104
|
+
}
|
|
105
|
+
if (tenant.scheduler) {
|
|
106
|
+
const events = await tenant.scheduler.list({ status: 'pending', limit: 3 }).catch(() => []);
|
|
107
|
+
if (events.length) parts.push(events.map(e => `- ${e.title}`).join('\n'));
|
|
108
|
+
}
|
|
109
|
+
return parts.join('\n');
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const peopleContext = contexts.filter(Boolean).join('\n\n---\n\n');
|
|
116
|
+
await runCuriosity(client, selfMemory, 0, { peopleContext });
|
|
117
|
+
|
|
118
|
+
const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
|
|
119
|
+
try {
|
|
120
|
+
const tenant = await getTenant(userId, config);
|
|
121
|
+
const patterns = tenant.patterns ? await tenant.patterns.format().catch(() => null) : null;
|
|
122
|
+
const events = tenant.scheduler
|
|
123
|
+
? await tenant.scheduler.list({ status: 'pending', limit: 5 }).catch(() => [])
|
|
124
|
+
: [];
|
|
125
|
+
const userProfile = tenant.personality?.user || null;
|
|
126
|
+
return { userId, chatId: userId, timezone: config.timezone || 'UTC', patterns, events, scheduler: tenant.scheduler, userProfile };
|
|
127
|
+
} catch { return null; }
|
|
128
|
+
}));
|
|
129
|
+
await runCuriosityDispatch(client, selfMemory, userDispatchData.filter(Boolean));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error('[curiosity] Failed:', e.message);
|
|
132
|
+
} finally {
|
|
133
|
+
_curiosityRunning = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runAnalysisForUser(bot, config, userId) {
|
|
138
|
+
const timezone = config.timezone || 'UTC';
|
|
139
|
+
try {
|
|
140
|
+
const tenant = await getTenant(userId, config);
|
|
141
|
+
if (!tenant.messageLog || !tenant.scheduler || !tenant.patterns) return;
|
|
142
|
+
await runAnalysis(tenant.claude.client, tenant.messageLog, tenant.scheduler, tenant.patterns, tenant.memory, userId, userId, timezone);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(`[analysis] Failed for user ${userId}:`, e.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
4
147
|
|
|
5
148
|
function makeFakeCtx(bot, chatId) {
|
|
6
149
|
return {
|
|
@@ -52,6 +195,46 @@ function setupHeartbeat(bot, config) {
|
|
|
52
195
|
}
|
|
53
196
|
});
|
|
54
197
|
|
|
198
|
+
const allowedUsers = config?.telegram?.allowedUsers || [];
|
|
199
|
+
if (allowedUsers.length > 0) {
|
|
200
|
+
cron.schedule('* * * * *', async () => {
|
|
201
|
+
const timezone = config.timezone || 'UTC';
|
|
202
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
203
|
+
if (hour !== 3 || minute !== 0) return;
|
|
204
|
+
|
|
205
|
+
for (const userId of allowedUsers) {
|
|
206
|
+
runEvolutionForUser(bot, config, userId).catch(e =>
|
|
207
|
+
console.error(`[evolution] Unhandled error for user ${userId}:`, e.message)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
console.log(` ā
Evolution cron running (daily 3am ${config.timezone || 'UTC'})`);
|
|
212
|
+
|
|
213
|
+
cron.schedule('* * * * *', async () => {
|
|
214
|
+
const timezone = config.timezone || 'UTC';
|
|
215
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
216
|
+
if (!ANALYSIS_HOURS.has(hour) || minute !== 0) return;
|
|
217
|
+
|
|
218
|
+
for (const userId of allowedUsers) {
|
|
219
|
+
runAnalysisForUser(bot, config, userId).catch(e =>
|
|
220
|
+
console.error(`[analysis] Unhandled error for user ${userId}:`, e.message)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
console.log(` ā
Analysis cron running (every 3h ${config.timezone || 'UTC'})`);
|
|
225
|
+
|
|
226
|
+
cron.schedule('* * * * *', async () => {
|
|
227
|
+
const timezone = config.timezone || 'UTC';
|
|
228
|
+
const { hour, minute } = getLocalHour(timezone);
|
|
229
|
+
if (!CURIOSITY_HOURS.has(hour) || minute !== 0) return;
|
|
230
|
+
|
|
231
|
+
runCuriosityOnce(config, allowedUsers).catch(e =>
|
|
232
|
+
console.error('[curiosity] Unhandled error:', e.message)
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
console.log(` ā
Curiosity cron running (every 6h ${config.timezone || 'UTC'})`);
|
|
236
|
+
}
|
|
237
|
+
|
|
55
238
|
console.log(' ā
Heartbeat running (every 1min)');
|
|
56
239
|
}
|
|
57
240
|
|
|
@@ -69,17 +252,54 @@ async function sendReminderMessage(bot, event) {
|
|
|
69
252
|
);
|
|
70
253
|
}
|
|
71
254
|
|
|
255
|
+
async function buildProactiveContext(tenant, timezone, query) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
|
|
258
|
+
const localTime = new Date().toLocaleString('en-US', {
|
|
259
|
+
timeZone: timezone,
|
|
260
|
+
weekday: 'long',
|
|
261
|
+
hour: '2-digit',
|
|
262
|
+
minute: '2-digit',
|
|
263
|
+
hour12: true,
|
|
264
|
+
});
|
|
265
|
+
parts.push(`Local time: ${localTime} (${timezone})`);
|
|
266
|
+
|
|
267
|
+
if (tenant.patterns) {
|
|
268
|
+
const formatted = await tenant.patterns.format().catch(() => null);
|
|
269
|
+
if (formatted) parts.push(`\nUser patterns:\n${formatted}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (tenant.memory) {
|
|
273
|
+
const memories = query
|
|
274
|
+
? await tenant.memory.search(query, { limit: 5 }).catch(() => [])
|
|
275
|
+
: await tenant.memory.recent({ limit: 5 }).catch(() => []);
|
|
276
|
+
if (memories.length > 0) {
|
|
277
|
+
parts.push(`\nRecent memory:\n${memories.map(m => `- ${m.content}`).join('\n')}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return parts.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
72
284
|
async function runAgenticEvent(bot, config, event) {
|
|
73
285
|
const tenant = await getTenant(event.user_id, config);
|
|
286
|
+
const timezone = event.timezone || config.timezone || 'UTC';
|
|
287
|
+
|
|
288
|
+
const query = event.description || event.instructions;
|
|
289
|
+
const context = await buildProactiveContext(tenant, timezone, query).catch(() => '');
|
|
290
|
+
const instructions = context
|
|
291
|
+
? `[Context]\n${context}\n\n---\n\n${event.instructions}`
|
|
292
|
+
: event.instructions;
|
|
293
|
+
|
|
74
294
|
const fakeCtx = makeFakeCtx(bot, event.chat_id);
|
|
75
295
|
|
|
76
296
|
const taskId = tenant.bg.spawn(
|
|
77
297
|
tenant.claude,
|
|
78
|
-
|
|
298
|
+
instructions,
|
|
79
299
|
fakeCtx,
|
|
80
300
|
tenant.memory,
|
|
81
301
|
null,
|
|
82
|
-
{},
|
|
302
|
+
{ silent: true },
|
|
83
303
|
{
|
|
84
304
|
userId: event.user_id,
|
|
85
305
|
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 };
|
package/src/messages.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tier 1: obol_messages ā raw log, every message
|
|
5
5
|
* Tier 2: obol_memory ā curated facts, extracted after every assistant turn
|
|
6
|
+
* Tier 3: obol_events ā follow-up intents, detected by batch analysis (see analysis.js)
|
|
7
|
+
* Tier 4: obol_user_patterns ā synthesized behavioral patterns, refreshed by batch analysis (see analysis.js)
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
@@ -24,6 +26,7 @@ class MessageLog {
|
|
|
24
26
|
this.exchangeCount = new Map();
|
|
25
27
|
this._lastUserMessage = new Map();
|
|
26
28
|
this._verboseCallbacks = new Map();
|
|
29
|
+
this._lastActivity = new Map();
|
|
27
30
|
this._cleanup = setInterval(() => {
|
|
28
31
|
const now = Date.now();
|
|
29
32
|
for (const [key] of this.exchangeCount) {
|
|
@@ -35,7 +38,6 @@ class MessageLog {
|
|
|
35
38
|
}
|
|
36
39
|
}, 600000);
|
|
37
40
|
this._cleanup.unref();
|
|
38
|
-
this._lastActivity = new Map();
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
async log(chatId, role, content, opts = {}) {
|
|
@@ -71,17 +73,9 @@ class MessageLog {
|
|
|
71
73
|
if (lastUser) {
|
|
72
74
|
this._extractFacts(chatId, lastUser, truncated).catch(() => {});
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
const { checkEvolution } = require('./evolve');
|
|
76
|
-
checkEvolution(this.userDir, this).then(result => {
|
|
77
|
-
if (result?.ready && !this._evolutionReady && !this._evolutionPending) this._evolutionReady = true;
|
|
78
|
-
}).catch(() => {});
|
|
79
76
|
}
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/**
|
|
83
|
-
* Get recent messages for context loading on boot
|
|
84
|
-
*/
|
|
85
79
|
async getRecent(chatId, limit = 50) {
|
|
86
80
|
try {
|
|
87
81
|
const res = await fetch(
|
|
@@ -89,7 +83,7 @@ class MessageLog {
|
|
|
89
83
|
{ headers: this.headers }
|
|
90
84
|
);
|
|
91
85
|
const data = await res.json();
|
|
92
|
-
const rows = data.reverse();
|
|
86
|
+
const rows = data.reverse();
|
|
93
87
|
const firstUserIdx = rows.findIndex(r => r.role === 'user');
|
|
94
88
|
return firstUserIdx > 0 ? rows.slice(firstUserIdx) : rows;
|
|
95
89
|
} catch {
|
|
@@ -97,9 +91,20 @@ class MessageLog {
|
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
async getSince(chatId, since, limit = 500) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(
|
|
97
|
+
`${this.url}/rest/v1/obol_messages?chat_id=eq.${chatId}&created_at=gte.${since.toISOString()}&order=created_at.asc&limit=${limit}&select=role,content,created_at`,
|
|
98
|
+
{ headers: this.headers }
|
|
99
|
+
);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (!res.ok) return [];
|
|
102
|
+
return data;
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
async getByDate(chatId, dateStr, opts = {}) {
|
|
104
109
|
const { start, end } = parseDateRange(dateStr);
|
|
105
110
|
const limit = opts.limit || 50;
|
|
@@ -165,13 +170,12 @@ class MessageLog {
|
|
|
165
170
|
const response = await client.messages.create({
|
|
166
171
|
model: 'claude-haiku-4-5-20251001',
|
|
167
172
|
max_tokens: 1024,
|
|
168
|
-
system: `Extract
|
|
173
|
+
system: `Extract facts from this exchange.
|
|
169
174
|
|
|
175
|
+
FACTS (0-5 atomic facts worth remembering long-term):
|
|
170
176
|
Store: personal details, preferences, decisions, projects, plans, people mentioned, technical details, events, deadlines, emotional context, resources.
|
|
171
|
-
Skip: greetings, acknowledgments, content-free exchanges.
|
|
172
|
-
|
|
173
|
-
Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.
|
|
174
|
-
Keep each fact atomic ā one idea per entry.`,
|
|
177
|
+
Skip: greetings, acknowledgments, content-free exchanges.
|
|
178
|
+
Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
|
|
175
179
|
tools: extractTool,
|
|
176
180
|
tool_choice: { type: 'tool', name: 'save_memory' },
|
|
177
181
|
messages: [{ role: 'user', content: `Human: ${userMsg.substring(0, 2000)}\nAssistant: ${assistantMsg.substring(0, 2000)}` }],
|
|
@@ -181,38 +185,31 @@ Keep each fact atomic ā one idea per entry.`,
|
|
|
181
185
|
if (!toolUse) return;
|
|
182
186
|
|
|
183
187
|
const facts = toolUse.input?.facts;
|
|
184
|
-
if (!Array.isArray(facts)) return;
|
|
185
|
-
|
|
186
|
-
if (facts.length === 0) {
|
|
187
|
-
vlog?.('[extract] 0 facts (trivial exchange)');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
category,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
210
|
-
stored++;
|
|
211
|
-
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
212
|
-
}
|
|
189
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
190
|
+
const validCategories = new Set(['fact','preference','decision','lesson','person','project','event','conversation','resource','pattern','context','email']);
|
|
191
|
+
let stored = 0;
|
|
192
|
+
let duped = 0;
|
|
193
|
+
|
|
194
|
+
for (const fact of facts.slice(0, 5)) {
|
|
195
|
+
if (!fact.content || fact.content.length <= 10) continue;
|
|
196
|
+
try {
|
|
197
|
+
const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.92 });
|
|
198
|
+
if (existing.length > 0) { duped++; continue; }
|
|
199
|
+
} catch {}
|
|
200
|
+
const category = validCategories.has(fact.category) ? fact.category : 'fact';
|
|
201
|
+
const importance = typeof fact.importance === 'number' ? Math.min(1, Math.max(0, fact.importance)) : 0.5;
|
|
202
|
+
const tags = Array.isArray(fact.tags) ? fact.tags.slice(0, 5) : [];
|
|
203
|
+
await this.memory.add(fact.content, { category, tags, importance, source: 'turn-extraction' });
|
|
204
|
+
stored++;
|
|
205
|
+
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
206
|
+
}
|
|
213
207
|
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
if (stored > 0 || duped > 0) {
|
|
209
|
+
vlog?.(`[extract] ${stored} stored, ${duped} duped, ${facts.length} extracted`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
vlog?.('[extract] 0 facts (trivial exchange)');
|
|
216
213
|
}
|
|
217
214
|
} catch (e) {
|
|
218
215
|
console.error('[extract] Failed:', e.message);
|
package/src/patterns.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const VALID_DIMENSIONS = new Set(['timing', 'mood', 'humor', 'engagement', 'communication', 'topics']);
|
|
2
|
+
|
|
3
|
+
async function createPatterns(supabaseConfig, userId) {
|
|
4
|
+
const { url, serviceKey } = supabaseConfig;
|
|
5
|
+
|
|
6
|
+
const headers = {
|
|
7
|
+
'apikey': serviceKey,
|
|
8
|
+
'Authorization': `Bearer ${serviceKey}`,
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'Prefer': 'return=representation',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function upsert(key, dimension, summary, data = {}, confidence = 0.5) {
|
|
14
|
+
if (!VALID_DIMENSIONS.has(dimension)) throw new Error(`Invalid dimension: ${dimension}`);
|
|
15
|
+
|
|
16
|
+
const res = await fetch(`${url}/rest/v1/obol_user_patterns`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { ...headers, 'Prefer': 'resolution=merge-duplicates,return=representation' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
user_id: userId,
|
|
21
|
+
key,
|
|
22
|
+
dimension,
|
|
23
|
+
summary,
|
|
24
|
+
data,
|
|
25
|
+
confidence,
|
|
26
|
+
updated_at: new Date().toISOString(),
|
|
27
|
+
observation_count: 1,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const result = await res.json();
|
|
31
|
+
if (!res.ok) throw new Error(JSON.stringify(result));
|
|
32
|
+
return result[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function incrementObservation(key, dimension, summary, data = {}, confidence = 0.5) {
|
|
36
|
+
const existing = await get(key);
|
|
37
|
+
const count = (existing?.observation_count || 0) + 1;
|
|
38
|
+
|
|
39
|
+
const res = await fetch(`${url}/rest/v1/obol_user_patterns`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { ...headers, 'Prefer': 'resolution=merge-duplicates,return=representation' },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
user_id: userId,
|
|
44
|
+
key,
|
|
45
|
+
dimension,
|
|
46
|
+
summary,
|
|
47
|
+
data,
|
|
48
|
+
confidence,
|
|
49
|
+
observation_count: count,
|
|
50
|
+
updated_at: new Date().toISOString(),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
const result = await res.json();
|
|
54
|
+
if (!res.ok) throw new Error(JSON.stringify(result));
|
|
55
|
+
return result[0];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function get(key) {
|
|
59
|
+
const res = await fetch(
|
|
60
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&key=eq.${encodeURIComponent(key)}&limit=1`,
|
|
61
|
+
{ headers }
|
|
62
|
+
);
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
65
|
+
return data[0] || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getAll() {
|
|
69
|
+
const res = await fetch(
|
|
70
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&order=dimension.asc,updated_at.desc`,
|
|
71
|
+
{ headers }
|
|
72
|
+
);
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getByDimension(dimension) {
|
|
79
|
+
const res = await fetch(
|
|
80
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&dimension=eq.${dimension}&order=updated_at.desc`,
|
|
81
|
+
{ headers }
|
|
82
|
+
);
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function remove(key) {
|
|
89
|
+
await fetch(
|
|
90
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&key=eq.${encodeURIComponent(key)}`,
|
|
91
|
+
{ method: 'DELETE', headers: { ...headers, 'Prefer': 'return=minimal' } }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function format() {
|
|
96
|
+
const all = await getAll();
|
|
97
|
+
if (!all.length) return null;
|
|
98
|
+
const byDimension = {};
|
|
99
|
+
for (const p of all) {
|
|
100
|
+
if (!byDimension[p.dimension]) byDimension[p.dimension] = [];
|
|
101
|
+
byDimension[p.dimension].push(p.summary);
|
|
102
|
+
}
|
|
103
|
+
return Object.entries(byDimension)
|
|
104
|
+
.map(([dim, summaries]) => `[${dim}]\n${summaries.map(s => `- ${s}`).join('\n')}`)
|
|
105
|
+
.join('\n\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { upsert, incrementObservation, get, getAll, getByDimension, remove, format };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { createPatterns };
|