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 +8 -0
- package/README.md +90 -37
- package/package.json +1 -1
- package/src/analysis.js +20 -44
- package/src/claude/constants.js +6 -0
- package/src/curiosity/humor.js +23 -3
- package/src/curiosity/news.js +9 -1
- package/src/memory/index.js +4 -0
- package/src/runtime/background.js +2 -5
- package/src/runtime/heartbeat.js +20 -5
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
|
|
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
|
|
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
|
|
79
|
-
↓ ↓
|
|
80
|
-
Haiku
|
|
81
|
-
consolidation
|
|
82
|
-
↓
|
|
83
|
-
Extract facts
|
|
84
|
-
→ obol_memory
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
123
|
+
Evolution runs nightly at 3am in each user's local timezone. It checks whether it already ran today — if 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,
|
|
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
|
|
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
|
-
→
|
|
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
|

|
|
219
244
|
|
|
220
|
-
Every request shows a live status message with elapsed time, model routing info, and
|
|
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 (
|
|
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
|
-
| |
|
|
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
|
|
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
|
-
|
|
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** |
|
|
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** | ~
|
|
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.
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
57
|
+
IMPORTANT: Only describe the human's behavior. Every observation must be about what the human said or did.
|
|
62
58
|
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/src/claude/constants.js
CHANGED
package/src/curiosity/humor.js
CHANGED
|
@@ -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
|
|
62
|
+
const system = `You just finished a curiosity cycle and explored some things. Now find the humor in what you found — weird 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')
|
|
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(
|
|
118
|
+
console.log(`[curiosity-humor] Humor pass complete — scheduled ${scheduled}`);
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
async function handleTool(name, input, selfMemory, userMap) {
|
package/src/curiosity/news.js
CHANGED
|
@@ -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.` });
|
package/src/memory/index.js
CHANGED
|
@@ -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,
|
|
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
|
});
|
package/src/runtime/heartbeat.js
CHANGED
|
@@ -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,
|
|
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(
|
|
107
|
+
const firstTenant = await getTenant(enabledUsers[0], config);
|
|
94
108
|
const client = firstTenant.claude.client;
|
|
95
109
|
|
|
96
|
-
const contexts = await Promise.all(
|
|
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(
|
|
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
|
|
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) {
|