obol-ai 0.3.22 → 0.3.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
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": {
@@ -188,6 +188,7 @@ async function manageUsers(cfg, saveConfig) {
188
188
  if (removeId !== null) {
189
189
  const idx = currentUsers.indexOf(removeId);
190
190
  if (idx !== -1) currentUsers.splice(idx, 1);
191
+ if (cfg.users) delete cfg.users[String(removeId)];
191
192
  console.log(` ✅ Removed ${removeId}`);
192
193
  console.log(` ⚠️ Workspace at ${getUserDir(removeId)} was NOT deleted (remove manually if needed)`);
193
194
  }
@@ -1,4 +1,5 @@
1
1
  const { buildStatusHtml, formatToolCall } = require('../status');
2
+ const { markdownToTelegramHtml } = require('../telegram/utils');
2
3
 
3
4
  const MAX_CONCURRENT_TASKS = 3;
4
5
 
@@ -30,7 +31,20 @@ class BackgroundRunner {
30
31
  const verbose = parentContext?.verbose || false;
31
32
  const verboseNotify = parentContext?._verboseNotify;
32
33
 
33
- const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, opts.extraContext || extraContext, opts.silent || false);
34
+ const inherited = parentContext ? {
35
+ toolPrefs: parentContext.toolPrefs,
36
+ config: parentContext.config,
37
+ scheduler: parentContext.scheduler,
38
+ messageLog: parentContext.messageLog,
39
+ userId: parentContext.userId,
40
+ userDir: parentContext.userDir,
41
+ telegramAsk: parentContext.telegramAsk,
42
+ _notifyFn: parentContext._notifyFn,
43
+ } : {};
44
+
45
+ const mergedExtra = { ...inherited, ...(opts.extraContext || extraContext) };
46
+
47
+ const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, mergedExtra, opts.silent || false);
34
48
  taskState.promise = promise;
35
49
 
36
50
  return taskId;
@@ -93,18 +107,25 @@ TASK: ${task}`;
93
107
  },
94
108
  });
95
109
 
96
- taskState.status = 'done';
97
- taskState.result = result;
98
110
  claude.clearHistory(`bg-${taskState.id}`);
99
-
100
111
  clearStatus();
101
112
 
102
- const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
103
- if (silent) {
104
- await sendLong(ctx, result);
113
+ if (!result?.trim()) {
114
+ taskState.status = 'error';
115
+ taskState.error = 'No result returned';
116
+ if (!silent) {
117
+ await ctx.reply(`⚠️ BG #${taskState.id} finished but produced no result.`).catch(() => {});
118
+ }
105
119
  } else {
106
- const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
107
- await sendLong(ctx, header + result);
120
+ taskState.status = 'done';
121
+ taskState.result = result;
122
+ const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
123
+ if (silent) {
124
+ await sendLong(ctx, result);
125
+ } else {
126
+ const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
127
+ await sendLong(ctx, header + result);
128
+ }
108
129
  }
109
130
 
