obol-ai 0.3.35 → 0.3.37

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 CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.3.37
2
+ - changelog
3
+ - fix media group race condition — buffer before download
4
+
5
+ ## 0.3.36
6
+ - save media assets instantly in date-based folders
7
+ - fix agentic event raw JSON output and update_event wiping instructions
8
+
1
9
  ## 0.3.35
2
10
  - add order param to memory, self-memory, and events queries
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.35",
3
+ "version": "0.3.37",
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": {
@@ -59,7 +59,8 @@ Always search memory first for the user's timezone/location.`,
59
59
  event_id: { type: 'string', description: 'UUID of the event to update' },
60
60
  title: { type: 'string' },
61
61
  description: { type: 'string' },
62
- instructions: { type: 'string', description: 'LLM instructions to execute when the event fires. Set to empty string to clear.' },
62
+ instructions: { type: 'string', description: 'New LLM instructions to execute when the event fires. Only provide if the user wants to change them.' },
63
+ clear_instructions: { type: 'boolean', description: 'Set to true to remove instructions and convert an agentic event to a plain reminder.' },
63
64
  timezone: { type: 'string' },
64
65
  cron_expr: { type: 'string' },
65
66
  max_runs: { type: 'number' },
@@ -146,7 +147,8 @@ const handlers = {
146
147
  const fields = {};
147
148
  if (rest.title !== undefined) fields.title = rest.title;
148
149
  if (rest.description !== undefined) fields.description = rest.description;
149
- if (typeof rest.instructions === 'string') fields.instructions = rest.instructions || null;
150
+ if (rest.clear_instructions === true) fields.instructions = null;
151
+ else if (typeof rest.instructions === 'string' && rest.instructions.length > 0) fields.instructions = rest.instructions;
150
152
  if (rest.timezone !== undefined) fields.timezone = rest.timezone;
151
153
  if (rest.cron_expr !== undefined) fields.cron_expr = rest.cron_expr;
152
154
  if (rest.max_runs !== undefined) fields.max_runs = rest.max_runs;
@@ -363,6 +363,10 @@ async function runAgenticEvent(bot, config, event) {
363
363
  if (tenant.personality?.soul) systemParts.push(tenant.personality.soul);
364
364
  if (tenant.personality?.user) systemParts.push(`About this user:\n${tenant.personality.user}`);
365
365
  if (context) systemParts.push(`Current context:\n${context}`);
366
+ systemParts.push(
367
+ 'You are executing a scheduled task. Respond ONLY with a clean, natural-language message for the user. ' +
368
+ 'Do NOT output JSON, code blocks, tool calls, or structured data. Write as a direct Telegram message.'
369
+ );
366
370
 
367
371
  const response = await tenant.claude.client.messages.create({
368
372
  model: 'claude-sonnet-4-6',
@@ -9,7 +9,7 @@ const TEXT_BUFFER_GAP_MS = 1500;
9
9
  const TEXT_BUFFER_MAX_PARTS = 12;
10
10
  const TEXT_BUFFER_MAX_CHARS = 50000;
11
11
  const TEXT_BUFFER_THRESHOLD = 4000;
12
- const MEDIA_GROUP_DELAY_MS = 500;
12
+ const MEDIA_GROUP_DELAY_MS = 5000;
13
13
  const MAX_MEDIA_SIZE = 50 * 1024 * 1024;
14
14
  const TERM_SEP = '━'.repeat(TERM_WIDTH);
15
15
 
@@ -1,5 +1,6 @@
1
1
  const path = require('path');
2
2
  const { getTenant } = require('../../tenant');
3
+ const { ensureUserDir, getUserTimezone } = require('../../config');
3
4
  const { buildStatusHtml, formatToolCall, formatTokenStats } = require('../../status');
4
5
  const media = require('../../media');
5
6
  const { sendHtml, startTyping, splitMessage } = require('../utils');
@@ -15,6 +16,29 @@ async function downloadMediaItem(ctx, fileInfo, telegramToken) {
15
16
  return { buffer, filename, fileInfo, caption: ctx.message.caption || '' };
16
17
  }
17
18
 
19
+ async function downloadAndProcess(ctx, entries, deps, token) {
20
+ const userId = ctx.from.id;
21
+ const userDir = ensureUserDir(userId);
22
+ const tz = getUserTimezone(deps.config, userId);
23
+ const today = new Date().toLocaleDateString('en-CA', { timeZone: tz });
24
+ const assetsDir = path.join(userDir, 'assets', today);
25
+
26
+ const items = (await Promise.all(
27
+ entries.map(({ ctx: entryCtx, fileInfo }) =>
28
+ downloadMediaItem(entryCtx, fileInfo, token).then(item => {
29
+ if (item) item.savedPath = media.saveFile(item.buffer, assetsDir, item.filename);
30
+ return item;
31
+ }).catch(e => {
32
+ console.error('Media download error:', e.message);
33
+ return null;
34
+ })
35
+ )
36
+ )).filter(Boolean);
37
+
38
+ if (items.length === 0) return;
39
+ await processMediaItems(ctx, items, deps);
40
+ }
41
+
18
42
  async function processMediaItems(ctx, items, { config, allowedUsers, bot, createAsk }) {
19
43
  if (!ctx.from) return;
20
44
  const userId = ctx.from.id;
@@ -23,13 +47,12 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
23
47
 
24
48
  try {
25
49
  const tenant = await getTenant(userId, config);
26
- const assetsDir = path.join(tenant.userDir, 'assets');
27
50
  const imageBlocks = [];
28
51
  const nonImageParts = [];
29
52
  const caption = items.map(i => i.caption).filter(Boolean).join('\n') || '';
30
53
 
31
54
  for (const item of items) {
32
- const savedPath = media.saveFile(item.buffer, assetsDir, item.filename);
55
+ const savedPath = item.savedPath;
33
56
 
34
57
  if (tenant.memory && !media.isImage(item.fileInfo)) {
35
58
  const memContent = media.buildMemoryContent(item.fileInfo, item.filename, savedPath, item.caption);
@@ -57,6 +80,9 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
57
80
  }
58
81
  }
59
82
 
83
+ const fileList = items.map(i => `- ${i.savedPath}`).join('\n');
84
+ const fileRef = `Files saved:\n${fileList}`;
85
+
60
86
  let prompt, chatImages;
61
87
  if (imageBlocks.length > 0) {
62
88
  prompt = caption || `The user sent ${imageBlocks.length} image(s). Describe what you see and respond naturally.`;
@@ -65,6 +91,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
65
91
  } else {
66
92
  prompt = nonImageParts.join('\n');
67
93
  }
94
+ prompt = fileRef + '\n\n' + prompt;
68
95
 
69
96
  const mediaChatCtx = createChatContext(ctx, tenant, config, { allowedUsers, bot, createAsk });
70
97
  if (chatImages) mediaChatCtx.images = chatImages;
@@ -150,32 +177,26 @@ function registerMediaHandler(bot, telegramConfig, deps) {
150
177
  return;
151
178
  }
152
179
 
153
- const item = await downloadMediaItem(ctx, fileInfo, telegramConfig.token).catch(e => {
154
- console.error('Media download error:', e.message);
155
- return null;
156
- });
157
- if (!item) return;
158
-
159
180
  const groupId = ctx.message.media_group_id;
160
181
  if (groupId) {
161
182
  const existing = mediaGroups.get(groupId);
162
183
  if (existing) {
163
184
  clearTimeout(existing.timer);
164
- existing.items.push(item);
165
- existing.ctx = ctx;
185
+ existing.entries.push({ ctx, fileInfo });
186
+ existing.latestCtx = ctx;
166
187
  existing.timer = setTimeout(() => {
167
188
  mediaGroups.delete(groupId);
168
- processMediaItems(existing.ctx, existing.items, deps).catch(e =>
189
+ downloadAndProcess(existing.latestCtx, existing.entries, deps, telegramConfig.token).catch(e =>
169
190
  console.error('Media group error:', e.message)
170
191
  );
171
192
  }, MEDIA_GROUP_DELAY_MS);
172
193
  } else {
173
194
  const group = {
174
- items: [item],
175
- ctx,
195
+ entries: [{ ctx, fileInfo }],
196
+ latestCtx: ctx,
176
197
  timer: setTimeout(() => {
177
198
  mediaGroups.delete(groupId);
178
- processMediaItems(ctx, [item], deps).catch(e =>
199
+ downloadAndProcess(ctx, [{ ctx, fileInfo }], deps, telegramConfig.token).catch(e =>
179
200
  console.error('Media group error:', e.message)
180
201
  );
181
202
  }, MEDIA_GROUP_DELAY_MS),
@@ -185,6 +206,18 @@ function registerMediaHandler(bot, telegramConfig, deps) {
185
206
  return;
186
207
  }
187
208
 
209
+ const item = await downloadMediaItem(ctx, fileInfo, telegramConfig.token).catch(e => {
210
+ console.error('Media download error:', e.message);
211
+ return null;
212
+ });
213
+ if (!item) return;
214
+
215
+ const userDir = ensureUserDir(userId);
216
+ const tz = getUserTimezone(deps.config, userId);
217
+ const today = new Date().toLocaleDateString('en-CA', { timeZone: tz });
218
+ const assetsDir = path.join(userDir, 'assets', today);
219
+ item.savedPath = media.saveFile(item.buffer, assetsDir, item.filename);
220
+
188
221
  await processMediaItems(ctx, [item], deps);
189
222
  }
190
223