obol-ai 0.3.17 → 0.3.19

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.19
2
+ - update changelog
3
+ - add curiosity toggle, fix describeToolCall crash in background tasks
4
+
5
+ ## 0.3.18
6
+ - store news in self-memory, boost humor output, fix analysis self-referential patterns
7
+ - update readme with curiosity, analysis, news, voice, patterns, and remove traits
8
+
1
9
  ## 0.3.16
2
10
  - Merge pull request #7 from jestersimpps/fix/stt-whisper-transcribe
3
11
  - fix: STT pipeline - add missing whisper_transcribe.py and fix media handler
package/README.md CHANGED
@@ -19,7 +19,7 @@ obol start -d # runs as background daemon (auto-installs pm2)
19
19
 
20
20
  ---
21
21
 
22
- 🧬 **Self-evolving** — Grows its own personality through conversation. Rewrites SOUL.md, USER.md, and AGENTS.md after 24h + minimum exchanges (configurable). Pre-evolution growth analysis guides personality continuity.
22
+ 🧬 **Self-evolving** — Grows its own personality through conversation. Rewrites SOUL.md, USER.md, and AGENTS.md nightly at 3am (per-user timezone). Pre-evolution growth analysis guides personality continuity.
23
23
 
24
24
  🔧 **Self-healing** — Writes tests for every script. Regressions get an automatic fix attempt before rollback. Failures stored as lessons.
25
25
 
@@ -27,9 +27,17 @@ obol start -d # runs as background daemon (auto-installs pm2)
27
27
 
28
28
  🧠 **Living memory** — Vector memory with semantic search. Haiku routes queries and rewrites them for better embedding hits. Free local embeddings.
29
29
 
30
- 🤖 **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
30
+ 🤖 **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.
31
31
 
32
- 💰 **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
32
+ 💰 **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.
33
+
34
+ 🔍 **Curious** — Explores the web on its own every 6 hours. Saves findings, schedules insights and humor for each user based on what it learns and who they are.
35
+
36
+ 📰 **Proactive news** — Searches for news on topics you care about twice daily (8am + 6pm). Cross-references with your memory to only share what's personally relevant. Friend-style, not newsletter.
37
+
38
+ 📊 **Pattern analysis** — Tracks your behavioral patterns every 3 hours — timing, mood, humor, engagement, communication, topics. Schedules natural follow-ups based on what it observes.
39
+
40
+ 🎙️ **Voice** — Text-to-speech voice messages and speech-to-text transcription for incoming voice notes. Toggle on/off per user.
33
41
 
34
42
  🛡️ **Self-hardening** — Auto-configures SSH (port 2222), firewall, fail2ban, encrypted secrets, and kernel hardening on first run.
35
43
 
@@ -41,7 +49,7 @@ obol start -d # runs as background daemon (auto-installs pm2)
41
49
 
42
50
  OBOL is an AI agent that evolves its own personality, rewrites its own code, tests its changes, and fixes what breaks — all from Telegram on your VPS.
43
51
 
44
- It starts as a blank slate. Through conversation it learns who you are, develops a personality shaped by your interactions, and builds operational knowledge about how to work with you. Every 24 hours (with enough conversation), it runs a growth analysis comparing who it was against who it's becoming, then rewrites its personality, refactors its own scripts, writes tests, fixes regressions, and builds you new tools based on patterns it spots in your conversations — scripts, commands, or full web apps. Over months it becomes an agent that's uniquely yours. No two OBOL instances are alike.
52
+ It starts as a blank slate. Through conversation it learns who you are, develops a personality shaped by your interactions, and builds operational knowledge about how to work with you. Every night at 3am it runs a growth analysis comparing who it was against who it's becoming, then rewrites its personality, refactors its own scripts, writes tests, fixes regressions, and builds you new tools based on patterns it spots in your conversations — scripts, commands, or full web apps. Between conversations it explores the web on its own, tracks your behavioral patterns, and proactively shares news and insights that connect to things you care about. Over months it becomes an agent that's uniquely yours. No two OBOL instances are alike.
45
53
 
46
54
  One bot, multiple users. Each allowed Telegram user gets a fully isolated context — their own personality, memory, evolution cycle, and workspace. User A's personality drift, scripts, and memories never leak into User B's. Everything runs in a single process with shared API credentials.
47
55
 
@@ -73,18 +81,17 @@ ranked recall escalates on tool use)
73
81
 
74
82
  Response → obol_messages