110
131
  if (memory) {
@@ -160,27 +181,22 @@ function formatDuration(seconds) {
160
181
 
161
182
  async function sendLong(ctx, text) {
162
183
  if (!text?.trim()) return;
163
- if (text.length <= 4096) {
164
- await ctx.reply(text, { parse_mode: 'HTML' }).catch(() =>
165
- ctx.reply(text)
166
- );
184
+ const html = markdownToTelegramHtml(text);
185
+ if (html.length <= 4096) {
186
+ await ctx.reply(html, { parse_mode: 'HTML' }).catch(() => ctx.reply(text));
167
187
  return;
168
188
  }
169
189
 
170
- let remaining = text;
190
+ let remaining = html;
171
191
  while (remaining.length > 0) {
172
192
  if (remaining.length <= 4096) {
173
- await ctx.reply(remaining, { parse_mode: 'HTML' }).catch(() =>
174
- ctx.reply(remaining)
175
- );
193
+ await ctx.reply(remaining, { parse_mode: 'HTML' }).catch(() => ctx.reply(remaining));
176
194
  break;
177
195
  }
178
196
  let splitAt = remaining.lastIndexOf('\n', 4096);
179
197
  if (splitAt === -1 || splitAt < 2000) splitAt = 4096;
180
198
  const chunk = remaining.substring(0, splitAt);
181
- await ctx.reply(chunk, { parse_mode: 'HTML' }).catch(() =>
182
- ctx.reply(chunk)
183
- );
199
+ await ctx.reply(chunk, { parse_mode: 'HTML' }).catch(() => ctx.reply(chunk));
184
200
  remaining = remaining.substring(splitAt).trimStart();
185
201
  }
186
202
  }
package/src/status.js CHANGED
@@ -33,4 +33,19 @@ function formatToolCall(toolName, inputSummary) {
33
33
  return `${toolName} "${truncated}"`;
34
34
  }
35
35
 
36
- module.exports = { buildStatusHtml, formatToolCall, TERM_WIDTH };
36
+ /**
37
+ * @param {{ model: string, usage: { input_tokens: number, output_tokens: number }, startTime: number | null }} params
38
+ * @returns {string | null}
39
+ */
40
+ function formatTokenStats({ model, usage, startTime }) {
41
+ if (!usage || !model) return null;
42
+ const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
43
+ const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
44
+ const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
45
+ const dur = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : null;
46
+ const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
47
+ if (dur) parts.push(`${dur}s`);
48
+ return `<code>${parts.join(' ▪ ')}</code>`;
49
+ }
50
+
51
+ module.exports = { buildStatusHtml, formatToolCall, formatTokenStats, TERM_WIDTH };
@@ -103,6 +103,7 @@ function createBot(telegramConfig, config) {
103
103
  { command: 'options', description: 'Toggle optional features on/off' },
104
104
  { command: 'topics', description: 'Edit news topics' },
105
105
  { command: 'stop', description: 'Stop the current request' },
106
+ { command: 'restart', description: 'Restart the bot (pm2)' },
106
107
  { command: 'upgrade', description: 'Check for updates and upgrade' },
107
108
  { command: 'help', description: 'Show available commands' },
108
109
  ]).catch(() => {});
@@ -168,6 +168,16 @@ Summarize what was cleaned and secrets migrated.`);
168
168
  }
169
169
  });
170
170
 
171
+ bot.command('restart', async (ctx) => {
172
+ if (!ctx.from) return;
173
+ await ctx.reply('🔄 Restarting...');
174
+ try {
175
+ execSync('pm2 restart obol', { encoding: 'utf-8', timeout: 15000 });
176
+ } catch (e) {
177
+ await ctx.reply(`⚠️ Restart failed: ${e.message.substring(0, 200)}`);
178
+ }
179
+ });
180
+
171
181
  bot.command('toolimit', async (ctx) => {
172
182
  if (!ctx.from) return;
173
183
  const args = ctx.message.text.split(' ').slice(1);
@@ -54,6 +54,7 @@ function register(bot, config) {
54
54
  /verbose — Toggle verbose mode on/off
55
55
  /toolimit — View or set max tool iterations
56
56
  /stop — Stop the current request
57
+ /restart — Restart the bot
57
58
  /upgrade — Check for updates and upgrade
58
59
  /help — This message`);
59
60
  });
@@ -1,6 +1,6 @@
1
1
  const path = require('path');
2
2
  const { getTenant } = require('../../tenant');
3
- const { buildStatusHtml, formatToolCall } = require('../../status');
3
+ const { buildStatusHtml, formatToolCall, formatTokenStats } = require('../../status');
4
4
  const media = require('../../media');
5
5
  const { sendHtml, startTyping, splitMessage } = require('../utils');
6
6
  const { MAX_MEDIA_SIZE, MEDIA_GROUP_DELAY_MS } = require('../constants');
@@ -124,17 +124,8 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
124
124
 
125
125
  const statsPref = tenant.toolPrefs?.get('model_stats');
126
126
  const showStats = statsPref ? statsPref.enabled : true;
