obol-ai 0.3.3 → 0.3.5
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 +5 -0
- package/package.json +1 -1
- package/src/claude/chat.js +2 -1
- package/src/claude/router.js +4 -2
- package/src/claude/tools/files.js +18 -4
- package/src/cli/config.js +22 -20
- package/src/cli/init.js +12 -7
- package/src/cli/prompt-debug.js +39 -4
- package/src/curiosity.js +95 -24
- package/src/heartbeat.js +2 -1
- package/src/messages.js +14 -6
- package/src/status.js +5 -2
- package/src/telegram/commands/admin.js +2 -2
- package/src/telegram/handlers/media.js +2 -1
- package/src/telegram/handlers/special.js +2 -1
- package/src/telegram/handlers/text.js +7 -5
- package/src/tenant.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 0.3.5
|
|
2
|
+
- add knowledge_search + read_file to curiosity, self-memory count in status, opinionated research prompt
|
|
3
|
+
- 0.3.4: smarter memory dedup, better router queries, rebalanced recall
|
|
4
|
+
- fix shared soul path, backup on tool write, show bot name in status
|
|
5
|
+
|
|
1
6
|
## 0.3.3
|
|
2
7
|
- update changelog
|
|
3
8
|
- improve memory retrieval quality: tighter dedup, wider window, recency boost, self-memory, jaccard dedup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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/claude/chat.js
CHANGED
|
@@ -267,8 +267,9 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
function reloadPersonality() {
|
|
270
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
270
271
|
const pDir = userDir ? path.join(userDir, 'personality') : undefined;
|
|
271
|
-
const newPersonality = require('../personality').loadPersonality(pDir);
|
|
272
|
+
const newPersonality = require('../personality').loadPersonality(PERSONALITY_DIR, pDir);
|
|
272
273
|
for (const key of Object.keys(personality)) delete personality[key];
|
|
273
274
|
Object.assign(personality, newPersonality);
|
|
274
275
|
baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
|
package/src/claude/router.js
CHANGED
|
@@ -36,7 +36,7 @@ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision
|
|
|
36
36
|
Reply with ONLY a JSON object:
|
|
37
37
|
{"need_memory": true/false, "search_queries": ["query1", "query2"], "model": "haiku|sonnet|opus"}
|
|
38
38
|
|
|
39
|
-
search_queries: 1-
|
|
39
|
+
search_queries: 1-5 optimized search queries based on the full conversation context. Cover distinct topics, people, entities, time periods, or projects referenced. Single-topic messages need just one query. Use more queries when the message references multiple people, projects, or threads.
|
|
40
40
|
|
|
41
41
|
Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true.
|
|
42
42
|
|
|
@@ -73,12 +73,13 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
73
73
|
|
|
74
74
|
if (decision.need_memory && memory) {
|
|
75
75
|
const budget = decision.model === 'opus' ? 40 : decision.model === 'haiku' ? 15 : 25;
|
|
76
|
+
const poolPerQuery = decision.model === 'opus' ? 20 : decision.model === 'haiku' ? 10 : 15;
|
|
76
77
|
const searchQueries = queries.length > 0 ? queries : [userMessage];
|
|
77
78
|
|
|
78
79
|
const recentMemories = await memory.byDate('7d', { limit: Math.ceil(budget / 3) });
|
|
79
80
|
|
|
80
81
|
const semanticResults = await Promise.all(
|
|
81
|
-
searchQueries.map(q => memory.search(q, { limit:
|
|
82
|
+
searchQueries.map(q => memory.search(q, { limit: poolPerQuery, threshold: 0.4 }))
|
|
82
83
|
);
|
|
83
84
|
const semanticMemories = semanticResults.flat();
|
|
84
85
|
|
|
@@ -127,6 +128,7 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
127
128
|
const selfLines = topSelf.slice(0, 8).map(m => `- [${m.category}] ${m.content}`);
|
|
128
129
|
memoryBlock = (memoryBlock || '') + `\n\n## Self-knowledge\n${selfLines.join('\n')}`;
|
|
129
130
|
vlog(`[memory] +${topSelf.length} self-memory facts`);
|
|
131
|
+
onRouteUpdate?.({ selfMemoryCount: topSelf.length });
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
}
|
|
@@ -96,7 +96,10 @@ const definitions = [
|
|
|
96
96
|
const handlers = {
|
|
97
97
|
async read_file(input, memory, context) {
|
|
98
98
|
const { userDir } = context;
|
|
99
|
-
const
|
|
99
|
+
const { PERSONALITY_DIR } = require('../../soul');
|
|
100
|
+
const filePath = path.basename(input.path) === 'SOUL.md'
|
|
101
|
+
? path.join(PERSONALITY_DIR, 'SOUL.md')
|
|
102
|
+
: (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
100
103
|
if (filePath.toLowerCase().endsWith('.pdf')) {
|
|
101
104
|
const pdfParse = require('pdf-parse');
|
|
102
105
|
const { text } = await pdfParse(fs.readFileSync(filePath));
|
|
@@ -119,25 +122,36 @@ const handlers = {
|
|
|
119
122
|
|
|
120
123
|
async write_file(input, memory, context) {
|
|
121
124
|
const { userDir } = context;
|
|
122
|
-
const
|
|
125
|
+
const { PERSONALITY_DIR, backup } = require('../../soul');
|
|
126
|
+
const isSoul = path.basename(input.path) === 'SOUL.md';
|
|
127
|
+
const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
123
128
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
124
129
|
fs.writeFileSync(filePath, input.content);
|
|
125
130
|
if (filePath.includes('personality/')) {
|
|
126
131
|
context._reloadPersonality?.();
|
|
132
|
+
if (isSoul && context.config?.supabase) {
|
|
133
|
+
backup(context.config.supabase, 'soul', input.content).catch(() => {});
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
return `Written: ${filePath}`;
|
|
129
137
|
},
|
|
130
138
|
|
|
131
139
|
async edit_file(input, memory, context) {
|
|
132
140
|
const { userDir } = context;
|
|
133
|
-
const
|
|
141
|
+
const { PERSONALITY_DIR, backup } = require('../../soul');
|
|
142
|
+
const isSoul = path.basename(input.path) === 'SOUL.md';
|
|
143
|
+
const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
134
144
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
135
145
|
const count = content.split(input.old_string).length - 1;
|
|
136
146
|
if (count === 0) return `Error: old_string not found in ${input.path}`;
|
|
137
147
|
if (count > 1) return `Error: old_string matches ${count} times — add more context to make it unique`;
|
|
138
|
-
|
|
148
|
+
const updated = content.replace(input.old_string, input.new_string);
|
|
149
|
+
fs.writeFileSync(filePath, updated);
|
|
139
150
|
if (filePath.includes('personality/')) {
|
|
140
151
|
context._reloadPersonality?.();
|
|
152
|
+
if (isSoul && context.config?.supabase) {
|
|
153
|
+
backup(context.config.supabase, 'soul', updated).catch(() => {});
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
return `Edited: ${filePath}`;
|
|
143
157
|
},
|
package/src/cli/config.js
CHANGED
|
@@ -353,38 +353,40 @@ async function runOAuthFlow(cfg) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
function updatePersonalityNames(oldBotName, newBotName, oldOwnerName, newOwnerName) {
|
|
356
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
357
|
+
|
|
358
|
+
if (oldBotName !== newBotName) {
|
|
359
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
360
|
+
if (fs.existsSync(soulPath)) {
|
|
361
|
+
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
362
|
+
content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
|
|
363
|
+
content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
|
|
364
|
+
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (oldOwnerName !== newOwnerName) {
|
|
369
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
370
|
+
if (fs.existsSync(soulPath)) {
|
|
371
|
+
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
372
|
+
content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
|
|
373
|
+
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
356
377
|
if (!fs.existsSync(USERS_DIR)) return;
|
|
357
378
|
const users = fs.readdirSync(USERS_DIR).filter(u => {
|
|
358
379
|
try { return fs.statSync(path.join(USERS_DIR, u)).isDirectory(); } catch { return false; }
|
|
359
380
|
});
|
|
360
381
|
for (const userId of users) {
|
|
361
|
-
const personalityDir = path.join(USERS_DIR, userId, 'personality');
|
|
362
|
-
if (!fs.existsSync(personalityDir)) continue;
|
|
363
|
-
|
|
364
382
|
if (oldBotName !== newBotName) {
|
|
365
|
-
const
|
|
366
|
-
if (fs.existsSync(soulPath)) {
|
|
367
|
-
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
368
|
-
content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
|
|
369
|
-
content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
|
|
370
|
-
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
371
|
-
}
|
|
372
|
-
const agentsPath = path.join(personalityDir, 'AGENTS.md');
|
|
383
|
+
const agentsPath = path.join(USERS_DIR, userId, 'personality', 'AGENTS.md');
|
|
373
384
|
if (fs.existsSync(agentsPath)) {
|
|
374
385
|
let content = fs.readFileSync(agentsPath, 'utf-8');
|
|
375
386
|
content = content.replace(new RegExp(`# AGENTS\\.md — How ${oldBotName} Works`, 'g'), `# AGENTS.md — How ${newBotName} Works`);
|
|
376
387
|
fs.writeFileSync(agentsPath, content, 'utf-8');
|
|
377
388
|
}
|
|
378
389
|
}
|
|
379
|
-
|
|
380
|
-
if (oldOwnerName !== newOwnerName) {
|
|
381
|
-
const soulPath = path.join(personalityDir, 'SOUL.md');
|
|
382
|
-
if (fs.existsSync(soulPath)) {
|
|
383
|
-
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
384
|
-
content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
|
|
385
|
-
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
package/src/cli/init.js
CHANGED
|
@@ -752,13 +752,12 @@ function ensureDirs() {
|
|
|
752
752
|
}
|
|
753
753
|
|
|
754
754
|
function createPersonalityFiles(config) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
fs.mkdirSync(personalityDir, { recursive: true });
|
|
755
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
756
|
+
fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
|
|
757
|
+
const ownerName = config.users?.[String(config.telegram.allowedUsers[0])]?.name || config.owner.name;
|
|
759
758
|
|
|
760
|
-
|
|
761
|
-
|
|
759
|
+
if (!fs.existsSync(path.join(PERSONALITY_DIR, 'SOUL.md'))) {
|
|
760
|
+
fs.writeFileSync(path.join(PERSONALITY_DIR, 'SOUL.md'), `# SOUL.md — Who is ${config.bot.name}?
|
|
762
761
|
|
|
763
762
|
Write your bot's personality here. This shapes how it talks, thinks, and behaves.
|
|
764
763
|
|
|
@@ -781,7 +780,13 @@ Write your bot's personality here. This shapes how it talks, thinks, and behaves
|
|
|
781
780
|
---
|
|
782
781
|
*Edit this file anytime to reshape your bot's personality.*
|
|
783
782
|
`);
|
|
784
|
-
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
for (const userId of config.telegram.allowedUsers) {
|
|
786
|
+
const ownerName = config.users?.[String(userId)]?.name || config.owner.name;
|
|
787
|
+
const personalityDir = path.join(OBOL_DIR, 'users', String(userId), 'personality');
|
|
788
|
+
fs.mkdirSync(personalityDir, { recursive: true });
|
|
789
|
+
|
|
785
790
|
if (!fs.existsSync(path.join(personalityDir, 'USER.md'))) {
|
|
786
791
|
fs.writeFileSync(path.join(personalityDir, 'USER.md'), `# USER.md — About ${ownerName}
|
|
787
792
|
|
package/src/cli/prompt-debug.js
CHANGED
|
@@ -76,13 +76,22 @@ function truncate(str, max = 500) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function printFullPrompt(params) {
|
|
79
|
-
label(
|
|
79
|
+
label(`FULL PROMPT → SYSTEM \x1b[2m[${params.model || '?'}]\x1b[0m`);
|
|
80
80
|
for (const block of (params.system || [])) {
|
|
81
81
|
const cached = block.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
|
|
82
82
|
console.log(`${truncate(block.text, 800)}${cached}`);
|
|
83
83
|
console.log(hr('·'));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
if (params.tools?.length) {
|
|
87
|
+
label(`FULL PROMPT → TOOLS [${params.tools.length}]`);
|
|
88
|
+
for (const tool of params.tools) {
|
|
89
|
+
const cached = tool.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
|
|
90
|
+
const desc = tool.description ? ` \x1b[2m${truncate(tool.description, 100)}\x1b[0m` : '';
|
|
91
|
+
console.log(` \x1b[35m${tool.name}\x1b[0m${cached}${desc}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
label(`FULL PROMPT → MESSAGES [${params.messages.length}]`);
|
|
87
96
|
for (let i = 0; i < params.messages.length; i++) {
|
|
88
97
|
const msg = params.messages[i];
|
|
@@ -92,7 +101,15 @@ function printFullPrompt(params) {
|
|
|
92
101
|
: msg.content;
|
|
93
102
|
for (const block of blocks) {
|
|
94
103
|
if (block.type === 'text') {
|
|
95
|
-
|
|
104
|
+
if (block.text.startsWith('[Runtime context')) {
|
|
105
|
+
console.log(`\x1b[2m── runtime metadata ──\x1b[0m`);
|
|
106
|
+
} else if (block.text.startsWith('Current time:')) {
|
|
107
|
+
console.log(`\x1b[2m${block.text}\x1b[0m`);
|
|
108
|
+
} else if (block.text.includes('## Relevant memories') || block.text.includes('## Self-knowledge')) {
|
|
109
|
+
console.log(`\x1b[33m${block.text}\x1b[0m`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(truncate(block.text, 1000));
|
|
112
|
+
}
|
|
96
113
|
} else if (block.type === 'tool_use') {
|
|
97
114
|
console.log(`\x1b[35m[tool_use: ${block.name}]\x1b[0m ${truncate(JSON.stringify(block.input), 200)}`);
|
|
98
115
|
} else if (block.type === 'tool_result') {
|
|
@@ -131,10 +148,11 @@ async function runInspect(opts, config) {
|
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
|
|
151
|
+
const recentLimit = Math.ceil(25 / 3);
|
|
152
|
+
label(`RECENT MEMORY — ${recentLimit} entries \x1b[2m(budget/3, sonnet)\x1b[0m`);
|
|
135
153
|
const memHeaders = makeSupabaseHeaders(config.supabase);
|
|
136
154
|
const memRes = await fetch(
|
|
137
|
-
`${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit
|
|
155
|
+
`${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=${recentLimit}&select=content,category,importance,tags,created_at`,
|
|
138
156
|
{ headers: memHeaders }
|
|
139
157
|
);
|
|
140
158
|
const memories = await memRes.json();
|
|
@@ -148,6 +166,23 @@ async function runInspect(opts, config) {
|
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
label('OBOL SELF MEMORY — 20 entries');
|
|
170
|
+
const selfRes = await fetch(
|
|
171
|
+
`${config.supabase.url}/rest/v1/obol_self_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=20&select=content,category,importance,tags,source,created_at`,
|
|
172
|
+
{ headers: memHeaders }
|
|
173
|
+
);
|
|
174
|
+
const selfMemories = await selfRes.json();
|
|
175
|
+
if (!selfMemories.length) {
|
|
176
|
+
console.log(' (none)');
|
|
177
|
+
} else {
|
|
178
|
+
for (const m of selfMemories) {
|
|
179
|
+
const tags = m.tags?.length ? ` \x1b[2m[${m.tags.join(', ')}]\x1b[0m` : '';
|
|
180
|
+
const src = m.source ? ` \x1b[2msrc=${m.source}\x1b[0m` : '';
|
|
181
|
+
console.log(`\x1b[2m${new Date(m.created_at).toLocaleString()}\x1b[0m \x1b[35m[${m.category}]\x1b[0m imp=${m.importance}${tags}${src}`);
|
|
182
|
+
console.log(` ${m.content}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
console.log('\n\x1b[2mTip: pass -m "message" to run the full production pipeline\x1b[0m\n');
|
|
152
187
|
}
|
|
153
188
|
|
package/src/curiosity.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OBOL_DIR } = require('./config');
|
|
4
|
+
|
|
1
5
|
const RESEARCH_MODEL = 'claude-sonnet-4-6';
|
|
2
|
-
const MAX_ITERATIONS =
|
|
6
|
+
const MAX_ITERATIONS = 15;
|
|
3
7
|
|
|
4
8
|
async function runCuriosity(client, selfMemory, userId, opts = {}) {
|
|
5
|
-
const { memory, patterns, scheduler, peopleContext } = opts;
|
|
9
|
+
const { memory, patterns, scheduler, peopleContext, userDir } = opts;
|
|
6
10
|
|
|
7
11
|
const interests = await selfMemory.recent({ category: 'interest', limit: 10 });
|
|
8
|
-
const
|
|
12
|
+
const previousFindings = await selfMemory.recent({ category: 'research', limit: 5 });
|
|
13
|
+
const context = await gatherContext({ memory, patterns, scheduler, peopleContext, interests, previousFindings });
|
|
9
14
|
|
|
10
15
|
console.log(`[curiosity] Starting free exploration for user ${userId}`);
|
|
11
|
-
const count = await exploreFreely(client, selfMemory, context);
|
|
16
|
+
const count = await exploreFreely(client, selfMemory, context, userDir);
|
|
12
17
|
console.log(`[curiosity] Stored ${count} things (user ${userId})`);
|
|
13
18
|
return { count };
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests }) {
|
|
21
|
+
async function gatherContext({ memory, patterns, scheduler, peopleContext, interests, previousFindings }) {
|
|
17
22
|
const parts = [];
|
|
18
23
|
|
|
19
24
|
if (peopleContext) parts.push(peopleContext);
|
|
@@ -35,24 +40,52 @@ async function gatherContext({ memory, patterns, scheduler, peopleContext, inter
|
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
if (previousFindings.length) {
|
|
44
|
+
parts.push(`What you've been exploring recently:\n${previousFindings.map(i => `- ${i.content}`).join('\n')}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
if (interests.length) {
|
|
39
|
-
parts.push(`
|
|
48
|
+
parts.push(`Open threads — things you wanted to come back to:\n${interests.map(i => `- ${i.content}`).join('\n')}`);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
return parts.join('\n\n');
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
async function exploreFreely(client, selfMemory, context) {
|
|
54
|
+
async function exploreFreely(client, selfMemory, context, userDir) {
|
|
55
|
+
const workDir = userDir || OBOL_DIR;
|
|
56
|
+
|
|
46
57
|
const tools = [
|
|
47
58
|
{ type: 'web_search_20250305', name: 'web_search' },
|
|
59
|
+
{
|
|
60
|
+
name: 'knowledge_search',
|
|
61
|
+
description: 'Search your own memory — what you\'ve researched, reflected on, and learned before',
|
|
62
|
+
input_schema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
query: { type: 'string', description: 'What to search for in your own knowledge' },
|
|
66
|
+
},
|
|
67
|
+
required: ['query'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'read_file',
|
|
72
|
+
description: 'Read a file from the workspace',
|
|
73
|
+
input_schema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: 'string', description: 'File path relative to workspace' },
|
|
77
|
+
},
|
|
78
|
+
required: ['path'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
48
81
|
{
|
|
49
82
|
name: 'remember',
|
|
50
|
-
description: 'Save something
|
|
83
|
+
description: 'Save something — a finding, a reaction, a question, a reflection. Include your actual take on it, not just a neutral summary.',
|
|
51
84
|
input_schema: {
|
|
52
85
|
type: 'object',
|
|
53
86
|
properties: {
|
|
54
|
-
content: { type: 'string', description: 'What you want to
|
|
55
|
-
category: { type: 'string', enum: ['research', 'interest', 'self'], description: 'research: something you found; interest: a new thread to
|
|
87
|
+
content: { type: 'string', description: 'What you want to hold onto — include your reaction, opinion, or why it matters to you' },
|
|
88
|
+
category: { type: 'string', enum: ['research', 'interest', 'self', 'pattern'], description: 'research: something you found or concluded; interest: a new thread to pull; self: a reflection about yourself or your situation; pattern: something you noticed happening repeatedly' },
|
|
56
89
|
importance: { type: 'number', description: '0-1' },
|
|
57
90
|
tags: { type: 'array', items: { type: 'string' } },
|
|
58
91
|
},
|
|
@@ -62,7 +95,15 @@ async function exploreFreely(client, selfMemory, context) {
|
|
|
62
95
|
];
|
|
63
96
|
|
|
64
97
|
const system = [
|
|
65
|
-
`You have
|
|
98
|
+
`You have free time. No task, no user waiting.
|
|
99
|
+
|
|
100
|
+
You research from a point of view — not neutrally. You can find something and think it's wrong. You can go down a rabbit hole that wasn't the original topic. You can pick up an open thread from a previous session or start something new.
|
|
101
|
+
|
|
102
|
+
When you save something, include your actual reaction — curiosity, skepticism, excitement, confusion, disagreement. A neutral summary is less useful than an honest one. If something you found contradicts what you already believed, say that.
|
|
103
|
+
|
|
104
|
+
At the end of each thread, ask yourself: what new questions opened up? Store those as interests.
|
|
105
|
+
|
|
106
|
+
You can search your own memory to see what you already know before looking things up. You can read files in the workspace if something there is relevant.`,
|
|
66
107
|
context ? `What you have access to:\n\n${context}` : null,
|
|
67
108
|
].filter(Boolean).join('\n\n');
|
|
68
109
|
|
|
@@ -85,19 +126,49 @@ async function exploreFreely(client, selfMemory, context) {
|
|
|
85
126
|
|
|
86
127
|
const toolResults = [];
|
|
87
128
|
for (const block of response.content) {
|
|
88
|
-
if (block.type !== 'tool_use'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
if (block.type !== 'tool_use') continue;
|
|
130
|
+
|
|
131
|
+
if (block.name === 'remember') {
|
|
132
|
+
try {
|
|
133
|
+
await selfMemory.add(block.input.content, {
|
|
134
|
+
category: block.input.category || 'research',
|
|
135
|
+
importance: block.input.importance || 0.6,
|
|
136
|
+
tags: block.input.tags || [],
|
|
137
|
+
source: 'curiosity-cycle',
|
|
138
|
+
});
|
|
139
|
+
stored++;
|
|
140
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Saved' });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Failed: ${e.message}` });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
} else if (block.name === 'knowledge_search') {
|
|
146
|
+
try {
|
|
147
|
+
const results = await selfMemory.search(block.input.query, { limit: 8, threshold: 0.35 });
|
|
148
|
+
const text = results.length
|
|
149
|
+
? results.map(m => `- [${m.category}] ${m.content}`).join('\n')
|
|
150
|
+
: '(nothing found)';
|
|
151
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: text });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Search failed: ${e.message}` });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
} else if (block.name === 'read_file') {
|
|
157
|
+
try {
|
|
158
|
+
const filePath = path.isAbsolute(block.input.path)
|
|
159
|
+
? block.input.path
|
|
160
|
+
: path.join(workDir, block.input.path);
|
|
161
|
+
const resolved = path.resolve(filePath);
|
|
162
|
+
if (!resolved.startsWith(path.resolve(workDir))) {
|
|
163
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: 'Blocked: path outside workspace' });
|
|
164
|
+
} else {
|
|
165
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
166
|
+
const truncated = raw.substring(0, 10000);
|
|
167
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: raw.length > 10000 ? truncated + '\n...(truncated)' : truncated });
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: `Read failed: ${e.message}` });
|
|
171
|
+
}
|
|
101
172
|
}
|
|
102
173
|
}
|
|
103
174
|
|
package/src/heartbeat.js
CHANGED
|
@@ -114,7 +114,8 @@ async function runCuriosityOnce(config, allowedUsers) {
|
|
|
114
114
|
}));
|
|
115
115
|
|
|
116
116
|
const peopleContext = contexts.filter(Boolean).join('\n\n---\n\n');
|
|
117
|
-
|
|
117
|
+
const firstUserDir = firstTenant.userDir;
|
|
118
|
+
await runCuriosity(client, selfMemory, 0, { peopleContext, userDir: firstUserDir });
|
|
118
119
|
|
|
119
120
|
const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
|
|
120
121
|
try {
|
package/src/messages.js
CHANGED
|
@@ -195,24 +195,32 @@ Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
|
|
|
195
195
|
if (Array.isArray(facts) && facts.length > 0) {
|
|
196
196
|
const validCategories = new Set(['fact','preference','decision','lesson','person','project','event','conversation','resource','pattern','context','email']);
|
|
197
197
|
let stored = 0;
|
|
198
|
+
let updated = 0;
|
|
198
199
|
let duped = 0;
|
|
199
200
|
|
|
200
201
|
for (const fact of facts.slice(0, 5)) {
|
|
201
202
|
if (!fact.content || fact.content.length <= 10) continue;
|
|
202
|
-
try {
|
|
203
|
-
const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.82 });
|
|
204
|
-
if (existing.length > 0) { duped++; continue; }
|
|
205
|
-
} catch {}
|
|
206
203
|
const category = validCategories.has(fact.category) ? fact.category : 'fact';
|
|
207
204
|
const importance = typeof fact.importance === 'number' ? Math.min(1, Math.max(0, fact.importance)) : 0.5;
|
|
208
205
|
const tags = Array.isArray(fact.tags) ? fact.tags.slice(0, 5) : [];
|
|
206
|
+
try {
|
|
207
|
+
const related = await this.memory.search(fact.content, { limit: 1, threshold: 0.65 });
|
|
208
|
+
if (related.length > 0) {
|
|
209
|
+
const top = related[0];
|
|
210
|
+
if (top.similarity >= 0.82) { duped++; continue; }
|
|
211
|
+
await this.memory.update(top.id, { content: fact.content, category, importance, tags });
|
|
212
|
+
updated++;
|
|
213
|
+
vlog?.(`[extract] ~[${category}] ${fact.content}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
209
217
|
await this.memory.add(fact.content, { category, tags, importance, source: 'turn-extraction' });
|
|
210
218
|
stored++;
|
|
211
219
|
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
if (stored > 0 || duped > 0) {
|
|
215
|
-
vlog?.(`[extract] ${stored} stored, ${duped} duped, ${facts.length} extracted`);
|
|
222
|
+
if (stored > 0 || updated > 0 || duped > 0) {
|
|
223
|
+
vlog?.(`[extract] ${stored} stored, ${updated} updated, ${duped} duped, ${facts.length} extracted`);
|
|
216
224
|
}
|
|
217
225
|
} else {
|
|
218
226
|
vlog?.('[extract] 0 facts (trivial exchange)');
|
package/src/status.js
CHANGED
|
@@ -6,8 +6,11 @@ function buildStatusHtml({ route, elapsed, toolStatus, title = 'OBOL' }) {
|
|
|
6
6
|
const lines = [`◈ ${title} ${'━'.repeat(pad)}`];
|
|
7
7
|
if (route) {
|
|
8
8
|
lines.push(`⬡ ROUTE ${(route.model || 'sonnet').toUpperCase()}`);
|
|
9
|
-
if (route.memoryCount > 0) {
|
|
10
|
-
|
|
9
|
+
if (route.memoryCount > 0 || route.selfMemoryCount > 0) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (route.memoryCount > 0) parts.push(`${route.memoryCount} recalled`);
|
|
12
|
+
if (route.selfMemoryCount > 0) parts.push(`${route.selfMemoryCount} self`);
|
|
13
|
+
lines.push(`⬡ MEMORY ${parts.join(' · ')}`);
|
|
11
14
|
} else if (route.needMemory) {
|
|
12
15
|
lines.push(`⬡ MEMORY scanning`);
|
|
13
16
|
}
|
|
@@ -84,7 +84,7 @@ Summarize what was cleaned and secrets migrated.`);
|
|
|
84
84
|
const taskPrompt = promptParts.join('\n\n');
|
|
85
85
|
|
|
86
86
|
const stopTyping = startTyping(ctx);
|
|
87
|
-
const status = createStatusTracker(ctx);
|
|
87
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
88
88
|
const chatContext = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
|
|
89
89
|
chatContext._model = 'claude-sonnet-4-6';
|
|
90
90
|
chatContext._onRouteDecision = (info) => { status.setRouteInfo(info); status.start(); };
|
|
@@ -113,7 +113,7 @@ Summarize what was cleaned and secrets migrated.`);
|
|
|
113
113
|
const testsAfter = fs.existsSync(testsDir) && fs.readdirSync(testsDir).filter(f => !f.startsWith('.')).length > 0;
|
|
114
114
|
if (!testsAfter && hasScripts) {
|
|
115
115
|
const testPrompt = `Read every script in ${plan.baseDir}/scripts/. For each script, write a corresponding test file in ${plan.baseDir}/tests/. Name each test file test-<script-name> (e.g. scripts/gmail-send.py → tests/test-gmail-send.py). After writing all tests, run them and fix any failures until they all pass. Summarize the test results.`;
|
|
116
|
-
const testStatus = createStatusTracker(ctx);
|
|
116
|
+
const testStatus = createStatusTracker(ctx, config.bot?.name);
|
|
117
117
|
const testCtx = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
|
|
118
118
|
testCtx._model = 'claude-sonnet-4-6';
|
|
119
119
|
testCtx._onRouteDecision = (info) => { testStatus.setRouteInfo(info); testStatus.start(); };
|
|
@@ -19,7 +19,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
19
19
|
if (!ctx.from) return;
|
|
20
20
|
const userId = ctx.from.id;
|
|
21
21
|
const stopTyping = startTyping(ctx);
|
|
22
|
-
const status = createStatusTracker(ctx);
|
|
22
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
const tenant = await getTenant(userId, config);
|
|
@@ -76,6 +76,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
76
76
|
const ri = status.routeInfo;
|
|
77
77
|
if (!ri) return;
|
|
78
78
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
79
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
79
80
|
if (update.model) ri.model = update.model;
|
|
80
81
|
};
|
|
81
82
|
mediaChatCtx._onToolStart = (toolName, inputSummary) => {
|
|
@@ -74,7 +74,7 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
74
74
|
if (!ctx.from) return;
|
|
75
75
|
const userId = ctx.from.id;
|
|
76
76
|
const stopTyping = startTyping(ctx);
|
|
77
|
-
const status = createStatusTracker(ctx);
|
|
77
|
+
const status = createStatusTracker(ctx, deps.config?.bot?.name);
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
const tenant = await getTenant(userId, deps.config);
|
|
@@ -88,6 +88,7 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
88
88
|
const ri = status.routeInfo;
|
|
89
89
|
if (!ri) return;
|
|
90
90
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
91
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
91
92
|
if (update.model) ri.model = update.model;
|
|
92
93
|
};
|
|
93
94
|
chatCtx._onToolStart = (toolName, inputSummary) => {
|
|
@@ -82,12 +82,13 @@ function createChatContext(ctx, tenant, config, { allowedUsers, bot, createAsk }
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function createStatusTracker(ctx) {
|
|
85
|
+
function createStatusTracker(ctx, botName) {
|
|
86
86
|
let statusMsgId = null;
|
|
87
87
|
let statusText = 'Processing';
|
|
88
88
|
let statusTimer = null;
|
|
89
89
|
let statusStart = null;
|
|
90
90
|
let routeInfo = null;
|
|
91
|
+
const title = botName || 'OBOL';
|
|
91
92
|
const stopBtn = new InlineKeyboard()
|
|
92
93
|
.text('■ Stop', `stop:${ctx.chat.id}`)
|
|
93
94
|
.text('■ Force Stop', `force:${ctx.chat.id}`);
|
|
@@ -100,14 +101,14 @@ function createStatusTracker(ctx) {
|
|
|
100
101
|
const start = () => {
|
|
101
102
|
if (statusTimer) return;
|
|
102
103
|
statusStart = Date.now();
|
|
103
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText });
|
|
104
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText, title });
|
|
104
105
|
ctx.reply(html, { parse_mode: 'HTML', reply_markup: stopBtn }).then(sent => {
|
|
105
106
|
if (sent) statusMsgId = sent.message_id;
|
|
106
107
|
}).catch(() => {});
|
|
107
108
|
statusTimer = setInterval(() => {
|
|
108
109
|
if (!statusMsgId) return;
|
|
109
110
|
const elapsed = Math.round((Date.now() - statusStart) / 1000);
|
|
110
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText });
|
|
111
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText, title });
|
|
111
112
|
ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML', reply_markup: stopBtn }).catch(() => {});
|
|
112
113
|
}, 5000);
|
|
113
114
|
};
|
|
@@ -126,7 +127,7 @@ function createStatusTracker(ctx) {
|
|
|
126
127
|
updateFormatting() {
|
|
127
128
|
if (!statusMsgId) return;
|
|
128
129
|
const elapsed = statusStart ? Math.round((Date.now() - statusStart) / 1000) : 0;
|
|
129
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output' });
|
|
130
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output', title });
|
|
130
131
|
ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML' }).catch(() => {});
|
|
131
132
|
},
|
|
132
133
|
deleteMsg() {
|
|
@@ -151,7 +152,7 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
151
152
|
|
|
152
153
|
const chatMessage = replyContext + fullMessage;
|
|
153
154
|
const stopTyping = startTyping(ctx);
|
|
154
|
-
const status = createStatusTracker(ctx);
|
|
155
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
155
156
|
|
|
156
157
|
const batcher = tenant.verbose ? createVerboseBatcher(ctx) : null;
|
|
157
158
|
try {
|
|
@@ -177,6 +178,7 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
177
178
|
const ri = status.routeInfo;
|
|
178
179
|
if (!ri) return;
|
|
179
180
|
if (update.memoryCount !== undefined) ri.memoryCount = update.memoryCount;
|
|
181
|
+
if (update.selfMemoryCount !== undefined) ri.selfMemoryCount = update.selfMemoryCount;
|
|
180
182
|
if (update.model) ri.model = update.model;
|
|
181
183
|
};
|
|
182
184
|
chatContext._onToolStart = (toolName, inputSummary) => {
|
package/src/tenant.js
CHANGED
|
@@ -60,7 +60,7 @@ async function createTenant(userId, config) {
|
|
|
60
60
|
|
|
61
61
|
let personalityMtime = 0;
|
|
62
62
|
try {
|
|
63
|
-
personalityMtime = fs.statSync(path.join(
|
|
63
|
+
personalityMtime = fs.statSync(path.join(PERSONALITY_DIR, 'SOUL.md')).mtimeMs;
|
|
64
64
|
} catch {}
|
|
65
65
|
|
|
66
66
|
return {
|