75
83
 
76
- ┌───────┴────────┐
77
- ↓ ↓
78
- Each exchange 24h + 10 exchanges
79
- ↓ ↓
80
- Haiku Sonnet
81
- consolidation evolution cycle
82
-
83
- Extract facts Growth analysis →
84
- → obol_memory rewrite personality,
85
- scripts, tests, commands.
86
- Build new tools.
87
- Git snapshot before + after.
84
+ ┌───────┴────────────────────────────────┐
85
+ ↓ ↓ ↓ ↓
86
+ Each exchange 3am daily Every 3h Every 6h
87
+ ↓ ↓ ↓ ↓
88
+ Haiku Sonnet Sonnet Sonnet
89
+ consolidation evolution analysis curiosity
90
+ cycle
91
+ Extract facts Rewrite Patterns Explore web,
92
+ → obol_memory personality, + follow dispatch
93
+ scripts, -ups insights
94
+ tests, apps + humor
88
95
  ```
89
96
 
90
97
  ### Layer 1: Message Log + Vector Memory
@@ -113,14 +120,12 @@ A 1-year-old memory with high similarity and high importance still surfaces. A t
113
120
 
114
121
  ### Layer 2: The Evolution Cycle
115
122
 
116
- Evolution triggers after a configurable time interval (default 24h) AND a minimum number of exchanges (default 10). The first evolution triggers earlier just 10 exchanges with no time gate. The bot checks readiness by querying the DB for assistant messages since the last evolution, so the count survives restarts.
123
+ Evolution runs nightly at 3am in each user's local timezone. It checks whether it already ran todayif so, it skips. The first evolution triggers on the first night after setup.
117
124
 
118
- **Pre-evolution growth analysis:** Before rewriting anything, Sonnet compares the previous SOUL against the current one, incorporating all new memories and conversations since the last evolution. It produces a structured growth report covering new learnings, relationship shifts, behavioral patterns, growth edges, trait pressure, and identity continuity. This report becomes the primary guide for the rewrite — evidence-based personality evolution instead of blind overwriting.
125
+ **Pre-evolution growth analysis:** Before rewriting anything, Sonnet compares the previous SOUL against the current one, incorporating all new memories and conversations since the last evolution. It produces a structured growth report covering new learnings, relationship shifts, behavioral patterns, growth edges, and identity continuity. This report becomes the primary guide for the rewrite — evidence-based personality evolution instead of blind overwriting.
119
126
 
120
127
  **Deep memory consolidation:** A Sonnet pass extracts every valuable fact from the full conversation history into vector memory, deduplicating against existing memories (threshold 0.92). This ensures nothing is lost between evolutions.
121
128
 
122
- **Personality traits** (humor, honesty, directness, curiosity, empathy, creativity) are scored 0-100 and adjusted ±5-15 each evolution based on conversation evidence. The growth report recommends specific trait shifts.
123
-
124
129
  **Cost-conscious model selection:** Evolution uses Sonnet for all phases — growth analysis, personality rewrites, code refactoring, and fix attempts. Sonnet keeps evolution costs negligible (~$0.02 per cycle).
125
130
 
126
131
  **Git snapshot before.** Full commit + push so you can always diff what changed.
@@ -129,10 +134,9 @@ Evolution triggers after a configurable time interval (default 24h) AND a minimu
129
134
 
130
135
  | Target | What happens |
131
136
  |--------|-------------|
132
- | **SOUL.md** | First-person journal — who the bot has become, relationship dynamic, opinions, quirks |
137
+ | **SOUL.md** | First-person journal — who the bot has become, relationship dynamic, opinions, quirks (shared across all users) |
133
138
  | **USER.md** | Third-person owner profile — facts, preferences, projects, people, communication style |
134
139
  | **AGENTS.md** | Operational manual — tools, workflows, lessons learned, patterns, rules |
135
- | **Traits** | Personality trait scores adjusted based on conversation evidence |
136
140
  | **scripts/** | Refactored, dead code removed, strict standards enforced |
137
141
  | **tests/** | Test for every script, run before and after refactor |
138
142
  | **commands/** | Cleaned up, new commands for new tools |
@@ -172,6 +176,24 @@ It searches npm/GitHub for existing libraries, installs dependencies, and writes
172
176
  Refined voice, updated your project list, cleaned up 2 unused scripts.
173
177
  ```
174
178
 
