obol-ai 0.3.46 → 0.3.48
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 +6 -0
- package/package.json +1 -1
- package/src/claude/router.js +19 -17
- package/src/runtime/background.js +2 -1
- package/src/runtime/heartbeat.js +49 -32
- package/src/telegram/handlers/text.js +1 -1
- package/src/ws/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.3.47
|
|
2
|
+
- fix elapsed scope bug in background task completion
|
|
3
|
+
- fix TTS summary to speak in first person
|
|
4
|
+
- inject proactive follow-up messages into conversation history
|
|
5
|
+
- fix OAuth token expiry in heartbeat scheduled events
|
|
6
|
+
|
|
1
7
|
## 0.3.44
|
|
2
8
|
- exclude electron from npm package
|
|
3
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.48",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude/router.js
CHANGED
|
@@ -14,6 +14,18 @@ function buildRouterMessages(recentHistory, userMessage) {
|
|
|
14
14
|
return [...trimmed, { role: 'user', content: userMessage }];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function extractRecentUserMessages(recentHistory, userMessage, count = 5) {
|
|
18
|
+
const userMsgs = recentHistory
|
|
19
|
+
.filter(m => m.role === 'user')
|
|
20
|
+
.map(m => typeof m.content === 'string'
|
|
21
|
+
? m.content
|
|
22
|
+
: m.content.filter(b => b.type === 'text').map(b => b.text).join(''))
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.slice(-count);
|
|
25
|
+
userMsgs.push(userMessage);
|
|
26
|
+
return userMsgs;
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
function tokenize(s) {
|
|
18
30
|
return new Set(s.toLowerCase().split(/\W+/).filter(Boolean));
|
|
19
31
|
}
|
|
@@ -38,9 +50,7 @@ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision
|
|
|
38
50
|
2. What model complexity does it need?
|
|
39
51
|
|
|
40
52
|
Reply with ONLY a JSON object:
|
|
41
|
-
{"need_memory": true/false, "
|
|
42
|
-
|
|
43
|
-
search_queries: 1-5 optimized search queries based on the full conversation context. Cover distinct topics, people, entities, time periods, or projects referenced. Single-topic messages need just one query. Use more queries when the message references multiple people, projects, or threads.
|
|
53
|
+
{"need_memory": true/false, "model": "sonnet|opus"}
|
|
44
54
|
|
|
45
55
|
Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true.
|
|
46
56
|
|
|
@@ -57,15 +67,11 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
57
67
|
if (jsonStr) decision = JSON.parse(jsonStr);
|
|
58
68
|
} catch {}
|
|
59
69
|
|
|
60
|
-
const queries = Array.isArray(decision.search_queries) && decision.search_queries.length > 0
|
|
61
|
-
? decision.search_queries.slice(0, 3)
|
|
62
|
-
: decision.search_query ? [decision.search_query] : [];
|
|
63
|
-
|
|
64
70
|
if (decision.model !== 'sonnet' && decision.model !== 'opus') {
|
|
65
71
|
decision.model = 'sonnet';
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
vlog(`[router] model=${decision.model} memory=${decision.need_memory || false}
|
|
74
|
+
vlog(`[router] model=${decision.model} memory=${decision.need_memory || false}`);
|
|
69
75
|
|
|
70
76
|
onRouteDecision?.({
|
|
71
77
|
model: decision.model,
|
|
@@ -80,23 +86,19 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
80
86
|
if (decision.need_memory && memory) {
|
|
81
87
|
const budget = decision.model === 'opus' ? 60 : 40;
|
|
82
88
|
const poolPerQuery = decision.model === 'opus' ? 25 : 20;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const recentMemories = await memory.byDate('7d', { limit: Math.ceil(budget / 3) });
|
|
89
|
+
const conversationQueries = extractRecentUserMessages(recentHistory, userMessage);
|
|
86
90
|
|
|
87
91
|
const semanticResults = await Promise.all(
|
|
88
|
-
|
|
92
|
+
conversationQueries.map(q => memory.search(q, { limit: poolPerQuery, threshold: 0.4 }))
|
|
89
93
|
);
|
|
90
94
|
const semanticMemories = semanticResults.flat();
|
|
91
95
|
|
|
92
96
|
const seen = new Set();
|
|
93
97
|
const combined = [];
|
|
94
|
-
for (const m of
|
|
98
|
+
for (const m of semanticMemories) {
|
|
95
99
|
if (!seen.has(m.id)) {
|
|
96
100
|
seen.add(m.id);
|
|
97
|
-
|
|
98
|
-
const recencyBonus = Math.max(0, 1 - ageDays / 7) * 0.3;
|
|
99
|
-
m._score = (m.similarity || 0.5) * 0.5 + (m.importance || 0.5) * 0.2 + recencyBonus;
|
|
101
|
+
m._score = (m.similarity || 0.5) * 0.7 + (m.importance || 0.5) * 0.3;
|
|
100
102
|
combined.push(m);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
@@ -112,7 +114,7 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
112
114
|
if (!isDup) topFacts.push(m);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
vlog(`[memory] ${topFacts.length} facts
|
|
117
|
+
vlog(`[memory] ${topFacts.length} facts from ${conversationQueries.length} conversation queries (${semanticMemories.length} candidates, budget=${budget})`);
|
|
116
118
|
onRouteUpdate?.({ memoryCount: topFacts.length });
|
|
117
119
|
|
|
118
120
|
memoryBlock = formatMemoryBlock(topFacts);
|
|
@@ -110,6 +110,8 @@ TASK: ${task}`;
|
|
|
110
110
|
claude.clearHistory(`bg-${taskState.id}`);
|
|
111
111
|
clearStatus();
|
|
112
112
|
|
|
113
|
+
const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
|
|
114
|
+
|
|
113
115
|
if (!result?.trim()) {
|
|
114
116
|
taskState.status = 'error';
|
|
115
117
|
taskState.error = 'No result returned';
|
|
@@ -119,7 +121,6 @@ TASK: ${task}`;
|
|
|
119
121
|
} else {
|
|
120
122
|
taskState.status = 'done';
|
|
121
123
|
taskState.result = result;
|
|
122
|
-
const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
|
|
123
124
|
if (silent) {
|
|
124
125
|
await sendLong(ctx, result);
|
|
125
126
|
} else {
|
package/src/runtime/heartbeat.js
CHANGED
|
@@ -7,6 +7,7 @@ const { runAnalysis } = require('../analysis');
|
|
|
7
7
|
const { runProactiveNews } = require('../news');
|
|
8
8
|
const { createSelfMemory } = require('../memory/self');
|
|
9
9
|
const { createAnthropicClient, ensureFreshToken } = require('../claude/client');
|
|
10
|
+
const { markdownToTelegramHtml } = require('../telegram/utils');
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
const ANALYSIS_HOURS = new Set([4, 7, 10, 13, 16, 19, 22]);
|
|
@@ -145,7 +146,6 @@ async function runNewsForUser(bot, config, userId) {
|
|
|
145
146
|
|
|
146
147
|
const MAX_ATTEMPTS = 3;
|
|
147
148
|
const STALE_MS = 2 * 60 * 60 * 1000;
|
|
148
|
-
const EVENT_TIMEOUT_MS = 30_000;
|
|
149
149
|
|
|
150
150
|
async function processEvent(bot, config, scheduler, event) {
|
|
151
151
|
const tz = event.timezone || 'UTC';
|
|
@@ -320,40 +320,30 @@ async function runAgenticEvent(bot, config, event) {
|
|
|
320
320
|
const timezone = event.timezone || getUserTimezone(config, event.user_id);
|
|
321
321
|
|
|
322
322
|
const query = event.description || event.instructions;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
323
|
+
const proactiveContext = await buildProactiveContext(tenant, timezone, query).catch(() => '');
|
|
324
|
+
|
|
325
|
+
const contextPrefix = proactiveContext
|
|
326
|
+
? `[Scheduled task context — ${timezone}]\n${proactiveContext}\n\n`
|
|
327
|
+
: '';
|
|
328
|
+
const prompt = `${contextPrefix}Scheduled task "${event.title}":\n${event.instructions}`;
|
|
329
|
+
|
|
330
|
+
const chatId = `scheduled-${event.id}-${Date.now()}`;
|
|
331
|
+
const { text } = await tenant.claude.chat(prompt, {
|
|
332
|
+
chatId,
|
|
333
|
+
userName: 'ScheduledTask',
|
|
334
|
+
userId: event.user_id,
|
|
335
|
+
userDir: tenant.userDir,
|
|
336
|
+
toolPrefs: tenant.toolPrefs,
|
|
337
|
+
config,
|
|
338
|
+
scheduler: tenant.scheduler,
|
|
339
|
+
messageLog: tenant.messageLog,
|
|
340
|
+
});
|
|
338
341
|
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
response = await client.messages.create({
|
|
342
|
-
model: 'claude-sonnet-4-6',
|
|
343
|
-
max_tokens: 300,
|
|
344
|
-
system: systemParts.join('\n\n'),
|
|
345
|
-
messages: [{ role: 'user', content: event.instructions }],
|
|
346
|
-
}, { signal: controller.signal });
|
|
347
|
-
} finally {
|
|
348
|
-
clearTimeout(timeout);
|
|
349
|
-
}
|
|
342
|
+
tenant.claude.clearHistory(chatId);
|
|
350
343
|
|
|
351
|
-
|
|
352
|
-
if (!text) throw new Error('Empty response from model');
|
|
344
|
+
if (!text?.trim()) throw new Error('Empty response from model');
|
|
353
345
|
|
|
354
|
-
await bot
|
|
355
|
-
bot.api.sendMessage(event.chat_id, text, { parse_mode: undefined })
|
|
356
|
-
);
|
|
346
|
+
await sendScheduledMessage(bot, event.chat_id, text);
|
|
357
347
|
|
|
358
348
|
tenant.claude.injectHistory(event.chat_id, 'assistant', text);
|
|
359
349
|
if (tenant.messageLog) {
|
|
@@ -361,4 +351,31 @@ async function runAgenticEvent(bot, config, event) {
|
|
|
361
351
|
}
|
|
362
352
|
}
|
|
363
353
|
|
|
354
|
+
async function sendScheduledMessage(bot, chatId, text) {
|
|
355
|
+
const html = markdownToTelegramHtml(text);
|
|
356
|
+
if (html.length <= 4096) {
|
|
357
|
+
await bot.api.sendMessage(chatId, html, { parse_mode: 'HTML' }).catch(() =>
|
|
358
|
+
bot.api.sendMessage(chatId, text)
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let remaining = html;
|
|
364
|
+
while (remaining.length > 0) {
|
|
365
|
+
if (remaining.length <= 4096) {
|
|
366
|
+
await bot.api.sendMessage(chatId, remaining, { parse_mode: 'HTML' }).catch(() =>
|
|
367
|
+
bot.api.sendMessage(chatId, remaining)
|
|
368
|
+
);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
let splitAt = remaining.lastIndexOf('\n', 4096);
|
|
372
|
+
if (splitAt === -1 || splitAt < 2000) splitAt = 4096;
|
|
373
|
+
const chunk = remaining.substring(0, splitAt);
|
|
374
|
+
await bot.api.sendMessage(chatId, chunk, { parse_mode: 'HTML' }).catch(() =>
|
|
375
|
+
bot.api.sendMessage(chatId, chunk)
|
|
376
|
+
);
|
|
377
|
+
remaining = remaining.substring(splitAt).trimStart();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
364
381
|
module.exports = { setupHeartbeat };
|
|
@@ -20,7 +20,7 @@ async function sendTtsVoiceSummary(ctx, tenant, responseText) {
|
|
|
20
20
|
max_tokens: 200,
|
|
21
21
|
messages: [{
|
|
22
22
|
role: 'user',
|
|
23
|
-
content: `Summarize the following
|
|
23
|
+
content: `Summarize the following message in 1-2 short spoken sentences. Write in first person as if YOU are speaking directly to the user — say "I" not "the assistant". Use plain conversational language — no markdown, no code, no lists:\n\n${responseText.substring(0, 3000)}`,
|
|
24
24
|
}],
|
|
25
25
|
});
|
|
26
26
|
|
package/src/ws/server.js
CHANGED
|
@@ -466,7 +466,7 @@ async function generateTtsAudio(ws, tenant, responseText) {
|
|
|
466
466
|
max_tokens: 200,
|
|
467
467
|
messages: [{
|
|
468
468
|
role: 'user',
|
|
469
|
-
content: `Summarize the following
|
|
469
|
+
content: `Summarize the following message in 1-2 short spoken sentences. Write in first person as if YOU are speaking directly to the user \u2014 say "I" not "the assistant". Use plain conversational language \u2014 no markdown, no code, no lists:\n\n${responseText.substring(0, 3000)}`,
|
|
470
470
|
}],
|
|
471
471
|
});
|
|
472
472
|
|