obol-ai 0.2.16 ā 0.2.18
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 +11 -0
- package/README.md +42 -15
- package/docs/obol-status.png +0 -0
- package/package.json +1 -1
- package/src/bridge.js +4 -2
- package/src/claude/chat.js +14 -1
- package/src/claude/prompt.js +3 -3
- package/src/claude/tool-registry.js +12 -3
- package/src/claude/tools/memory.js +17 -1
- package/src/claude/tools/web.js +3 -23
- package/src/evolve/prompts.js +1 -1
- package/src/telegram/bot.js +1 -1
- package/src/telegram/commands/status.js +5 -16
- package/src/telegram/handlers/callbacks.js +70 -1
- package/src/telegram/handlers/text.js +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 0.2.18
|
|
2
|
+
- remove evolution progress bar from status UI
|
|
3
|
+
- bidirectional bridge with reply button + memory_remove tool
|
|
4
|
+
- update background tasks section in readme
|
|
5
|
+
- add status UI screenshot to readme
|
|
6
|
+
- update readme with stop controls, commands, and model escalation
|
|
7
|
+
|
|
8
|
+
## 0.2.17
|
|
9
|
+
- add force stop button to instantly abort mid-tool execution
|
|
10
|
+
- replace web_fetch with native web_search tool
|
|
11
|
+
|
|
1
12
|
## 0.2.16
|
|
2
13
|
- add chat_history tool for retrieving past conversations by date
|
|
3
14
|
- add stop button to status UI with concurrent update processing
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ obol start -d # runs as background daemon (auto-installs pm2)
|
|
|
22
22
|
|
|
23
23
|
š§ **Living memory** ā Vector memory with semantic search. Haiku routes queries and rewrites them for better embedding hits. Free local embeddings.
|
|
24
24
|
|
|
25
|
-
š¤ **Smart routing** ā Haiku decides per-message: does it need memory? Sonnet or Opus? No wasted API calls
|
|
25
|
+
š¤ **Smart routing** ā Haiku decides per-message: does it need memory? Sonnet or Opus? Auto-escalates to Sonnet when tool use is needed. No wasted API calls
|
|
26
26
|
|
|
27
27
|
š° **Prompt caching** ā Static system prompt and conversation history prefix are cached via Anthropic's prompt caching, cutting ~85% of repeated input token costs across turns
|
|
28
28
|
|
|
@@ -59,9 +59,9 @@ User message
|
|
|
59
59
|
ā ā
|
|
60
60
|
Memory recall Model selection
|
|
61
61
|
ā ā
|
|
62
|
-
Multi-query Sonnet (
|
|
63
|
-
ranked recall
|
|
64
|
-
ā
|
|
62
|
+
Multi-query Haiku ā Sonnet (auto-
|
|
63
|
+
ranked recall escalates on tool use)
|
|
64
|
+
ā or Opus (complex)
|
|
65
65
|
āāāāāāāā¬āāāāāāā
|
|
66
66
|
ā
|
|
67
67
|
Claude (tool use loop)
|
|
@@ -199,21 +199,32 @@ Month 6: evolution/ has 180+ archived souls
|
|
|
199
199
|
|
|
200
200
|
### Background Tasks
|
|
201
201
|
|
|
202
|
-
Heavy work runs in the background. The main conversation stays responsive.
|
|
202
|
+
Heavy work runs in the background with its own live status UI. The main conversation stays responsive ā you can keep chatting while tasks run.
|
|
203
203
|
|
|
204
204
|
```
|
|
205
205
|
You: "research the best coworking spaces in Barcelona"
|
|
206
|
-
OBOL:
|
|
207
|
-
|
|
208
|
-
[30s] ā³ Found 15 spaces, filtering by reviews...
|
|
209
|
-
[60s] ā³ Narrowed to top 7, checking prices...
|
|
206
|
+
OBOL: spawns BG #1 with live status
|
|
210
207
|
|
|
211
208
|
You: "what time is it?"
|
|
212
209
|
OBOL: "11:42 PM CET"
|
|
213
210
|
|
|
214
|
-
|
|
211
|
+
ā
BG #1 done (1m 32s)
|
|
212
|
+
Here are the top 5 coworking spaces: ...
|
|
215
213
|
```
|
|
216
214
|
|
|
215
|
+
### Live Status & Stop Controls
|
|
216
|
+
|
|
217
|
+

|
|
218
|
+
|
|
219
|
+
Every request shows a live status message with elapsed time, model routing info, and what tools are being used. Two inline buttons let you cancel:
|
|
220
|
+
|
|
221
|
+
| Button | Behavior |
|
|
222
|
+
|--------|----------|
|
|
223
|
+
| **ā Stop** | Cancels after the current API call finishes |
|
|
224
|
+
| **ā Force Stop** | Instantly aborts mid-tool ā races the handler and returns immediately |
|
|
225
|
+
|
|
226
|
+
The `/stop` command also works as a text alternative.
|
|
227
|
+
|
|
217
228
|
## Multi-User Architecture
|
|
218
229
|
|
|
219
230
|
One Telegram bot token, one Node.js process, full per-user isolation.
|
|
@@ -275,29 +286,39 @@ Each new user starts fresh. Their bot evolves independently from every other use
|
|
|
275
286
|
|
|
276
287
|
### Bridge (couples / roommates / teams)
|
|
277
288
|
|
|
278
|
-
When two users share the same OBOL instance, their agents can talk to each other.
|
|
289
|
+
When two users share the same OBOL instance, their agents can talk to each other ā bidirectionally.
|
|
279
290
|
|
|
280
291
|
```
|
|
281
292
|
User A: "what does Jo want for dinner tonight?"
|
|
282
293
|
Agent A: ā bridge_ask ā Agent B (one-shot, no tools, no history)
|
|
283
294
|
Agent B: "Jo mentioned craving Thai food earlier today"
|
|
284
295
|
Agent A: "Jo's been wanting Thai ā maybe suggest pad see ew?"
|
|
296
|
+
|
|
297
|
+
Jo gets: "šŖ Your partner's agent asked: 'what does Jo want for dinner?'
|
|
298
|
+
Your agent answered: 'Jo mentioned craving Thai food earlier today'"
|
|
285
299
|
```
|
|
286
300
|
|
|
287
301
|
```
|
|
288
302
|
User A: "remind Jo I'll be home late"
|
|
289
303
|
Agent A: ā bridge_tell ā stores in Agent B's memory + Telegram notification
|
|
290
|
-
|
|
304
|
+
|
|
305
|
+
Jo gets: "šŖ Message from your partner's agent:
|
|
306
|
+
'I'll be home late'"
|
|
307
|
+
[ā© Reply]
|
|
308
|
+
|
|
309
|
+
Jo taps Reply ā Jo's agent reads recent bridge context, composes a reply
|
|
310
|
+
ā sends back via bridge_tell
|
|
311
|
+
A gets: "šŖ Message from your partner's agent: 'Got it, I'll start dinner around 7'"
|
|
291
312
|
```
|
|
292
313
|
|
|
293
314
|
Two tools:
|
|
294
315
|
|
|
295
316
|
| Tool | Direction | What happens |
|
|
296
317
|
|------|-----------|--------------|
|
|
297
|
-
| `bridge_ask` | A ā B ā A | Query the partner's agent. One-shot
|
|
298
|
-
| `bridge_tell` | A ā B | Send a message to the partner. Stored in their memory (importance 0.6) + Telegram notification.
|
|
318
|
+
| `bridge_ask` | A ā B ā A | Query the partner's agent. One-shot Haiku call with partner's personality + memories. No tools, no history, no recursion risk. Partner is notified with both the question and your agent's answer. |
|
|
319
|
+
| `bridge_tell` | A ā B (ā© B ā A) | Send a message to the partner. Stored in their memory (importance 0.6) + Telegram notification with a Reply button. Tapping Reply has their agent compose a contextual response and send it back ā no typing needed. |
|
|
299
320
|
|
|
300
|
-
The partner always gets notified when their agent is contacted. Privacy rules apply ā the responding agent gives summaries, never raw data or secrets.
|
|
321
|
+
The partner always gets notified when their agent is contacted. Privacy rules apply ā the responding agent gives summaries, never raw data or secrets. Rate-limited to 20 bridge calls per user per hour.
|
|
301
322
|
|
|
302
323
|
Enable during `obol init` (auto-prompted when 2+ users are added) or toggle later with `obol config` ā Bridge.
|
|
303
324
|
|
|
@@ -471,6 +492,7 @@ Or edit `~/.obol/config.json` directly:
|
|
|
471
492
|
/memory ā Search or view memory stats
|
|
472
493
|
/recent ā Last 10 memories
|
|
473
494
|
/today ā Today's memories
|
|
495
|
+
/events ā Show upcoming scheduled events
|
|
474
496
|
/tasks ā Running background tasks
|
|
475
497
|
/status ā Bot status, uptime, evolution progress, traits
|
|
476
498
|
/backup ā Trigger GitHub backup
|
|
@@ -478,6 +500,11 @@ Or edit `~/.obol/config.json` directly:
|
|
|
478
500
|
/traits ā View or adjust personality traits (0-100)
|
|
479
501
|
/secret ā Manage per-user encrypted secrets
|
|
480
502
|
/evolution ā Evolution progress
|
|
503
|
+
/verbose ā Toggle verbose mode on/off
|
|
504
|
+
/toolimit ā View or set max tool iterations per message
|
|
505
|
+
/tools ā Toggle optional tools on/off
|
|
506
|
+
/stop ā Stop the current request
|
|
507
|
+
/upgrade ā Check for updates and upgrade
|
|
481
508
|
/help ā Show available commands
|
|
482
509
|
```
|
|
483
510
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
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/bridge.js
CHANGED
|
@@ -106,7 +106,8 @@ async function bridgeAsk(question, fromUserId, config, notifyFn, targetId) {
|
|
|
106
106
|
|
|
107
107
|
if (notifyFn) {
|
|
108
108
|
try {
|
|
109
|
-
|
|
109
|
+
const preview = answer.length > 200 ? `${answer.substring(0, 200)}ā¦` : answer;
|
|
110
|
+
await notifyFn(partnerUserId, `šŖ Your partner's agent asked: "${question}"\nYour agent answered: "${preview}"`);
|
|
110
111
|
} catch (e) {
|
|
111
112
|
console.error(`[bridge] Notify failed for ${partnerUserId}:`, e.message);
|
|
112
113
|
}
|
|
@@ -170,7 +171,8 @@ async function bridgeTell(message, fromUserId, config, notifyFn, targetId) {
|
|
|
170
171
|
|
|
171
172
|
if (notifyFn) {
|
|
172
173
|
try {
|
|
173
|
-
|
|
174
|
+
const replyMarkup = { inline_keyboard: [[{ text: 'ā© Reply', callback_data: `bridge:reply:${fromUserId}` }]] };
|
|
175
|
+
await notifyFn(partnerUserId, `šŖ Message from your partner's agent:\n"${message}"`, { reply_markup: replyMarkup });
|
|
174
176
|
} catch (e) {
|
|
175
177
|
console.error(`[bridge] Notify failed for ${partnerUserId}:`, e.message);
|
|
176
178
|
}
|
package/src/claude/chat.js
CHANGED
|
@@ -16,6 +16,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
16
16
|
const histories = new ChatHistory(50);
|
|
17
17
|
const chatLocks = new Map();
|
|
18
18
|
const chatAbortControllers = new Map();
|
|
19
|
+
const chatForceControllers = new Map();
|
|
19
20
|
|
|
20
21
|
const tools = buildTools(memory, { bridgeEnabled });
|
|
21
22
|
|
|
@@ -45,7 +46,9 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
45
46
|
|
|
46
47
|
const releaseLock = await acquireChatLock(chatId);
|
|
47
48
|
const abortController = new AbortController();
|
|
49
|
+
const forceController = new AbortController();
|
|
48
50
|
chatAbortControllers.set(chatId, abortController);
|
|
51
|
+
chatForceControllers.set(chatId, forceController);
|
|
49
52
|
|
|
50
53
|
const history = histories.get(chatId);
|
|
51
54
|
|
|
@@ -95,6 +98,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
95
98
|
];
|
|
96
99
|
context._reloadPersonality = reloadPersonality;
|
|
97
100
|
context._abortSignal = abortController.signal;
|
|
101
|
+
context._forceSignal = forceController.signal;
|
|
98
102
|
const runnableTools = buildRunnableTools(tools, memory, context, vlog);
|
|
99
103
|
let activeModel = model;
|
|
100
104
|
|
|
@@ -198,6 +202,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
198
202
|
throw e;
|
|
199
203
|
} finally {
|
|
200
204
|
chatAbortControllers.delete(chatId);
|
|
205
|
+
chatForceControllers.delete(chatId);
|
|
201
206
|
releaseLock();
|
|
202
207
|
}
|
|
203
208
|
}
|
|
@@ -211,6 +216,14 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
211
216
|
return false;
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
function forceStopChat(chatId) {
|
|
220
|
+
const controller = chatAbortControllers.get(chatId);
|
|
221
|
+
const force = chatForceControllers.get(chatId);
|
|
222
|
+
if (force) force.abort();
|
|
223
|
+
if (controller) { controller.abort(); return true; }
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
function reloadPersonality() {
|
|
215
228
|
const pDir = userDir ? path.join(userDir, 'personality') : undefined;
|
|
216
229
|
const newPersonality = require('../personality').loadPersonality(pDir);
|
|
@@ -236,7 +249,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
236
249
|
return histories.estimateTokens(id, baseSystemPrompt.length);
|
|
237
250
|
}
|
|
238
251
|
|
|
239
|
-
return { chat, client, reloadPersonality, clearHistory, injectHistory, getContextStats, stopChat };
|
|
252
|
+
return { chat, client, reloadPersonality, clearHistory, injectHistory, getContextStats, stopChat, forceStopChat };
|
|
240
253
|
}
|
|
241
254
|
|
|
242
255
|
module.exports = { createClaude };
|
package/src/claude/prompt.js
CHANGED
|
@@ -114,8 +114,8 @@ Categories: \`fact\`, \`preference\`, \`decision\`, \`lesson\`, \`person\`, \`pr
|
|
|
114
114
|
Read and write files within your workspace. Parent directories created automatically.
|
|
115
115
|
Cannot access paths outside workspace or /tmp.
|
|
116
116
|
|
|
117
|
-
### Web (\`
|
|
118
|
-
|
|
117
|
+
### Web (\`web_search\`)
|
|
118
|
+
Search the web for current information.
|
|
119
119
|
|
|
120
120
|
### Vercel (\`vercel_deploy\`, \`vercel_list\`)
|
|
121
121
|
Deploy directories to Vercel. Ship websites, dashboards, web apps.
|
|
@@ -252,7 +252,7 @@ Structure tips:
|
|
|
252
252
|
- Search memory before claiming you don't know something
|
|
253
253
|
- Use \`store_secret\`/\`read_secret\` for all credential operations
|
|
254
254
|
- If a user sends what appears to be an API key, token, or credential in conversation, immediately warn them that it's visible in chat history, tell them to revoke/rotate it, and direct them to use \`/secret set <key> <value>\` instead
|
|
255
|
-
- After executing tools (exec,
|
|
255
|
+
- After executing tools (exec, web_search, read_secret, etc.), ALWAYS provide a text response summarizing what you found or did. Never end your turn with only tool calls and no text reply ā the user cannot see tool results directly, they only see your text responses
|
|
256
256
|
`);
|
|
257
257
|
|
|
258
258
|
return parts.join('\n');
|
|
@@ -32,8 +32,9 @@ const INPUT_SUMMARIES = {
|
|
|
32
32
|
read_file: (i) => i.path,
|
|
33
33
|
memory_search: (i) => i.query,
|
|
34
34
|
memory_add: (i) => `[${i.category || 'fact'}]`,
|
|
35
|
+
memory_remove: (i) => i.ids?.join(', '),
|
|
35
36
|
memory_query: (i) => `${i.date || ''}${i.tags ? ' #' + i.tags.join(' #') : ''}${i.category ? ' [' + i.category + ']' : ''}`.trim() || 'all',
|
|
36
|
-
|
|
37
|
+
web_search: (i) => i.query,
|
|
37
38
|
background_task: (i) => i.task?.substring(0, 60),
|
|
38
39
|
schedule_event: (i) => `${i.title} @ ${i.due_at}${i.cron_expr ? ` [${i.cron_expr}]` : ''}`,
|
|
39
40
|
cancel_event: (i) => i.event_id,
|
|
@@ -95,7 +96,9 @@ function buildRunnableTools(tools, memory, context, vlog) {
|
|
|
95
96
|
.map(tool => ({
|
|
96
97
|
...tool,
|
|
97
98
|
run: async (input) => {
|
|
98
|
-
|
|
99
|
+
const signal = context._abortSignal;
|
|
100
|
+
const forceSignal = context._forceSignal;
|
|
101
|
+
if (signal?.aborted) return 'Aborted.';
|
|
99
102
|
const inputSummary = summarizeInput(tool.name, input);
|
|
100
103
|
vlog(`[tool] ${tool.name}: ${inputSummary}`);
|
|
101
104
|
context._onToolStart?.(tool.name, inputSummary);
|
|
@@ -104,8 +107,14 @@ function buildRunnableTools(tools, memory, context, vlog) {
|
|
|
104
107
|
if (!handler) return `Unknown tool: ${tool.name}`;
|
|
105
108
|
|
|
106
109
|
try {
|
|
107
|
-
return await handler(input, memory, context);
|
|
110
|
+
if (!forceSignal) return await handler(input, memory, context);
|
|
111
|
+
if (forceSignal.aborted) return 'Aborted.';
|
|
112
|
+
const forcePromise = new Promise((resolve) => {
|
|
113
|
+
forceSignal.addEventListener('abort', () => resolve('Aborted.'), { once: true });
|
|
114
|
+
});
|
|
115
|
+
return await Promise.race([handler(input, memory, context), forcePromise]);
|
|
108
116
|
} catch (e) {
|
|
117
|
+
if (signal?.aborted || forceSignal?.aborted) return 'Aborted.';
|
|
109
118
|
return `Error: ${e.message}`;
|
|
110
119
|
}
|
|
111
120
|
},
|
|
@@ -27,6 +27,17 @@ const definitions = [
|
|
|
27
27
|
required: ['content'],
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
name: 'memory_remove',
|
|
32
|
+
description: 'Remove one or more memories by their IDs. Use memory_query or memory_search first to get IDs.',
|
|
33
|
+
input_schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to remove' },
|
|
37
|
+
},
|
|
38
|
+
required: ['ids'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
30
41
|
{
|
|
31
42
|
name: 'memory_query',
|
|
32
43
|
description: 'Filter memories by tag, date, category, source, or importance. Use for "what did we do today", "anything tagged X", "all decisions this week".',
|
|
@@ -47,7 +58,7 @@ const definitions = [
|
|
|
47
58
|
function formatMemory(m) {
|
|
48
59
|
const date = m.created_at ? new Date(m.created_at).toISOString().slice(0, 10) : null;
|
|
49
60
|
const tags = m.tags?.length ? ` #${m.tags.join(' #')}` : '';
|
|
50
|
-
return `[${date || '?'}] [${m.category}] ${m.content}${tags}`;
|
|
61
|
+
return `[id:${m.id}] [${date || '?'}] [${m.category}] ${m.content}${tags}`;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const handlers = {
|
|
@@ -69,6 +80,11 @@ const handlers = {
|
|
|
69
80
|
return `Stored memory: ${result.id}`;
|
|
70
81
|
},
|
|
71
82
|
|
|
83
|
+
async memory_remove(input, memory) {
|
|
84
|
+
await Promise.all(input.ids.map(id => memory.forget(id)));
|
|
85
|
+
return `Removed ${input.ids.length} memory${input.ids.length !== 1 ? 'ies' : ''}`;
|
|
86
|
+
},
|
|
87
|
+
|
|
72
88
|
async memory_query(input, memory) {
|
|
73
89
|
const results = await memory.query({
|
|
74
90
|
date: input.date,
|
package/src/claude/tools/web.js
CHANGED
|
@@ -1,28 +1,8 @@
|
|
|
1
|
-
const { isAllowedUrl } = require('../../sanitize');
|
|
2
|
-
|
|
3
1
|
const definitions = [{
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
input_schema: {
|
|
7
|
-
type: 'object',
|
|
8
|
-
properties: {
|
|
9
|
-
url: { type: 'string', description: 'URL to fetch' },
|
|
10
|
-
},
|
|
11
|
-
required: ['url'],
|
|
12
|
-
},
|
|
2
|
+
type: 'web_search_20250305',
|
|
3
|
+
name: 'web_search',
|
|
13
4
|
}];
|
|
14
5
|
|
|
15
|
-
const handlers = {
|
|
16
|
-
async web_fetch(input) {
|
|
17
|
-
if (!isAllowedUrl(input.url)) return 'Blocked: URL points to a private/internal address.';
|
|
18
|
-
const jinaUrl = `https://r.jina.ai/${input.url}`;
|
|
19
|
-
const res = await fetch(jinaUrl, {
|
|
20
|
-
headers: { 'Accept': 'text/markdown' },
|
|
21
|
-
});
|
|
22
|
-
if (!res.ok) return `Failed to fetch: HTTP ${res.status}`;
|
|
23
|
-
const text = await res.text();
|
|
24
|
-
return text.substring(0, 15000);
|
|
25
|
-
},
|
|
26
|
-
};
|
|
6
|
+
const handlers = {};
|
|
27
7
|
|
|
28
8
|
module.exports = { definitions, handlers };
|
package/src/evolve/prompts.js
CHANGED
|
@@ -17,7 +17,7 @@ Third person factual profile: name, location, timezone, nationality, job, skills
|
|
|
17
17
|
Operational manual written as instructions to yourself. Focus on owner-specific workflows, service integrations, and lessons learned from conversations.
|
|
18
18
|
|
|
19
19
|
**Do NOT include in AGENTS.md** ā these are already hardcoded in the base system prompt and must not be duplicated:
|
|
20
|
-
- Tool documentation (exec, memory_*, read_file, write_file,
|
|
20
|
+
- Tool documentation (exec, memory_*, read_file, write_file, web_search, vercel_*, background_task, store_secret, read_secret, list_secrets, send_file, telegram_ask, bridge_*)
|
|
21
21
|
- Telegram Formatting rules
|
|
22
22
|
- Safety Rules (Never/Always)
|
|
23
23
|
- Workspace Structure
|
package/src/telegram/bot.js
CHANGED
|
@@ -48,7 +48,7 @@ function createBot(telegramConfig, config) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
bot.use(sequentialize((ctx) => {
|
|
51
|
-
if (ctx.callbackQuery?.data?.startsWith('stop:')) return undefined;
|
|
51
|
+
if (ctx.callbackQuery?.data?.startsWith('stop:') || ctx.callbackQuery?.data?.startsWith('force:')) return undefined;
|
|
52
52
|
return ctx.chat?.id.toString();
|
|
53
53
|
}));
|
|
54
54
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { loadConfig } = require('../../config');
|
|
4
3
|
const { loadTraits } = require('../../personality');
|
|
5
|
-
const {
|
|
4
|
+
const { loadEvolutionState } = require('../../evolve');
|
|
6
5
|
const { getMaxToolIterations } = require('../../claude');
|
|
7
6
|
const { termBar, formatTraits } = require('../utils');
|
|
8
7
|
const { TERM_SEP } = require('../constants');
|
|
@@ -44,16 +43,13 @@ function register(bot, config) {
|
|
|
44
43
|
);
|
|
45
44
|
|
|
46
45
|
const evoState = loadEvolutionState(tenant.userDir);
|
|
47
|
-
const cfg = loadConfig();
|
|
48
|
-
const intervalHours = cfg?.evolution?.intervalHours ?? 24;
|
|
49
|
-
const elapsed = evoState.lastEvolution ? (Date.now() - new Date(evoState.lastEvolution).getTime()) / 3600000 : Infinity;
|
|
50
|
-
const evoPct = Math.min(100, Math.round((elapsed / intervalHours) * 100));
|
|
51
|
-
const timeLeft = Math.max(0, intervalHours - elapsed);
|
|
52
46
|
lines.push(
|
|
53
47
|
``, `EVOLUTION`,
|
|
54
|
-
` ${
|
|
55
|
-
` ${timeLeft < 1 ? 'ready' : `${timeLeft.toFixed(1)}h remaining`} āŖ ${evoState.evolutionCount || 0} completed`,
|
|
48
|
+
` ${evoState.evolutionCount || 0} completed`,
|
|
56
49
|
);
|
|
50
|
+
if (evoState.lastEvolution) {
|
|
51
|
+
lines.push(` last ${new Date(evoState.lastEvolution).toLocaleDateString()}`);
|
|
52
|
+
}
|
|
57
53
|
|
|
58
54
|
const personalityDir = path.join(tenant.userDir, 'personality');
|
|
59
55
|
const traits = loadTraits(personalityDir);
|
|
@@ -68,18 +64,11 @@ function register(bot, config) {
|
|
|
68
64
|
if (!ctx.from) return;
|
|
69
65
|
const tenant = await getTenant(ctx.from.id, config);
|
|
70
66
|
const state = loadEvolutionState(tenant.userDir);
|
|
71
|
-
const cfg = loadConfig();
|
|
72
|
-
const intervalHours = cfg?.evolution?.intervalHours ?? 24;
|
|
73
|
-
const elapsed = state.lastEvolution ? (Date.now() - new Date(state.lastEvolution).getTime()) / 3600000 : Infinity;
|
|
74
|
-
const pct = Math.min(100, Math.round((elapsed / intervalHours) * 100));
|
|
75
|
-
const timeLeft = Math.max(0, intervalHours - elapsed);
|
|
76
67
|
|
|
77
68
|
const lines = [
|
|
78
69
|
`ā OBOL EVOLUTION CYCLE`,
|
|
79
70
|
TERM_SEP,
|
|
80
71
|
``,
|
|
81
|
-
` ${termBar(pct)} ${pct}%`,
|
|
82
|
-
` ${timeLeft < 1 ? 'ready' : `${timeLeft.toFixed(1)}h remaining`}`,
|
|
83
72
|
` ${state.evolutionCount || 0} completed`,
|
|
84
73
|
];
|
|
85
74
|
if (state.lastEvolution) {
|
|
@@ -12,7 +12,16 @@ function registerCallbackHandler(bot, { config, pendingAsks, getTenant }) {
|
|
|
12
12
|
const userId = ctx.from.id;
|
|
13
13
|
const tenant = await getTenant(userId, config);
|
|
14
14
|
const stopped = tenant?.claude?.stopChat(chatId);
|
|
15
|
-
await answer({ text: stopped ? 'Stopping' : 'Nothing to stop' });
|
|
15
|
+
await answer({ text: stopped ? 'Stopping...' : 'Nothing to stop' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (data.startsWith('force:')) {
|
|
20
|
+
const chatId = parseInt(data.slice(6));
|
|
21
|
+
const userId = ctx.from.id;
|
|
22
|
+
const tenant = await getTenant(userId, config);
|
|
23
|
+
const stopped = tenant?.claude?.forceStopChat(chatId);
|
|
24
|
+
await answer({ text: stopped ? 'Force stopped' : 'Nothing to stop' });
|
|
16
25
|
return;
|
|
17
26
|
}
|
|
18
27
|
|
|
@@ -27,6 +36,66 @@ function registerCallbackHandler(bot, { config, pendingAsks, getTenant }) {
|
|
|
27
36
|
return;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
if (data.startsWith('bridge:reply:')) {
|
|
40
|
+
const targetUserId = parseInt(data.split(':')[2]);
|
|
41
|
+
const reactingUserId = ctx.from.id;
|
|
42
|
+
|
|
43
|
+
const tenant = await getTenant(reactingUserId, config);
|
|
44
|
+
if (!tenant) return answer({ text: 'Could not load your agent' });
|
|
45
|
+
|
|
46
|
+
const { checkBridgeRateLimit, bridgeTell } = require('../../bridge');
|
|
47
|
+
const rateErr = checkBridgeRateLimit(reactingUserId);
|
|
48
|
+
if (rateErr) return answer({ text: rateErr });
|
|
49
|
+
|
|
50
|
+
let memoryContext = '';
|
|
51
|
+
if (tenant.memory) {
|
|
52
|
+
try {
|
|
53
|
+
const memories = await tenant.memory.search('message from partner bridge', { limit: 5, threshold: 0.3 });
|
|
54
|
+
if (memories.length > 0) {
|
|
55
|
+
memoryContext = '\n\n[Recent bridge messages]\n' + memories.map(m => `- ${m.content}`).join('\n');
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const systemParts = [
|
|
61
|
+
'Compose a brief, natural reply to send back to your partner\'s agent via bridge. 1-3 sentences. Be genuine and respond to the most recent message from them.',
|
|
62
|
+
];
|
|
63
|
+
if (tenant.personality?.soul) systemParts.push(`\n## Your Personality\n${tenant.personality.soul}`);
|
|
64
|
+
if (tenant.personality?.user) systemParts.push(`\n## About You\n${tenant.personality.user}`);
|
|
65
|
+
if (memoryContext) systemParts.push(memoryContext);
|
|
66
|
+
|
|
67
|
+
let replyText;
|
|
68
|
+
try {
|
|
69
|
+
const response = await tenant.claude.client.messages.create({
|
|
70
|
+
model: 'claude-haiku-4-5-20251001',
|
|
71
|
+
max_tokens: 256,
|
|
72
|
+
system: systemParts.join('\n'),
|
|
73
|
+
messages: [{ role: 'user', content: 'Compose your reply to send via bridge.' }],
|
|
74
|
+
});
|
|
75
|
+
replyText = response.content.filter(b => b.type === 'text').map(b => b.text).join('\n').trim();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error('[bridge:reply] Generation failed:', e.message);
|
|
78
|
+
return answer({ text: 'Failed to generate reply' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!replyText) return answer({ text: 'Could not generate a reply' });
|
|
82
|
+
|
|
83
|
+
const notifyFn = (uid, msg, opts = {}) => ctx.api.sendMessage(uid, msg, opts);
|
|
84
|
+
try {
|
|
85
|
+
await bridgeTell(replyText, reactingUserId, config, notifyFn, targetUserId);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('[bridge:reply] bridgeTell failed:', e.message);
|
|
88
|
+
return answer({ text: 'Failed to send reply' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ctx.editMessageText(
|
|
92
|
+
ctx.callbackQuery.message.text + '\n\nā Reply sent',
|
|
93
|
+
{ reply_markup: { inline_keyboard: [] } }
|
|
94
|
+
).catch(() => {});
|
|
95
|
+
|
|
96
|
+
return answer({ text: 'Reply sent!' });
|
|
97
|
+
}
|
|
98
|
+
|
|
30
99
|
if (!data.startsWith('ask:')) return answer();
|
|
31
100
|
const parts = data.split(':');
|
|
32
101
|
const askId = parseInt(parts[1]);
|
|
@@ -27,9 +27,9 @@ function createChatContext(ctx, tenant, config, { allowedUsers, bot, createAsk }
|
|
|
27
27
|
sendHtml(ctx, `\`${msg}\``).catch(() => {});
|
|
28
28
|
} : undefined,
|
|
29
29
|
telegramAsk: (message, options, timeout) => createAsk(ctx, message, options, timeout),
|
|
30
|
-
_notifyFn: (targetUserId, message) => {
|
|
30
|
+
_notifyFn: (targetUserId, message, opts = {}) => {
|
|
31
31
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
32
|
-
return bot.api.sendMessage(targetUserId, message);
|
|
32
|
+
return bot.api.sendMessage(targetUserId, message, opts);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
|
@@ -40,7 +40,9 @@ function createStatusTracker(ctx) {
|
|
|
40
40
|
let statusTimer = null;
|
|
41
41
|
let statusStart = null;
|
|
42
42
|
let routeInfo = null;
|
|
43
|
-
const stopBtn = new InlineKeyboard()
|
|
43
|
+
const stopBtn = new InlineKeyboard()
|
|
44
|
+
.text('ā Stop', `stop:${ctx.chat.id}`)
|
|
45
|
+
.text('ā Force Stop', `force:${ctx.chat.id}`);
|
|
44
46
|
|
|
45
47
|
const clear = () => {
|
|
46
48
|
if (statusTimer) { clearInterval(statusTimer); statusTimer = null; }
|