179
+ ### Layer 3: Background Intelligence
180
+
181
+ Three autonomous cycles run alongside conversations — no user interaction needed.
182
+
183
+ **Behavioral Analysis (every 3h):** Sonnet analyzes the last 3 hours of conversation, searching long-term memory for context. It extracts behavioral patterns across six dimensions — timing, mood, humor, engagement, communication, and topics — and schedules natural follow-ups with exact dates and times based on what it observes. Patterns accumulate over time with observation counts and confidence scores.
184
+
185
+ ```
186
+ "Mentioned a job interview on Thursday" → schedules a casual check-in for Thursday evening
187
+ "Most active between 7-10pm on weekdays" → stored as timing.active_hours (confidence 0.8)
188
+ ```
189
+
190
+ **Curiosity Engine (every 6h):** Sonnet gets free time with web search, its own knowledge base, and workspace file access. It researches from a point of view — not neutrally. Findings are saved with reactions, opinions, and open questions. After exploring, two passes run:
191
+
192
+ - **Dispatch** — decides which findings are worth sharing with which user, based on their patterns and interests. Schedules insights to arrive naturally.
193
+ - **Humor** — looks for puns, funny connections, and inside jokes tied to what it knows about each person. Schedules them to land at the right moment.
194
+
195
+ **Proactive News (8am + 6pm per-user timezone):** Searches the web for topics each user cares about, then cross-references with their memory to find personal connections. Only sends messages when something is genuinely relevant — max 3 per cycle, friend-style delivery with natural spacing between messages. Topics configured via `/tools`.
196
+
175
197
  ### The Lifecycle
176
198
 