127
- if (showStats && usage && model) {
128
- const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
129
- const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
130
- const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
131
- const dur = status.statusStart ? ((Date.now() - status.statusStart)/1000).toFixed(1) : null;
132
- const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
133
- if (dur) parts.push(`${dur}s`);
134
- await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
135
- }
136
-
137
- status.deleteMsg();
127
+ const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
128
+ status.finalize(statsHtml);
138
129
  } catch (e) {
139
130
  status.clear();
140
131
  stopTyping();
@@ -1,5 +1,5 @@
1
1
  const { getTenant } = require('../../tenant');
2
- const { formatToolCall } = require('../../status');
2
+ const { formatToolCall, formatTokenStats } = require('../../status');
3
3
  const { sendHtml, startTyping, splitMessage } = require('../utils');
4
4
  const { createChatContext, createStatusTracker } = require('./text');
5
5
 
@@ -120,17 +120,8 @@ async function processSpecial(ctx, prompt, deps) {
120
120
 
121
121
  const statsPref = tenant.toolPrefs?.get('model_stats');
122
122
  const showStats = statsPref ? statsPref.enabled : true;
123
- if (showStats && usage && model) {
124
- const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
125
- const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
126
- const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
127
- const dur = status.statusStart ? ((Date.now() - status.statusStart) / 1000).toFixed(1) : null;
128
- const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
129
- if (dur) parts.push(`${dur}s`);
130
- await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
131
- }
132
-
133
- status.deleteMsg();
123
+ const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
124
+ status.finalize(statsHtml);
134
125
  } catch (e) {
135
126
  status.clear();
136
127
  stopTyping();
@@ -1,6 +1,6 @@
1
1
  const { InlineKeyboard } = require('grammy');
2
2
  const { getTenant } = require('../../tenant');
3
- const { buildStatusHtml, formatToolCall } = require('../../status');
3
+ const { buildStatusHtml, formatToolCall, formatTokenStats } = require('../../status');
4
4
  const { sendHtml, startTyping, splitMessage } = require('../utils');
5
5
  const { TEXT_BUFFER_GAP_MS, TEXT_BUFFER_MAX_PARTS, TEXT_BUFFER_MAX_CHARS, TEXT_BUFFER_THRESHOLD } = require('../constants');
6
6
 
@@ -10,7 +10,7 @@ const VERBOSE_FLUSH_MS = 2000;
10
10
  async function sendTtsVoiceSummary(ctx, tenant, responseText) {
11
11
  const fs = require('fs');
12
12
  const { InputFile } = require('grammy');
13
- const tts = require('../../tts');
13
+ const tts = require('../../media/tts');
14
14
 
15
15
  const ttsConfig = tenant.toolPrefs.get('text_to_speech')?.config || {};
16
16
  const voice = ttsConfig.voice || 'en-US-JennyNeural';
@@ -142,6 +142,18 @@ function createStatusTracker(ctx, botName) {
142
142
  deleteMsg() {
143
143
  if (statusMsgId) ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {});
144
144
  },
145
+ finalize(statsHtml) {
146
+ if (statusTimer) { clearInterval(statusTimer); statusTimer = null; }
147
+ if (statsHtml && statusMsgId) {
148
+ ctx.api.editMessageText(ctx.chat.id, statusMsgId, statsHtml, { parse_mode: 'HTML' }).catch(() => {
149
+ ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {});
150
+ });
151
+ statusMsgId = null;
152
+ } else {
153
+ if (statusMsgId) { ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {}); statusMsgId = null; }
154
+ if (statsHtml) ctx.reply(statsHtml, { parse_mode: 'HTML' }).catch(() => {});
155
+ }
156
+ },
145
157
  };
146
158
  }
147
159
 
@@ -233,17 +245,8 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
233
245
 
234
246
  const statsPref = tenant.toolPrefs?.get('model_stats');
235
247
  const showStats = statsPref ? statsPref.enabled : true;
236
- if (showStats && usage && model) {
237
- const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
238
- const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
239
- const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
240
- const dur = status.statusStart ? ((Date.now() - status.statusStart)/1000).toFixed(1) : null;
241
- const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
242
- if (dur) parts.push(`${dur}s`);
243
- await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
244
- }
245
-
246
- status.deleteMsg();
248
+ const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
249
+ status.finalize(statsHtml);
247
250
  } catch (e) {
248
251
  batcher?.flush();
249
252
  status.clear();