obol-ai 0.2.39 → 0.3.1
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 +19 -0
- package/package.json +1 -1
- package/src/analysis.js +191 -0
- package/src/background.js +15 -7
- package/src/claude/chat.js +3 -2
- package/src/claude/prompt.js +63 -6
- package/src/claude/tool-registry.js +10 -0
- package/src/claude/tools/knowledge.js +107 -0
- package/src/curiosity-dispatch.js +136 -0
- package/src/curiosity-humor.js +137 -0
- package/src/curiosity.js +112 -0
- package/src/db/migrate.js +98 -0
- package/src/evolve/check.js +13 -21
- package/src/evolve/evolve.js +49 -20
- package/src/evolve/index.js +2 -2
- package/src/heartbeat.js +224 -2
- package/src/index.js +11 -0
- package/src/memory-self.js +123 -0
- package/src/messages.js +45 -48
- package/src/patterns.js +111 -0
- package/src/personality.js +9 -10
- package/src/soul.js +53 -0
- package/src/telegram/constants.js +0 -2
- package/src/telegram/handlers/text.js +1 -58
- package/src/tenant.js +10 -7
package/src/messages.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tier 1: obol_messages — raw log, every message
|
|
5
5
|
* Tier 2: obol_memory — curated facts, extracted after every assistant turn
|
|
6
|
+
* Tier 3: obol_events — follow-up intents, detected by batch analysis (see analysis.js)
|
|
7
|
+
* Tier 4: obol_user_patterns — synthesized behavioral patterns, refreshed by batch analysis (see analysis.js)
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
@@ -24,6 +26,7 @@ class MessageLog {
|
|
|
24
26
|
this.exchangeCount = new Map();
|
|
25
27
|
this._lastUserMessage = new Map();
|
|
26
28
|
this._verboseCallbacks = new Map();
|
|
29
|
+
this._lastActivity = new Map();
|
|
27
30
|
this._cleanup = setInterval(() => {
|
|
28
31
|
const now = Date.now();
|
|
29
32
|
for (const [key] of this.exchangeCount) {
|
|
@@ -35,7 +38,6 @@ class MessageLog {
|
|
|
35
38
|
}
|
|
36
39
|
}, 600000);
|
|
37
40
|
this._cleanup.unref();
|
|
38
|
-
this._lastActivity = new Map();
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
async log(chatId, role, content, opts = {}) {
|
|
@@ -71,17 +73,9 @@ class MessageLog {
|
|
|
71
73
|
if (lastUser) {
|
|
72
74
|
this._extractFacts(chatId, lastUser, truncated).catch(() => {});
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
const { checkEvolution } = require('./evolve');
|
|
76
|
-
checkEvolution(this.userDir, this).then(result => {
|
|
77
|
-
if (result?.ready && !this._evolutionReady && !this._evolutionPending) this._evolutionReady = true;
|
|
78
|
-
}).catch(() => {});
|
|
79
76
|
}
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/**
|
|
83
|
-
* Get recent messages for context loading on boot
|
|
84
|
-
*/
|
|
85
79
|
async getRecent(chatId, limit = 50) {
|
|
86
80
|
try {
|
|
87
81
|
const res = await fetch(
|
|
@@ -89,7 +83,7 @@ class MessageLog {
|
|
|
89
83
|
{ headers: this.headers }
|
|
90
84
|
);
|
|
91
85
|
const data = await res.json();
|
|
92
|
-
const rows = data.reverse();
|
|
86
|
+
const rows = data.reverse();
|
|
93
87
|
const firstUserIdx = rows.findIndex(r => r.role === 'user');
|
|
94
88
|
return firstUserIdx > 0 ? rows.slice(firstUserIdx) : rows;
|
|
95
89
|
} catch {
|
|
@@ -97,9 +91,20 @@ class MessageLog {
|
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
async getSince(chatId, since, limit = 500) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(
|
|
97
|
+
`${this.url}/rest/v1/obol_messages?chat_id=eq.${chatId}&created_at=gte.${since.toISOString()}&order=created_at.asc&limit=${limit}&select=role,content,created_at`,
|
|
98
|
+
{ headers: this.headers }
|
|
99
|
+
);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (!res.ok) return [];
|
|
102
|
+
return data;
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
async getByDate(chatId, dateStr, opts = {}) {
|
|
104
109
|
const { start, end } = parseDateRange(dateStr);
|
|
105
110
|
const limit = opts.limit || 50;
|
|
@@ -165,13 +170,12 @@ class MessageLog {
|
|
|
165
170
|
const response = await client.messages.create({
|
|
166
171
|
model: 'claude-haiku-4-5-20251001',
|
|
167
172
|
max_tokens: 1024,
|
|
168
|
-
system: `Extract
|
|
173
|
+
system: `Extract facts from this exchange.
|
|
169
174
|
|
|
175
|
+
FACTS (0-5 atomic facts worth remembering long-term):
|
|
170
176
|
Store: personal details, preferences, decisions, projects, plans, people mentioned, technical details, events, deadlines, emotional context, resources.
|
|
171
|
-
Skip: greetings, acknowledgments, content-free exchanges.
|
|
172
|
-
|
|
173
|
-
Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.
|
|
174
|
-
Keep each fact atomic — one idea per entry.`,
|
|
177
|
+
Skip: greetings, acknowledgments, content-free exchanges.
|
|
178
|
+
Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
|
|
175
179
|
tools: extractTool,
|
|
176
180
|
tool_choice: { type: 'tool', name: 'save_memory' },
|
|
177
181
|
messages: [{ role: 'user', content: `Human: ${userMsg.substring(0, 2000)}\nAssistant: ${assistantMsg.substring(0, 2000)}` }],
|
|
@@ -181,38 +185,31 @@ Keep each fact atomic — one idea per entry.`,
|
|
|
181
185
|
if (!toolUse) return;
|
|
182
186
|
|
|
183
187
|
const facts = toolUse.input?.facts;
|
|
184
|
-
if (!Array.isArray(facts)) return;
|
|
185
|
-
|
|
186
|
-
if (facts.length === 0) {
|
|
187
|
-
vlog?.('[extract] 0 facts (trivial exchange)');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
category,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
210
|
-
stored++;
|
|
211
|
-
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
212
|
-
}
|
|
189
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
190
|
+
const validCategories = new Set(['fact','preference','decision','lesson','person','project','event','conversation','resource','pattern','context','email']);
|
|
191
|
+
let stored = 0;
|
|
192
|
+
let duped = 0;
|
|
193
|
+
|
|
194
|
+
for (const fact of facts.slice(0, 5)) {
|
|
195
|
+
if (!fact.content || fact.content.length <= 10) continue;
|
|
196
|
+
try {
|
|
197
|
+
const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.92 });
|
|
198
|
+
if (existing.length > 0) { duped++; continue; }
|
|
199
|
+
} catch {}
|
|
200
|
+
const category = validCategories.has(fact.category) ? fact.category : 'fact';
|
|
201
|
+
const importance = typeof fact.importance === 'number' ? Math.min(1, Math.max(0, fact.importance)) : 0.5;
|
|
202
|
+
const tags = Array.isArray(fact.tags) ? fact.tags.slice(0, 5) : [];
|
|
203
|
+
await this.memory.add(fact.content, { category, tags, importance, source: 'turn-extraction' });
|
|
204
|
+
stored++;
|
|
205
|
+
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
206
|
+
}
|
|
213
207
|
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
if (stored > 0 || duped > 0) {
|
|
209
|
+
vlog?.(`[extract] ${stored} stored, ${duped} duped, ${facts.length} extracted`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
vlog?.('[extract] 0 facts (trivial exchange)');
|
|
216
213
|
}
|
|
217
214
|
} catch (e) {
|
|
218
215
|
console.error('[extract] Failed:', e.message);
|
package/src/patterns.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const VALID_DIMENSIONS = new Set(['timing', 'mood', 'humor', 'engagement', 'communication', 'topics']);
|
|
2
|
+
|
|
3
|
+
async function createPatterns(supabaseConfig, userId) {
|
|
4
|
+
const { url, serviceKey } = supabaseConfig;
|
|
5
|
+
|
|
6
|
+
const headers = {
|
|
7
|
+
'apikey': serviceKey,
|
|
8
|
+
'Authorization': `Bearer ${serviceKey}`,
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'Prefer': 'return=representation',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function upsert(key, dimension, summary, data = {}, confidence = 0.5) {
|
|
14
|
+
if (!VALID_DIMENSIONS.has(dimension)) throw new Error(`Invalid dimension: ${dimension}`);
|
|
15
|
+
|
|
16
|
+
const res = await fetch(`${url}/rest/v1/obol_user_patterns`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { ...headers, 'Prefer': 'resolution=merge-duplicates,return=representation' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
user_id: userId,
|
|
21
|
+
key,
|
|
22
|
+
dimension,
|
|
23
|
+
summary,
|
|
24
|
+
data,
|
|
25
|
+
confidence,
|
|
26
|
+
updated_at: new Date().toISOString(),
|
|
27
|
+
observation_count: 1,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const result = await res.json();
|
|
31
|
+
if (!res.ok) throw new Error(JSON.stringify(result));
|
|
32
|
+
return result[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function incrementObservation(key, dimension, summary, data = {}, confidence = 0.5) {
|
|
36
|
+
const existing = await get(key);
|
|
37
|
+
const count = (existing?.observation_count || 0) + 1;
|
|
38
|
+
|
|
39
|
+
const res = await fetch(`${url}/rest/v1/obol_user_patterns`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { ...headers, 'Prefer': 'resolution=merge-duplicates,return=representation' },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
user_id: userId,
|
|
44
|
+
key,
|
|
45
|
+
dimension,
|
|
46
|
+
summary,
|
|
47
|
+
data,
|
|
48
|
+
confidence,
|
|
49
|
+
observation_count: count,
|
|
50
|
+
updated_at: new Date().toISOString(),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
const result = await res.json();
|
|
54
|
+
if (!res.ok) throw new Error(JSON.stringify(result));
|
|
55
|
+
return result[0];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function get(key) {
|
|
59
|
+
const res = await fetch(
|
|
60
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&key=eq.${encodeURIComponent(key)}&limit=1`,
|
|
61
|
+
{ headers }
|
|
62
|
+
);
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
65
|
+
return data[0] || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getAll() {
|
|
69
|
+
const res = await fetch(
|
|
70
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&order=dimension.asc,updated_at.desc`,
|
|
71
|
+
{ headers }
|
|
72
|
+
);
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getByDimension(dimension) {
|
|
79
|
+
const res = await fetch(
|
|
80
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&dimension=eq.${dimension}&order=updated_at.desc`,
|
|
81
|
+
{ headers }
|
|
82
|
+
);
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function remove(key) {
|
|
89
|
+
await fetch(
|
|
90
|
+
`${url}/rest/v1/obol_user_patterns?user_id=eq.${userId}&key=eq.${encodeURIComponent(key)}`,
|
|
91
|
+
{ method: 'DELETE', headers: { ...headers, 'Prefer': 'return=minimal' } }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function format() {
|
|
96
|
+
const all = await getAll();
|
|
97
|
+
if (!all.length) return null;
|
|
98
|
+
const byDimension = {};
|
|
99
|
+
for (const p of all) {
|
|
100
|
+
if (!byDimension[p.dimension]) byDimension[p.dimension] = [];
|
|
101
|
+
byDimension[p.dimension].push(p.summary);
|
|
102
|
+
}
|
|
103
|
+
return Object.entries(byDimension)
|
|
104
|
+
.map(([dim, summaries]) => `[${dim}]\n${summaries.map(s => `- ${s}`).join('\n')}`)
|
|
105
|
+
.join('\n\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { upsert, incrementObservation, get, getAll, getByDimension, remove, format };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { createPatterns };
|
package/src/personality.js
CHANGED
|
@@ -4,17 +4,16 @@ const { OBOL_DIR } = require('./config');
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TRAITS = require('./defaults/traits.json');
|
|
6
6
|
|
|
7
|
-
function loadPersonality(
|
|
8
|
-
|
|
7
|
+
function loadPersonality(sharedDir, userDir) {
|
|
8
|
+
sharedDir = sharedDir || path.join(OBOL_DIR, 'personality');
|
|
9
|
+
userDir = userDir || sharedDir;
|
|
9
10
|
const personality = {};
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
soul
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
for (const [key, filename] of Object.entries(files)) {
|
|
12
|
+
for (const [key, filename, dir] of [
|
|
13
|
+
['soul', 'SOUL.md', sharedDir],
|
|
14
|
+
['agents', 'AGENTS.md', userDir],
|
|
15
|
+
['user', 'USER.md', userDir],
|
|
16
|
+
]) {
|
|
18
17
|
const filepath = path.join(dir, filename);
|
|
19
18
|
try {
|
|
20
19
|
personality[key] = fs.readFileSync(filepath, 'utf-8');
|
|
@@ -23,7 +22,7 @@ function loadPersonality(dir) {
|
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
personality.traits = loadTraits(
|
|
25
|
+
personality.traits = loadTraits(userDir);
|
|
27
26
|
|
|
28
27
|
return personality;
|
|
29
28
|
}
|
package/src/soul.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OBOL_DIR } = require('./config');
|
|
4
|
+
|
|
5
|
+
const PERSONALITY_DIR = path.join(OBOL_DIR, 'personality');
|
|
6
|
+
|
|
7
|
+
function makeHeaders(supabaseConfig) {
|
|
8
|
+
return {
|
|
9
|
+
'apikey': supabaseConfig.serviceKey,
|
|
10
|
+
'Authorization': `Bearer ${supabaseConfig.serviceKey}`,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'Prefer': 'return=minimal',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function backup(supabaseConfig, key, content) {
|
|
17
|
+
if (!supabaseConfig?.url || !supabaseConfig?.serviceKey) return;
|
|
18
|
+
await fetch(`${supabaseConfig.url}/rest/v1/obol_soul`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { ...makeHeaders(supabaseConfig), 'Prefer': 'resolution=merge-duplicates,return=minimal' },
|
|
21
|
+
body: JSON.stringify({ id: key, content, updated_at: new Date().toISOString() }),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function restore(supabaseConfig, key) {
|
|
26
|
+
if (!supabaseConfig?.url || !supabaseConfig?.serviceKey) return null;
|
|
27
|
+
const res = await fetch(`${supabaseConfig.url}/rest/v1/obol_soul?id=eq.${key}&select=content`, {
|
|
28
|
+
headers: makeHeaders(supabaseConfig),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) return null;
|
|
31
|
+
const rows = await res.json();
|
|
32
|
+
return rows?.[0]?.content || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function restoreIfMissing(supabaseConfig) {
|
|
36
|
+
if (!supabaseConfig?.url || !supabaseConfig?.serviceKey) return;
|
|
37
|
+
fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
40
|
+
if (!fs.existsSync(soulPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const content = await restore(supabaseConfig, 'soul');
|
|
43
|
+
if (content) {
|
|
44
|
+
fs.writeFileSync(soulPath, content);
|
|
45
|
+
console.log(' [soul] Restored SOUL.md from Supabase');
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(` [soul] Failed to restore SOUL.md: ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { backup, restore, restoreIfMissing, PERSONALITY_DIR };
|
|
@@ -3,7 +3,6 @@ const { TERM_WIDTH } = require('../status');
|
|
|
3
3
|
const RATE_LIMIT_MS = 3000;
|
|
4
4
|
const SPAM_THRESHOLD = 5;
|
|
5
5
|
const SPAM_COOLDOWN_MS = 30000;
|
|
6
|
-
const EVOLUTION_IDLE_MS = 15 * 60 * 1000;
|
|
7
6
|
const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
8
7
|
const DEDUP_MAX_SIZE = 2000;
|
|
9
8
|
const TEXT_BUFFER_GAP_MS = 1500;
|
|
@@ -18,7 +17,6 @@ module.exports = {
|
|
|
18
17
|
RATE_LIMIT_MS,
|
|
19
18
|
SPAM_THRESHOLD,
|
|
20
19
|
SPAM_COOLDOWN_MS,
|
|
21
|
-
EVOLUTION_IDLE_MS,
|
|
22
20
|
DEDUP_TTL_MS,
|
|
23
21
|
DEDUP_MAX_SIZE,
|
|
24
22
|
TEXT_BUFFER_GAP_MS,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
const { InlineKeyboard } = require('grammy');
|
|
2
2
|
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { evolve, loadEvolutionState } = require('../../evolve');
|
|
4
3
|
const { buildStatusHtml, describeToolCall } = require('../../status');
|
|
5
4
|
const { sendHtml, startTyping, splitMessage } = require('../utils');
|
|
6
|
-
const {
|
|
5
|
+
const { TEXT_BUFFER_GAP_MS, TEXT_BUFFER_MAX_PARTS, TEXT_BUFFER_MAX_CHARS, TEXT_BUFFER_THRESHOLD } = require('../constants');
|
|
7
6
|
|
|
8
|
-
const _evolutionTimers = new Map();
|
|
9
7
|
const textBuffers = new Map();
|
|
10
8
|
const VERBOSE_FLUSH_MS = 2000;
|
|
11
9
|
|
|
@@ -141,12 +139,6 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
141
139
|
const userId = ctx.from.id;
|
|
142
140
|
const tenant = await getTenant(userId, config);
|
|
143
141
|
|
|
144
|
-
if (_evolutionTimers.has(userId)) {
|
|
145
|
-
clearTimeout(_evolutionTimers.get(userId));
|
|
146
|
-
_evolutionTimers.delete(userId);
|
|
147
|
-
if (tenant.messageLog) tenant.messageLog._evolutionPending = false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
142
|
let replyContext = '';
|
|
151
143
|
const reply = ctx.message?.reply_to_message;
|
|
152
144
|
if (reply) {
|
|
@@ -214,55 +206,6 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
214
206
|
|
|
215
207
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response, { model, tokensIn: usage?.input_tokens, tokensOut: usage?.output_tokens });
|
|
216
208
|
|
|
217
|
-
if (tenant.messageLog?._evolutionReady && !_evolutionTimers.has(userId)) {
|
|
218
|
-
tenant.messageLog._evolutionReady = false;
|
|
219
|
-
tenant.messageLog._evolutionPending = true;
|
|
220
|
-
const timer = setTimeout(async () => {
|
|
221
|
-
_evolutionTimers.delete(userId);
|
|
222
|
-
try {
|
|
223
|
-
const result = await evolve(tenant.claude.client, tenant.messageLog, tenant.memory, tenant.userDir);
|
|
224
|
-
tenant.claude.reloadPersonality?.();
|
|
225
|
-
let msg = `🪙 Evolution #${result.evolutionNumber} complete.`;
|
|
226
|
-
|
|
227
|
-
if (result.scriptsFixed) {
|
|
228
|
-
msg += '\n🔧 Fixed a test regression automatically.';
|
|
229
|
-
} else if (result.scriptsRolledBack) {
|
|
230
|
-
msg += '\n⚠️ Rolled back a script refactor — tests couldn\'t be fixed.';
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (result.upgrades && result.upgrades.length > 0) {
|
|
234
|
-
msg += '\n\n🆕 **New capabilities:**';
|
|
235
|
-
for (const u of result.upgrades) {
|
|
236
|
-
msg += `\n• **${u.name}** — ${u.description}`;
|
|
237
|
-
if (u.command) msg += ` → \`${u.command}\``;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (result.deployedApps && result.deployedApps.length > 0) {
|
|
242
|
-
msg += '\n\n🚀 **Deployed:**';
|
|
243
|
-
for (const app of result.deployedApps) {
|
|
244
|
-
if (app.url) {
|
|
245
|
-
msg += `\n• ${app.name} → ${app.url}`;
|
|
246
|
-
} else if (app.error) {
|
|
247
|
-
msg += `\n• ${app.name} — deploy failed: ${app.error.substring(0, 100)}`;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (result.changelog) {
|
|
253
|
-
msg += `\n\n_${result.changelog}_`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await sendHtml(ctx, msg).catch(() => {});
|
|
257
|
-
} catch (e) {
|
|
258
|
-
console.error('Evolution failed:', e.message);
|
|
259
|
-
} finally {
|
|
260
|
-
tenant.messageLog._evolutionPending = false;
|
|
261
|
-
}
|
|
262
|
-
}, EVOLUTION_IDLE_MS);
|
|
263
|
-
_evolutionTimers.set(userId, timer);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
209
|
stopTyping();
|
|
267
210
|
|
|
268
211
|
if (response.length > 4096) {
|
package/src/tenant.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const { ensureUserDir } = require('./config');
|
|
2
|
+
const { PERSONALITY_DIR } = require('./soul');
|
|
2
3
|
const { loadPersonality } = require('./personality');
|
|
3
4
|
const { createMemory } = require('./memory');
|
|
5
|
+
const { createSelfMemory } = require('./memory-self');
|
|
6
|
+
const { createPatterns } = require('./patterns');
|
|
4
7
|
const { createClaude } = require('./claude');
|
|
5
8
|
const { createMessageLog } = require('./messages');
|
|
6
9
|
const { BackgroundRunner } = require('./background');
|
|
@@ -30,13 +33,14 @@ _tenantCleanup.unref();
|
|
|
30
33
|
|
|
31
34
|
async function createTenant(userId, config) {
|
|
32
35
|
const userDir = ensureUserDir(userId);
|
|
33
|
-
const
|
|
34
|
-
const personality = loadPersonality(personalityDir);
|
|
36
|
+
const personality = loadPersonality(PERSONALITY_DIR, path.join(userDir, 'personality'));
|
|
35
37
|
const memory = config.supabase ? await createMemory(config.supabase, userId) : null;
|
|
38
|
+
const selfMemory = config.supabase ? await createSelfMemory(config.supabase, userId) : null;
|
|
39
|
+
const patterns = config.supabase ? await createPatterns(config.supabase, userId) : null;
|
|
36
40
|
const bridgeEnabled = isBridgeEnabled(config) && (config.telegram?.allowedUsers?.length || 0) >= 2;
|
|
37
|
-
const claude = createClaude(config.anthropic, { personality, memory, userDir, bridgeEnabled, botName: config.bot?.name });
|
|
38
|
-
const messageLog = config.supabase ? createMessageLog(config.supabase, memory, config.anthropic, userId, userDir) : null;
|
|
41
|
+
const claude = createClaude(config.anthropic, { personality, memory, selfMemory, userDir, bridgeEnabled, botName: config.bot?.name });
|
|
39
42
|
const scheduler = config.supabase ? createScheduler(config.supabase, userId) : null;
|
|
43
|
+
const messageLog = config.supabase ? createMessageLog(config.supabase, memory, config.anthropic, userId, userDir) : null;
|
|
40
44
|
const toolPrefsApi = config.supabase ? createToolPrefs(config.supabase, userId) : null;
|
|
41
45
|
const bg = new BackgroundRunner();
|
|
42
46
|
|
|
@@ -60,7 +64,7 @@ async function createTenant(userId, config) {
|
|
|
60
64
|
} catch {}
|
|
61
65
|
|
|
62
66
|
return {
|
|
63
|
-
claude, memory, messageLog, personality, scheduler, bg, userDir, userId,
|
|
67
|
+
claude, memory, selfMemory, patterns, messageLog, personality, scheduler, bg, userDir, userId,
|
|
64
68
|
toolPrefs,
|
|
65
69
|
toolPrefsApi,
|
|
66
70
|
async reloadToolPrefs() {
|
|
@@ -78,9 +82,8 @@ async function getTenant(userId, config) {
|
|
|
78
82
|
if (tenants.has(userId)) {
|
|
79
83
|
const tenant = tenants.get(userId);
|
|
80
84
|
if (Date.now() - (tenant._personalityLoadedAt || 0) > PERSONALITY_CACHE_TTL) {
|
|
81
|
-
const personalityDir = path.join(tenant.userDir, 'personality');
|
|
82
85
|
try {
|
|
83
|
-
const soulPath = path.join(
|
|
86
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
84
87
|
const mtime = fs.statSync(soulPath).mtimeMs;
|
|
85
88
|
if (mtime > (tenant._personalityMtime || 0)) {
|
|
86
89
|
tenant.claude.reloadPersonality();
|