177
199
  ```
@@ -180,15 +202,18 @@ Day 1: obol init → obol start → first conversation
180
202
  → post-setup hardens your VPS automatically
181
203
 
182
204
  Day 1: Every exchange → Haiku extracts facts to vector memory
205
+ Every 3h → behavioral analysis builds your pattern profile
206
+ Every 6h → curiosity cycle explores, dispatches insights
183
207
 
184
- Day 2: Evolution #1 → growth analysis + Sonnet rewrites everything
208
+ Day 2: 3am → Evolution #1 → growth analysis + Sonnet rewrites
185
209
  → voice shifts from generic to personal
186
210
  → old soul archived in evolution/
187
- traits calibrated to your communication style
211
+ 8am/6pm proactive news on topics you care about
188
212
 
189
213
  Month 2: Evolution #30 → notices you check crypto daily
190
214
  → builds a crypto dashboard
191
215
  → adds /pdf because you kept asking for PDFs
216
+ → curiosity drops inside jokes about your interests
192
217
 
193
218
  Month 6: evolution/ has 180+ archived souls
194
219
  → a readable timeline of how your bot evolved from
@@ -217,7 +242,7 @@ Here are the top 5 coworking spaces: ...
217
242
 
218
243
  ![Status UI](docs/obol-status.png)
219
244
 
220
- 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:
245
+ Every request shows a live status message with elapsed time, model routing info, and the current tool call. Status updates are instant — tool names and input summaries display the moment a tool starts. Two inline buttons let you cancel:
221
246
 
222
247
  | Button | Behavior |
223
248
  |--------|----------|
@@ -226,6 +251,31 @@ Every request shows a live status message with elapsed time, model routing info,
226
251
 
227
252
  The `/stop` command also works as a text alternative.
228
253
 
254
+ ### Voice & Media
255
+
256
+ OBOL handles images (vision), documents (PDF extraction), and voice — all via Telegram.
257
+
258
+ | Feature | How it works | Toggle |
259
+ |---------|-------------|--------|
260
+ | **Speech-to-Text** | Incoming voice messages are transcribed locally using faster-whisper (tiny model, ~140MB, CPU). Transcription is injected as context. | `/tools` → Speech to Text |
261
+ | **Text-to-Speech** | OBOL can reply with voice messages using edge-tts. Choose from multiple voices and languages. | `/tools` → Text to Speech |
262
+ | **Images** | Photos and images are analyzed via Claude's vision. Analysis is stored in memory for later recall. | Always on |
263
+ | **PDFs** | PDF files are extracted and read via the `read_file` tool. | Always on |
264
+
265
+ ### Optional Tools
266
+
267
+ Toggle features on/off per user via the `/tools` command:
268
+
269
+ | Tool | Default | Description |
270
+ |------|---------|-------------|
271
+ | Speech to Text | On | Transcribe incoming voice messages |
272
+ | Text to Speech | Off | Voice message replies |
273
+ | PDF Generator | Off | Create PDFs from markdown |
274
+ | Background Tasks | Off | Spawn long-running tasks |
275
+ | Flowchart | Off | Generate Mermaid diagrams |
276
+ | Model Stats | On | Show model/token info in responses |
277
+ | Proactive News | Off | Twice-daily news on configured topics |
278
+
229
279
  ## Multi-User Architecture
230
280
 
231
281
  One Telegram bot token, one Node.js process, full per-user isolation.
@@ -250,12 +300,13 @@ Router: ctx.from.id → tenant context
250
300
 
251
301
  | Shared (one copy) | Isolated (per user) |
252
302
  |---|---|
253
- | Telegram bot token | Personality (SOUL.md, USER.md, AGENTS.md) |
303
+ | Telegram bot token | Personality (USER.md, AGENTS.md) |
254
304
  | Anthropic API key | Vector memory (scoped by user_id in DB) |
255
305
  | Supabase connection | Message history (scoped by user_id in DB) |
256
306
  | VPS hardening | Evolution cycle + state |
257
307
  | Process manager (pm2) | Scripts, tests, commands, apps |
258
- | | Workspace directory (`~/.obol/users/{id}/`) |
308
+ | SOUL.md (shared personality) | Behavioral patterns (scoped by user_id in DB) |
309
+ | Curiosity knowledge base | Workspace directory (`~/.obol/users/{id}/`) |
259
310
 
260
311
  ### Tenant routing
261
312
 
@@ -472,10 +523,9 @@ Or edit `~/.obol/config.json` directly:
472
523
 
473
524
  | Key | Default | Description |
474
525
  |-----|---------|-------------|
475
- | `evolution.intervalHours` | 24 | Hours between evolution cycles |
476
- | `evolution.minExchanges` | 10 | Minimum exchanges before evolution can trigger |
477
- | `heartbeat` | false | Enable proactive check-ins |
478
526
  | `bridge.enabled` | false | Let user agents query each other (requires 2+ users) |
527
+ | `timezone` | UTC | Default timezone for evolution, analysis, and news cycles |
528
+ | `users[].timezone` | config timezone | Per-user timezone override |
479
529
 
480
530
  ## Telegram Commands
481
531
 
@@ -486,15 +536,14 @@ Or edit `~/.obol/config.json` directly:
486
536
  /today — Today's memories
487
537
  /events — Show upcoming scheduled events
488
538
  /tasks — Running background tasks
489
- /status — Bot status, uptime, evolution progress, traits
539
+ /status — Bot status, uptime, memory, evolution count
490
540
  /backup — Trigger GitHub backup
491
541
  /clean — Audit workspace, remove rogue files, fix misplaced items
492
- /traits — View or adjust personality traits (0-100)
493
542
  /secret — Manage per-user encrypted secrets
494
543
  /evolution — Evolution progress
495
544
  /verbose — Toggle verbose mode on/off
496
545
  /toolimit — View or set max tool iterations per message
497
- /tools — Toggle optional tools on/off
546
+ /tools — Toggle optional tools on/off (STT, TTS, PDF, news, etc.)
498
547
  /stop — Stop the current request
499
548
  /upgrade — Check for updates and upgrade
500
549
  /help — Show available commands
@@ -526,10 +575,11 @@ obol delete # Full VPS cleanup (removes all OBOL data)
526
575
  ```
527
576
  ~/.obol/
528
577
  ├── config.json # Shared credentials + allowedUsers
578
+ ├── personality/
579
+ │ └── SOUL.md # Shared personality (rewritten each evolution)
529
580
  ├── users/
530
581
  │ └── <telegram-user-id>/ # Per-user isolated context
531
582
  │ ├── personality/
532
- │ │ ├── SOUL.md # Bot personality (rewritten each evolution)
533
583
  │ │ ├── USER.md # Owner profile (rewritten each evolution)
534
584
  │ │ ├── AGENTS.md # Operational knowledge
535
585
  │ │ └── evolution/ # Archived previous souls
@@ -541,7 +591,7 @@ obol delete # Full VPS cleanup (removes all OBOL data)
541
591
  └── logs/
