obol-ai 0.3.23 → 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.23",
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": {
@@ -31,7 +31,20 @@ class BackgroundRunner {
31
31
  const verbose = parentContext?.verbose || false;
32
32
  const verboseNotify = parentContext?._verboseNotify;
33
33
 
34
- 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);
35
48
  taskState.promise = promise;
36
49
 
37
50
  return taskId;
@@ -94,18 +107,25 @@ TASK: ${task}`;
94
107
  },
95
108
  });
96
109
 
97
- taskState.status = 'done';
98
- taskState.result = result;
99
110
  claude.clearHistory(`bg-${taskState.id}`);
100
-
101
111
  clearStatus();
102
112
 
103
- const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
104
- if (silent) {
105
- 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
+ }
106
119
  } else {
107
- const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
108
- 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
+ }
109
129
  }
110
130
 
111
131
  if (memory) {
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 };
@@ -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
 
@@ -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();