obol-ai 0.2.39 → 0.3.0

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.
@@ -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(dir) {
8
- dir = dir || path.join(OBOL_DIR, 'personality');
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 files = {
12
- soul: 'SOUL.md',
13
- user: 'USER.md',
14
- agents: 'AGENTS.md',
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(dir);
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 { EVOLUTION_IDLE_MS, TEXT_BUFFER_GAP_MS, TEXT_BUFFER_MAX_PARTS, TEXT_BUFFER_MAX_CHARS, TEXT_BUFFER_THRESHOLD } = require('../constants');
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 personalityDir = path.join(userDir, 'personality');
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(personalityDir, 'SOUL.md');
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();