542
592
  ```
543
593
 
544
- Each allowed Telegram user gets their own isolated context separate personality, memory namespace, evolution cycle, and first-run experience. One bot process, full per-user isolation.
594
+ SOUL.md is shared — it's the bot's core identity across all users. USER.md and AGENTS.md are per-user, so each person gets their own profile and operational knowledge. Memory, patterns, evolution state, and workspace are fully isolated.
545
595
 
546
596
  ## Backup & Restore
547
597
 
@@ -574,6 +624,7 @@ obol start -d
574
624
  - Anthropic API key
575
625
  - Telegram bot token
576
626
  - Supabase account (free tier)
627
+ - Python 3 + `pip3 install faster-whisper` (optional, for voice transcription)
577
628
 
578
629
  **[→ Full DigitalOcean deployment guide](docs/DEPLOY.md)**
579
630
 
@@ -590,8 +641,10 @@ obol start -d
590
641
  | **Security** | Auto-hardens on first run | Manual |
591
642
  | **Model routing** | Automatic (Haiku) | Manual overrides |
592
643
  | **Background tasks** | Built-in with check-ins | Sub-agent spawning |
644
+ | **Proactive intelligence** | Curiosity, analysis, news, humor | — |
645
+ | **Voice** | TTS + STT (faster-whisper) | TTS |
593
646
  | **Group chats** | — | Full support |
594
- | **Cron** | Basic node-cron | Full scheduler |
647
+ | **Cron** | Agentic cron (tool access) + basic | Full scheduler |
595
648
  | **Cost** | ~$9/mo | ~$9/mo+ |
596
649
 
597
650
  ### Performance
@@ -603,7 +656,7 @@ obol start -d
603
656
  | **Heap usage** | ~16 MB | ~80-200 MB |
604
657
  | **RSS** | ~109 MB | ~300-600 MB |
605
658
  | **node_modules** | 354 MB / 9 deps | ~1-2 GB / 50-100+ deps |
606
- | **Source code** | ~5,100 lines (plain JS) | Tens of thousands (TypeScript monorepo) |
659
+ | **Source code** | ~13,600 lines (plain JS) | Tens of thousands (TypeScript monorepo) |
607
660
  | **Native apps** | None | Swift (macOS/iOS), Kotlin (Android) |
608
661
 
609
662
  The Claude API call dominates response time at 1-5s for both — that's ~85-90% of total latency. User-perceived speed difference is ~10-20%. Where OBOL wins is cold start (10-20x), memory footprint (5-10x), and operational simplicity. On a $5/mo VPS, that matters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
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/analysis.js CHANGED
@@ -36,62 +36,38 @@ function buildTranscript(messages) {
36
36
  }
37
37
 
38
38
  async function generateReport(client, memory, transcript, timezone) {
39
- const tools = memory ? [{
40
- name: 'memory_search',
41
- description: 'Search long-term memory for context about a topic in this transcript',
42
- input_schema: {
43
- type: 'object',
44
- properties: { query: { type: 'string' } },
45
- required: ['query'],
46
- },
47
- }] : [];
39
+ const since = new Date(Date.now() - ANALYSIS_WINDOW_MS);
40
+ const memories = memory ? await memory.query({ since, limit: 100 }).catch(() => []) : [];
41
+ const memoryContext = memories.length
42
+ ? `\nWhat you already know about them:\n${memories.map(m => `- ${m.content}`).join('\n')}`
43
+ : '';
48
44
 
49
- const system = `You are an attentive observer analyzing a conversation transcript. Write a free-form analytical report covering:
45
+ const system = `You are analyzing a conversation transcript to understand the HUMAN in it. Write a report about THEIR behavior, personality, and patterns.
50
46
 
51
- 1. INTENTIONS & FOLLOW-UPS: Any intentions expressed, upcoming events, pending tasks, or things worth a natural check-in later. Be selective — only things a friend would genuinely remember.
47
+ 1. INTENTIONS & FOLLOW-UPS: Intentions they expressed, upcoming events, pending tasks, or things worth a natural check-in later. Be selective — only things a friend would genuinely remember.
52
48
 
53
- 2. BEHAVIORAL PATTERNS:
54
- - Timing: when they tend to message, active windows, energy by day/time
49
+ 2. BEHAVIORAL PATTERNS (about the human, not about yourself):
50
+ - Timing: when they message, active windows, energy by day/time
55
51
  - Mood signals: emotional baseline, stress indicators, good/bad day signals
56
52
  - Humor style: what lands, banter comfort, comedic preferences
57
53
  - Engagement depth: which topics generate longer responses, what they bring up unprompted
58
54
  - Communication style: message length, formality, response patterns
59
55
  - Recurring topics: what keeps coming up, what lights them up, what they avoid
60
56
 
61
- Write candidly and specifically. "Active between 9-11pm" beats "sometimes active at night". Skip categories with no signal. Timezone context: ${timezone}.${memory ? ' Use the memory_search tool to look up relevant context about topics in the transcript before writing your report.' : ''}`;
57
+ IMPORTANT: Only describe the human's behavior. Every observation must be about what the human said or did.
62
58
 
63
- const messages = [{ role: 'user', content: `Conversation transcript:\n\n${transcript}` }];
59
+ Write candidly and specifically. "Active between 9-11pm" beats "sometimes active at night". Skip categories with no signal. Timezone context: ${timezone}.${memoryContext}`;
64
60
 
65
61
  try {
66
- for (let i = 0; i < 6; i++) {
67
- const response = await client.messages.create({
68
- model: 'claude-sonnet-4-6',
69
- max_tokens: 2000,
70
- system,
71
- ...(tools.length ? { tools, tool_choice: { type: 'auto' } } : {}),
72
- messages,
73
- });
74
-
75
- const text = response.content.find(b => b.type === 'text');
76
- if (text) return text.text;
77
-
78
- const toolUses = response.content.filter(b => b.type === 'tool_use');
79
- if (!toolUses.length) return null;
80
-
81
- messages.push({ role: 'assistant', content: response.content });
82
- const results = [];
83
- for (const tu of toolUses) {
84
- const hits = await memory.search(tu.input.query, { limit: 5 }).catch(() => []);
85
- results.push({
86
- type: 'tool_result',
87
- tool_use_id: tu.id,
88
- content: hits.length ? hits.map(m => `- ${m.content}`).join('\n') : 'No relevant memories found',
89
- });
90
- }
91
- messages.push({ role: 'user', content: results });
92
- }
62
+ const response = await client.messages.create({
63
+ model: 'claude-sonnet-4-6',
64
+ max_tokens: 2000,
65
+ system,
66
+ messages: [{ role: 'user', content: `Conversation transcript:\n\n${transcript}` }],
67
+ });
93
68
 
94
- return null;
69
+ const text = response.content.find(b => b.type === 'text');
70
+ return text?.text || null;
95
71
  } catch (e) {
96
72
  console.error('[analysis] Report generation failed:', e.message);
97
73
  return null;
@@ -142,7 +118,7 @@ async function structureReport(client, report, scheduler, patterns, chatId, time
142
118
 
143
119
  try {
144
120
  const localTime = new Date().toLocaleString('en-US', { timeZone: timezone, dateStyle: 'full', timeStyle: 'short' });
145
- const patternGuidance = `Extract behavioral patterns about this user from the report. Each pattern must be a factual observation about the user's behavior not notes about your analysis process. If you see the same pattern in the existing list, reuse its exact key and update the summary/confidence. Skip patterns already at confidence >0.8 unless new evidence contradicts them.`;
121
+ const patternGuidance = `Extract behavioral patterns about the human from the report. Each pattern must describe something the HUMAN does how they write, when they're active, what they talk about, how they respond. Never include patterns about your own behavior, tool usage, analysis approach, or system processes. If you see the same pattern in the existing list, reuse its exact key and update the summary/confidence. Skip patterns already at confidence >0.8 unless new evidence contradicts them.`;
146
122
  const timingGuidance = `Current local time for this user: ${localTime}. For each follow-up, pick a specific date or datetime in the user's local time based on what you know from the transcript. Use ISO 8601 format: "2024-03-15" for date-only or "2024-03-15T20:00" for exact time.`;
147
123
 
148
124
  const system = formattedPatterns
@@ -42,6 +42,12 @@ const OPTIONAL_TOOLS = {
42
42
  topics: { label: 'Topics', default: [] },
43
43
  },
44
44
  },
45
+ curiosity: {
46
+ label: 'Curiosity',
47
+ tools: [],
48
+ config: {},
49
+ defaultEnabled: true,
50
+ },
45
51
  };
46
52
 
47
53
  const BLOCKED_EXEC_PATTERNS = [
@@ -59,9 +59,22 @@ async function runCuriosityHumor(client, selfMemory, users) {
59
59
  ];
60
60
 
61
61
  const userList = users.map(u => `- user_id: ${u.userId}`).join('\n');
62
- const system = `You just finished a curiosity cycle. Now look at what you found and see if anything is funnya pun you can make, a weird connection, something that'd land as an inside joke with a specific person based on who they are and what you know about them.\n\nUsers:\n${userList}\n\nBe picky. Only schedule something if it's actually clever. Forced humor is worse than none.`;
62
+ const system = `You just finished a curiosity cycle and explored some things. Now find the humor in what you foundweird facts, absurd connections, niche references that'd land with someone specific based on their personality and interests.
63
+
64
+ Types of humor that work well:
65
+ - Absurd juxtapositions between something you found and something you know about a person
66
+ - Niche references only they'd get
67
+ - Dry observations about something weird you stumbled on
68
+ - A follow-up to something you talked about before, with a twist
69
+ - Playful "did you know" moments that are genuinely surprising
70
+
71
+ Users:
72
+ ${userList}
73
+
74
+ Aim for at least 1 per user. Search the web if your findings alone aren't enough — look for weird facts, unexpected connections, or timely jokes related to their interests. Get user context first so you know what would land.`;
63
75
 
64
76
  const messages = [{ role: 'user', content: 'Take a look at what you found and see if anything is worth a laugh.' }];
77
+ let scheduled = 0;
65
78
 
66
79
  for (let i = 0; i < MAX_ITERATIONS; i++) {
67
80
  const response = await client.messages.create({
@@ -74,7 +87,13 @@ async function runCuriosityHumor(client, selfMemory, users) {
74
87
 
75
88
  messages.push({ role: 'assistant', content: response.content });
76
89
 
77
- if (response.stop_reason === 'end_turn') break;
90
+ if (response.stop_reason === 'end_turn') {
91
+ if (scheduled === 0 && i < 3) {
92
+ messages.push({ role: 'user', content: 'You haven\'t scheduled anything yet. Search the web for something funny related to their interests, or look at your findings from a different angle. Even a dry observation works.' });
93
+ continue;
94
+ }
95
+ break;
96
+ }
78
97
  if (response.stop_reason !== 'tool_use') break;
79
98
 
80
99
  const toolResults = [];
@@ -84,6 +103,7 @@ async function runCuriosityHumor(client, selfMemory, users) {
84
103
 
85
104
  try {
86
105
  const result = await handleTool(block.name, block.input, selfMemory, userMap);
106
+ if (block.name === 'schedule_humor' && result === 'Scheduled') scheduled++;
87
107
  toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
88
108
  } catch (e) {
89
109
  toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Error: ${e.message}` });
@@ -95,7 +115,7 @@ async function runCuriosityHumor(client, selfMemory, users) {
95
115
  }
96
116
  }
97
117
 
98
- console.log('[curiosity-humor] Humor pass complete');
118
+ console.log(`[curiosity-humor] Humor pass complete — scheduled ${scheduled}`);
99
119
  }
100
120
 
101
121
  async function handleTool(name, input, selfMemory, userMap) {
@@ -3,7 +3,7 @@ const MAX_ITERATIONS = 12;
3
3
  const CONFIDENCE_THRESHOLD = 0.6;
4
4
  const MAX_MESSAGES = 3;
5
5
 
6
- async function runProactiveNews(client, topics, memory, personality, timezone) {
6
+ async function runProactiveNews(client, topics, memory, personality, timezone, selfMemory) {
7
7
  const log = process.env.OBOL_VERBOSE ? (msg) => console.log(`[news] ${msg}`) : () => {};
8
8
  const composed = [];
9
9
 
@@ -102,6 +102,14 @@ async function runProactiveNews(client, topics, memory, personality, timezone) {
102
102
 
103
103
  if (confidence >= CONFIDENCE_THRESHOLD && composed.length < MAX_MESSAGES) {
104
104
  composed.push(message);
105
+ if (selfMemory) {
106
+ selfMemory.add(message, {
107
+ category: 'research',
108
+ importance: Math.min(confidence, 0.8),
109
+ tags: ['news', topic.toLowerCase()],
110
+ source: 'news',
111
+ }).catch(e => log(`Failed to store news in self-memory: ${e.message}`));
112
+ }
105
113
  toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Message queued for delivery' });
106
114
  } else if (composed.length >= MAX_MESSAGES) {
107
115
  toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Already have ${MAX_MESSAGES} messages queued. Done.` });
@@ -150,6 +150,10 @@ async function createMemory(supabaseConfig, userId = 0) {
150
150
  if (opts.source) parts.push(`source=eq.${opts.source}`);
151
151
  if (opts.minImportance) parts.push(`importance=gte.${opts.minImportance}`);
152
152
  if (opts.tags?.length) parts.push(`tags=ov.{${opts.tags.join(',')}}`);
153
+ if (opts.since) {
154
+ const sinceDate = opts.since instanceof Date ? opts.since : new Date(opts.since);
155
+ parts.push(`created_at=gte.${sinceDate.toISOString()}`);
156
+ }
153
157
  if (opts.date) {
154
158
  const { start, end } = parseDateRange(opts.date);
155
159
  parts.push(`created_at=gte.${start.toISOString()}`);
@@ -1,4 +1,4 @@
1
- const { buildStatusHtml, describeToolCall } = require('../status');
1
+ const { buildStatusHtml, formatToolCall } = require('../status');
2
2
 
3
3
  const MAX_CONCURRENT_TASKS = 3;
4
4
 
@@ -88,10 +88,7 @@ TASK: ${task}`;
88
88
  if (update.model) routeInfo.model = update.model;
89
89
  },
90
90
  _onToolStart: (toolName, inputSummary) => {
91
- statusText = 'Processing';
92
- describeToolCall(claude.client, toolName, inputSummary).then(desc => {
93
- if (desc) statusText = desc;
94
- });
91
+ statusText = formatToolCall(toolName, inputSummary) || 'Processing';
95
92
  startStatusTimer();
96
93
  },
97
94
  });
@@ -12,7 +12,7 @@ const { createSelfMemory } = require('../memory/self');
12
12
 
13
13
 
14
14
  const ANALYSIS_HOURS = new Set([4, 7, 10, 13, 16, 19, 22]);
15
- const CURIOSITY_HOURS = new Set([1, 7, 13, 19]);
15
+ const CURIOSITY_HOURS = new Set([1, 13]);
16
16
  const NEWS_HOURS = new Set([8, 18]);
17
17
 
18
18
  const _evolutionRunning = new Set();
@@ -87,13 +87,27 @@ async function runCuriosityOnce(config, allowedUsers) {
87
87
  console.log('[curiosity] Skipping — previous cycle still running');
88
88
  return;
89
89
  }
90
+
91
+ const enabledUsers = [];
92
+ for (const userId of allowedUsers) {
93
+ const tenant = await getTenant(userId, config);
94
+ const pref = tenant.toolPrefs?.get('curiosity');
95
+ const enabled = pref ? pref.enabled : true;
96
+ if (enabled) enabledUsers.push(userId);
97
+ }
98
+
99
+ if (!enabledUsers.length) {
100
+ console.log('[curiosity] Skipping — no users have curiosity enabled');
101
+ return;
102
+ }
103
+
90
104
  _curiosityRunning = true;
91
105
  try {
92
106
  const selfMemory = await createSelfMemory(config.supabase, 0);
93
- const firstTenant = await getTenant(allowedUsers[0], config);
107
+ const firstTenant = await getTenant(enabledUsers[0], config);
94
108
  const client = firstTenant.claude.client;
95
109
 
96
- const contexts = await Promise.all(allowedUsers.map(async (userId) => {
110
+ const contexts = await Promise.all(enabledUsers.map(async (userId) => {
97
111
  try {
98
112
  const tenant = await getTenant(userId, config);
99
113
  const parts = [];
@@ -120,7 +134,7 @@ async function runCuriosityOnce(config, allowedUsers) {
120
134
  const firstUserDir = firstTenant.userDir;
121
135
  await runCuriosity(client, selfMemory, 0, { peopleContext, userDir: firstUserDir });
122
136
 
123
- const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
137
+ const userDispatchData = await Promise.all(enabledUsers.map(async (userId) => {
124
138
  try {
125
139
  const tenant = await getTenant(userId, config);
126
140
  const patterns = tenant.patterns ? await tenant.patterns.format().catch(() => null) : null;
@@ -169,7 +183,8 @@ async function runNewsForUser(bot, config, userId) {
169
183
  const client = new Anthropic({ apiKey: config.anthropic.apiKey });
170
184
  const timezone = getUserTimezone(config, userId);
171
185
 
172
- const messages = await runProactiveNews(client, topics, tenant.memory, tenant.personality, timezone);
186
+ const selfMemory = config.supabase ? await createSelfMemory(config.supabase, 0).catch(() => null) : null;
187
+ const messages = await runProactiveNews(client, topics, tenant.memory, tenant.personality, timezone, selfMemory);
173
188
 
174
189
  for (let i = 0; i < messages.length; i++) {
175
190
  if (i > 0) {