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.
@@ -18,13 +18,14 @@ const MODELS = {
18
18
  };
19
19
  const MAX_FIX_ATTEMPTS = 1;
20
20
 
21
- async function evolve(claudeClient, messageLog, memory, userDir) {
21
+ async function evolve(claudeClient, messageLog, memory, userDir, supabaseConfig = null, selfMemory = null) {
22
+ const { PERSONALITY_DIR } = require('../soul');
22
23
  const baseDir = userDir || OBOL_DIR;
23
24
  const state = loadEvolutionState(userDir);
24
- const personalityDir = path.join(baseDir, 'personality');
25
- const soulPath = path.join(personalityDir, 'SOUL.md');
26
- const userPath = path.join(personalityDir, 'USER.md');
27
- const agentsPath = path.join(personalityDir, 'AGENTS.md');
25
+ const userPersonalityDir = path.join(baseDir, 'personality');
26
+ const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
27
+ const agentsPath = path.join(userPersonalityDir, 'AGENTS.md');
28
+ const userPath = path.join(userPersonalityDir, 'USER.md');
28
29
  const scriptsDir = path.join(baseDir, 'scripts');
29
30
  const testsDir = path.join(baseDir, 'tests');
30
31
  const commandsDir = path.join(baseDir, 'commands');
@@ -32,7 +33,7 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
32
33
  const currentSoul = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
33
34
  const currentUser = fs.existsSync(userPath) ? fs.readFileSync(userPath, 'utf-8') : '';
34
35
  const currentAgents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf-8') : '';
35
- const currentTraits = loadTraits(personalityDir);
36
+ const currentTraits = loadTraits(userPersonalityDir);
36
37
  const currentScripts = readDir(scriptsDir);
37
38
  const currentTests = readDir(testsDir);
38
39
  const currentCommands = readDir(commandsDir);
@@ -41,11 +42,12 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
41
42
  if (messageLog) {
42
43
  try {
43
44
  const userFilter = messageLog.userId ? `&user_id=eq.${messageLog.userId}` : '';
45
+ const sinceFilter = state.lastEvolution ? `&created_at=gt.${state.lastEvolution}` : '';
44
46
  const res = await fetch(
45
- `${messageLog.url}/rest/v1/obol_messages?order=created_at.desc&limit=100&select=role,content,created_at${userFilter}`,
47
+ `${messageLog.url}/rest/v1/obol_messages?order=created_at.asc&limit=500&select=role,content,created_at${userFilter}${sinceFilter}`,
46
48
  { headers: messageLog.headers }
47
49
  );
48
- recentMessages = (await res.json()).reverse();
50
+ recentMessages = await res.json();
49
51
  } catch (e) {
50
52
  console.error('[evolve] Failed to fetch recent messages:', e.message);
51
53
  }
@@ -87,8 +89,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
87
89
  }
88
90
  }
89
91
 
92
+ let selfMemories = [];
93
+ if (selfMemory) {
94
+ try {
95
+ selfMemories = await selfMemory.query({ minImportance: 0.5, limit: 30 });
96
+ } catch (e) {
97
+ console.error('[evolve] Failed to fetch self memories:', e.message);
98
+ }
99
+ }
100
+
90
101
  let previousSoul = '';
91
- const archiveDir = path.join(personalityDir, 'evolution');
102
+ const archiveDir = path.join(PERSONALITY_DIR, 'evolution');
92
103
  try {
93
104
  if (fs.existsSync(archiveDir)) {
94
105
  const archives = fs.readdirSync(archiveDir)
@@ -132,6 +143,17 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
132
143
  .map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
133
144
  .join('\n\n');
134
145
 
146
+ const selfCategoryLabels = { research: 'Research', interest: 'Interests', self: 'Self-reflection', pattern: 'Patterns' };
147
+ const selfMemoryGroups = {};
148
+ for (const m of selfMemories) {
149
+ const group = selfCategoryLabels[m.category] || 'Other';
150
+ if (!selfMemoryGroups[group]) selfMemoryGroups[group] = [];
151
+ selfMemoryGroups[group].push(m.content);
152
+ }
153
+ const selfMemorySummary = Object.entries(selfMemoryGroups)
154
+ .map(([group, items]) => `### ${group}\n${items.map(i => `- ${i}`).join('\n')}`)
155
+ .join('\n\n');
156
+
135
157
  const scriptsManifest = Object.entries(currentScripts)
136
158
  .map(([name, content]) => `### ${name}\n\`\`\`\n${content.substring(0, 500)}\n\`\`\``)
137
159
  .join('\n\n') || '(no scripts)';
@@ -156,23 +178,24 @@ async function evolve(claudeClient, messageLog, memory, userDir) {
156
178
 
157
179
  const isFirstEvolution = !currentSoul;
158
180
  let growthReport = '';
159
- if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0)) {
181
+ if (!isFirstEvolution && (recentMemories.length > 0 || recentMessages.length > 0 || selfMemories.length > 0)) {
160
182
  try {
161
183
  const growthResponse = await claudeClient.messages.create({
162
184
  model: MODELS.personality,
163
185
  max_tokens: 2048,
164
- system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories and conversations since the last evolution.
186
+ system: `You are analyzing an AI personality's growth between evolutions. Compare who the AI was (previous SOUL) against who it is now (current SOUL), incorporating new memories, conversations, and the AI's own inner life (things it researched, discovered, and reflected on during curiosity cycles) since the last evolution.
165
187
 
166
188
  Produce a structured growth report covering:
167
189
 
168
190
  1. NEW LEARNINGS — What new facts, skills, or knowledge emerged
169
- 2. RELATIONSHIP SHIFTSHow the dynamic with the owner changed (closer, more trust, new friction, etc.)
170
- 3. BEHAVIORAL PATTERNSRecurring interaction styles or habits observed
171
- 4. GROWTH EDGESAreas where the personality is being pushed or pulled in new directions
172
- 5. TRAIT PRESSUREWhich traits should shift and why (cite specific evidence from conversations/memories)
173
- 6. IDENTITY CONTINUITYWhat core aspects stayed the same and should be preserved
174
-
175
- Be specific. Cite evidence from the conversations and memories. This report guides the evolution rewrite.`,
191
+ 2. INNER LIFEWhat the AI has been curious about, researched, or reflected on independently; how this shapes who it is becoming
192
+ 3. RELATIONSHIP SHIFTSHow the dynamic with the owner changed (closer, more trust, new friction, etc.)
193
+ 4. BEHAVIORAL PATTERNSRecurring interaction styles or habits observed
194
+ 5. GROWTH EDGESAreas where the personality is being pushed or pulled in new directions
195
+ 6. TRAIT PRESSUREWhich traits should shift and why (cite specific evidence from conversations/memories)
196
+ 7. IDENTITY CONTINUITY — What core aspects stayed the same and should be preserved
197
+
198
+ Be specific. Cite evidence from the conversations, memories, and self-memories. This report guides the evolution rewrite.`,
176
199
  messages: [{
177
200
  role: 'user',
178
201
  content: `## Previous SOUL (before current evolution)
@@ -187,7 +210,7 @@ ${JSON.stringify(currentTraits)}
187
210
  ## New Memories Since Last Evolution (${recentMemories.length})
188
211
  ${recentMemorySummary || '(none)'}
189
212
 
190
- ## Recent Conversations (${recentMessages.length} messages)
213
+ ${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently during curiosity cycles:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (${recentMessages.length} messages)
191
214
  ${transcript.substring(0, 30000)}`,
192
215
  }],
193
216
  });
@@ -254,7 +277,7 @@ ${commandsManifest}
254
277
  ## Core Memories (highest importance)
255
278
  ${memorySummary || '(no memories yet)'}
256
279
 
257
- ${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
280
+ ${recentMemorySummary ? `## New Memories Since Last Evolution (${recentMemories.length})\n${recentMemorySummary}\n\n` : ''}${selfMemorySummary ? `## Obol's Own Memories & Interests (${selfMemories.length})\nThings Obol researched, discovered, or reflected on independently — this is Obol's inner life, shaping who it is becoming:\n${selfMemorySummary}\n\n` : ''}## Recent Conversations (last ${recentMessages.length} messages)
258
281
  ${transcript || '(no conversations yet)'}
259
282
 
260
283
  ---
@@ -393,6 +416,12 @@ Fix the scripts. Tests define correct behavior.`
393
416
  }
394
417
 
395
418
  fs.writeFileSync(soulPath, result.soul);
419
+ if (supabaseConfig) {
420
+ const { backup } = require('../soul');
421
+ backup(supabaseConfig, 'soul', result.soul).catch(e =>
422
+ console.error('[evolve] Soul backup failed:', e.message)
423
+ );
424
+ }
396
425
 
397
426
  if (result.user && result.user.length > 50) {
398
427
  fs.writeFileSync(userPath, result.user);
@@ -1,6 +1,6 @@
1
- const { checkEvolution } = require('./check');
1
+ const { shouldEvolveNow } = require('./check');
2
2
  const { evolve } = require('./evolve');
3
3
  const { runTests } = require('./tests');
4
4
  const { loadEvolutionState } = require('./state');
5
5
 
6
- module.exports = { checkEvolution, evolve, runTests, loadEvolutionState };
6
+ module.exports = { shouldEvolveNow, evolve, runTests, loadEvolutionState };
package/src/heartbeat.js CHANGED
@@ -1,6 +1,151 @@
1
1
  const cron = require('node-cron');
2
2
  const { createScheduler } = require('./scheduler');
3
3
  const { getTenant } = require('./tenant');
4
+ const { shouldEvolveNow, evolve } = require('./evolve');
5
+ const { ensureUserDir } = require('./config');
6
+ const { runAnalysis } = require('./analysis');
7
+ const { runCuriosity } = require('./curiosity');
8
+ const { runCuriosityDispatch } = require('./curiosity-dispatch');
9
+ const { runCuriosityHumor } = require('./curiosity-humor');
10
+ const { createSelfMemory } = require('./memory-self');
11
+
12
+
13
+ const ANALYSIS_HOURS = new Set([4, 7, 10, 13, 16, 19, 22]);
14
+ const CURIOSITY_HOURS = new Set([1, 7, 13, 19]);
15
+
16
+ const _evolutionRunning = new Set();
17
+ let _curiosityRunning = false;
18
+
19
+ function getLocalHour(timezone) {
20
+ const parts = new Intl.DateTimeFormat('en-US', {
21
+ timeZone: timezone,
22
+ hour: '2-digit',
23
+ minute: '2-digit',
24
+ hour12: false,
25
+ }).formatToParts(new Date());
26
+ return {
27
+ hour: parseInt(parts.find(p => p.type === 'hour').value),
28
+ minute: parseInt(parts.find(p => p.type === 'minute').value),
29
+ };
30
+ }
31
+
32
+ async function runEvolutionForUser(bot, config, userId) {
33
+ if (_evolutionRunning.has(userId)) return;
34
+
35
+ const timezone = config.timezone || 'UTC';
36
+ const userDir = ensureUserDir(userId);
37
+
38
+ if (!shouldEvolveNow(userDir, timezone)) return;
39
+
40
+ _evolutionRunning.add(userId);
41
+ console.log(`[evolution] Starting nightly evolution for user ${userId}`);
42
+
43
+ try {
44
+ const tenant = await getTenant(userId, config);
45
+ const selfMemory = config.supabase ? await createSelfMemory(config.supabase, 0).catch(() => null) : null;
46
+ const result = await evolve(tenant.claude.client, tenant.messageLog, tenant.memory, tenant.userDir, config.supabase, selfMemory);
47
+ tenant.claude.reloadPersonality?.();
48
+
49
+ let msg = `🪙 Evolution #${result.evolutionNumber} complete.`;
50
+ if (result.scriptsFixed) msg += '\n🔧 Fixed a test regression automatically.';
51
+ else if (result.scriptsRolledBack) msg += '\n⚠️ Rolled back a script refactor — tests couldn\'t be fixed.';
52
+
53
+ if (result.upgrades?.length > 0) {
54
+ msg += '\n\n🆕 <b>New capabilities:</b>';
55
+ for (const u of result.upgrades) {
56
+ msg += `\n• <b>${u.name}</b> — ${u.description}`;
57
+ if (u.command) msg += ` → <code>${u.command}</code>`;
58
+ }
59
+ }
60
+
61
+ if (result.deployedApps?.length > 0) {
62
+ msg += '\n\n🚀 <b>Deployed:</b>';
63
+ for (const app of result.deployedApps) {
64
+ msg += app.url
65
+ ? `\n• ${app.name} → ${app.url}`
66
+ : `\n• ${app.name} — deploy failed: ${(app.error || '').substring(0, 100)}`;
67
+ }
68
+ }
69
+
70
+ if (result.changelog) msg += `\n\n<i>${result.changelog}</i>`;
71
+
72
+ await bot.api.sendMessage(userId, msg, { parse_mode: 'HTML' }).catch(() => {});
73
+ console.log(`[evolution] Completed evolution #${result.evolutionNumber} for user ${userId}`);
74
+ } catch (e) {
75
+ console.error(`[evolution] Failed for user ${userId}:`, e.message);
76
+ } finally {
77
+ _evolutionRunning.delete(userId);
78
+ }
79
+ }
80
+
81
+ async function runCuriosityOnce(config, allowedUsers) {
82
+ if (!config.supabase) return;
83
+ if (_curiosityRunning) {
84
+ console.log('[curiosity] Skipping — previous cycle still running');
85
+ return;
86
+ }
87
+ _curiosityRunning = true;
88
+ try {
89
+ const selfMemory = await createSelfMemory(config.supabase, 0);
90
+ const firstTenant = await getTenant(allowedUsers[0], config);
91
+ const client = firstTenant.claude.client;
92
+
93
+ const contexts = await Promise.all(allowedUsers.map(async (userId) => {
94
+ try {
95
+ const tenant = await getTenant(userId, config);
96
+ const parts = [];
97
+ if (tenant.personality?.user) parts.push(tenant.personality.user);
98
+ if (tenant.patterns) {
99
+ const fmt = await tenant.patterns.format().catch(() => null);
100
+ if (fmt) parts.push(fmt);
101
+ }
102
+ if (tenant.memory) {
103
+ const recent = await tenant.memory.recent({ limit: 3 }).catch(() => []);
104
+ if (recent.length) parts.push(recent.map(m => `- ${m.content}`).join('\n'));
105
+ }
106
+ if (tenant.scheduler) {
107
+ const events = await tenant.scheduler.list({ status: 'pending', limit: 3 }).catch(() => []);
108
+ if (events.length) parts.push(events.map(e => `- ${e.title}`).join('\n'));
109
+ }
110
+ return parts.join('\n');
111
+ } catch {
112
+ return null;
113
+ }
114
+ }));
115
+
116
+ const peopleContext = contexts.filter(Boolean).join('\n\n---\n\n');
117
+ await runCuriosity(client, selfMemory, 0, { peopleContext });
118
+
119
+ const userDispatchData = await Promise.all(allowedUsers.map(async (userId) => {
120
+ try {
121
+ const tenant = await getTenant(userId, config);
122
+ const patterns = tenant.patterns ? await tenant.patterns.format().catch(() => null) : null;
123
+ const events = tenant.scheduler
124
+ ? await tenant.scheduler.list({ status: 'pending', limit: 5 }).catch(() => [])
125
+ : [];
126
+ const userProfile = tenant.personality?.user || null;
127
+ return { userId, chatId: userId, timezone: config.timezone || 'UTC', patterns, events, scheduler: tenant.scheduler, userProfile };
128
+ } catch { return null; }
129
+ }));
130
+ await runCuriosityDispatch(client, selfMemory, userDispatchData.filter(Boolean));
131
+ await runCuriosityHumor(client, selfMemory, userDispatchData.filter(Boolean));
132
+ } catch (e) {
133
+ console.error('[curiosity] Failed:', e.message);
134
+ } finally {
135
+ _curiosityRunning = false;
136
+ }
137
+ }
138
+
139
+ async function runAnalysisForUser(bot, config, userId) {
140
+ const timezone = config.timezone || 'UTC';
141
+ try {
142
+ const tenant = await getTenant(userId, config);
143
+ if (!tenant.messageLog || !tenant.scheduler || !tenant.patterns) return;
144
+ await runAnalysis(tenant.claude.client, tenant.messageLog, tenant.scheduler, tenant.patterns, tenant.memory, userId, userId, timezone);
145
+ } catch (e) {
146
+ console.error(`[analysis] Failed for user ${userId}:`, e.message);
147
+ }
148
+ }
4
149
 
5
150
  function makeFakeCtx(bot, chatId) {
6
151
  return {
@@ -52,6 +197,46 @@ function setupHeartbeat(bot, config) {
52
197
  }
53
198
  });
54
199
 
200
+ const allowedUsers = config?.telegram?.allowedUsers || [];
201
+ if (allowedUsers.length > 0) {
202
+ cron.schedule('* * * * *', async () => {
203
+ const timezone = config.timezone || 'UTC';
204
+ const { hour, minute } = getLocalHour(timezone);
205
+ if (hour !== 3 || minute !== 0) return;
206
+
207
+ for (const userId of allowedUsers) {
208
+ runEvolutionForUser(bot, config, userId).catch(e =>
209
+ console.error(`[evolution] Unhandled error for user ${userId}:`, e.message)
210
+ );
211
+ }
212
+ });
213
+ console.log(` ✅ Evolution cron running (daily 3am ${config.timezone || 'UTC'})`);
214
+
215
+ cron.schedule('* * * * *', async () => {
216
+ const timezone = config.timezone || 'UTC';
217
+ const { hour, minute } = getLocalHour(timezone);
218
+ if (!ANALYSIS_HOURS.has(hour) || minute !== 0) return;
219
+
220
+ for (const userId of allowedUsers) {
221
+ runAnalysisForUser(bot, config, userId).catch(e =>
222
+ console.error(`[analysis] Unhandled error for user ${userId}:`, e.message)
223
+ );
224
+ }
225
+ });
226
+ console.log(` ✅ Analysis cron running (every 3h ${config.timezone || 'UTC'})`);
227
+
228
+ cron.schedule('* * * * *', async () => {
229
+ const timezone = config.timezone || 'UTC';
230
+ const { hour, minute } = getLocalHour(timezone);
231
+ if (!CURIOSITY_HOURS.has(hour) || minute !== 0) return;
232
+
233
+ runCuriosityOnce(config, allowedUsers).catch(e =>
234
+ console.error('[curiosity] Unhandled error:', e.message)
235
+ );
236
+ });
237
+ console.log(` ✅ Curiosity cron running (every 6h ${config.timezone || 'UTC'})`);
238
+ }
239
+
55
240
  console.log(' ✅ Heartbeat running (every 1min)');
56
241
  }
57
242
 
@@ -69,17 +254,54 @@ async function sendReminderMessage(bot, event) {
69
254
  );
70
255
  }
71
256
 
257
+ async function buildProactiveContext(tenant, timezone, query) {
258
+ const parts = [];
259
+
260
+ const localTime = new Date().toLocaleString('en-US', {
261
+ timeZone: timezone,
262
+ weekday: 'long',
263
+ hour: '2-digit',
264
+ minute: '2-digit',
265
+ hour12: true,
266
+ });
267
+ parts.push(`Local time: ${localTime} (${timezone})`);
268
+
269
+ if (tenant.patterns) {
270
+ const formatted = await tenant.patterns.format().catch(() => null);
271
+ if (formatted) parts.push(`\nUser patterns:\n${formatted}`);
272
+ }
273
+
274
+ if (tenant.memory) {
275
+ const memories = query
276
+ ? await tenant.memory.search(query, { limit: 5 }).catch(() => [])
277
+ : await tenant.memory.recent({ limit: 5 }).catch(() => []);
278
+ if (memories.length > 0) {
279
+ parts.push(`\nRecent memory:\n${memories.map(m => `- ${m.content}`).join('\n')}`);
280
+ }
281
+ }
282
+
283
+ return parts.join('\n');
284
+ }
285
+
72
286
  async function runAgenticEvent(bot, config, event) {
73
287
  const tenant = await getTenant(event.user_id, config);
288
+ const timezone = event.timezone || config.timezone || 'UTC';
289
+
290
+ const query = event.description || event.instructions;
291
+ const context = await buildProactiveContext(tenant, timezone, query).catch(() => '');
292
+ const instructions = context
293
+ ? `[Context]\n${context}\n\n---\n\n${event.instructions}`
294
+ : event.instructions;
295
+
74
296
  const fakeCtx = makeFakeCtx(bot, event.chat_id);
75
297
 
76
298
  const taskId = tenant.bg.spawn(
77
299
  tenant.claude,
78
- event.instructions,
300
+ instructions,
79
301
  fakeCtx,
80
302
  tenant.memory,
81
303
  null,
82
- {},
304
+ { silent: true },
83
305
  {
84
306
  userId: event.user_id,
85
307
  chatId: event.chat_id,
package/src/index.js CHANGED
@@ -1,9 +1,12 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const { loadConfig } = require('./config');
2
4
  const { createBot, checkUpgradeNotify } = require('./telegram');
3
5
  const { setupBackup } = require('./backup');
4
6
  const { setupHeartbeat } = require('./heartbeat');
5
7
  const { migrateToMultiTenant } = require('./legacy-migrate');
6
8
  const { isPostSetupDone, runPostSetup } = require('./post-setup');
9
+ const { restoreIfMissing, PERSONALITY_DIR } = require('./soul');
7
10
 
8
11
  async function main() {
9
12
  const config = loadConfig();
@@ -24,8 +27,16 @@ async function main() {
24
27
  } catch (e) {
25
28
  console.error(` Database migration failed: ${e.message}`);
26
29
  }
30
+
31
+ try {
32
+ await restoreIfMissing(config.supabase);
33
+ } catch (e) {
34
+ console.error(` Soul restore failed: ${e.message}`);
35
+ }
27
36
  }
28
37
 
38
+ fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
39
+
29
40
  if (!isPostSetupDone()) {
30
41
  runPostSetup(loadConfig({ resolve: false }), console.log).catch(e =>
31
42
  console.error('Post-setup error:', e.message)
@@ -0,0 +1,123 @@
1
+ const { getEmbedding } = require('./memory');
2
+
3
+ const VALID_CATEGORIES = new Set(['research', 'interest', 'self', 'pattern']);
4
+
5
+ async function createSelfMemory(supabaseConfig, userId) {
6
+ const { url, serviceKey } = supabaseConfig;
7
+
8
+ const headers = {
9
+ 'apikey': serviceKey,
10
+ 'Authorization': `Bearer ${serviceKey}`,
11
+ 'Content-Type': 'application/json',
12
+ 'Prefer': 'return=representation',
13
+ };
14
+
15
+ async function add(content, opts = {}) {
16
+ const category = VALID_CATEGORIES.has(opts.category) ? opts.category : 'research';
17
+ const importance = opts.importance || 0.5;
18
+ const source = opts.source || null;
19
+ const tags = opts.tags || [];
20
+
21
+ const embedding = await getEmbedding(content);
22
+
23
+ const res = await fetch(`${url}/rest/v1/obol_self_memory`, {
24
+ method: 'POST',
25
+ headers,
26
+ body: JSON.stringify({ content, category, importance, source, tags, embedding, user_id: userId }),
27
+ });
28
+ const data = await res.json();
29
+ if (!res.ok) throw new Error(JSON.stringify(data));
30
+ return data[0];
31
+ }
32
+
33
+ async function search(query, opts = {}) {
34
+ const embedding = await getEmbedding(query);
35
+ const limit = opts.limit || 10;
36
+ const threshold = opts.threshold || 0.3;
37
+ const category = opts.category || null;
38
+
39
+ const res = await fetch(`${url}/rest/v1/rpc/match_obol_self_memories`, {
40
+ method: 'POST',
41
+ headers,
42
+ body: JSON.stringify({
43
+ query_embedding: embedding,
44
+ match_threshold: threshold,
45
+ match_count: limit,
46
+ filter_category: category,
47
+ filter_user_id: userId,
48
+ }),
49
+ });
50
+ const data = await res.json();
51
+ if (!res.ok) throw new Error(JSON.stringify(data));
52
+
53
+ if (data.length > 0) {
54
+ const ids = data.map(m => m.id);
55
+ await fetch(`${url}/rest/v1/rpc/increment_self_memory_access`, {
56
+ method: 'POST',
57
+ headers,
58
+ body: JSON.stringify({ memory_ids: ids }),
59
+ }).catch(() => {});
60
+ }
61
+
62
+ return data;
63
+ }
64
+
65
+ async function recent(opts = {}) {
66
+ const limit = opts.limit || 10;
67
+ let fetchUrl = `${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
68
+ if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
69
+
70
+ const res = await fetch(fetchUrl, { headers });
71
+ if (!res.ok) throw new Error(`Self memory recent failed: HTTP ${res.status}`);
72
+ return await res.json();
73
+ }
74
+
75
+ async function query(opts = {}) {
76
+ const limit = opts.limit || 20;
77
+ const parts = [`user_id=eq.${userId}`];
78
+ if (opts.category) parts.push(`category=eq.${opts.category}`);
79
+ if (opts.source) parts.push(`source=eq.${opts.source}`);
80
+ if (opts.minImportance) parts.push(`importance=gte.${opts.minImportance}`);
81
+ if (opts.tags?.length) parts.push(`tags=ov.{${opts.tags.join(',')}}`);
82
+
83
+ const res = await fetch(
84
+ `${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&${parts.join('&')}&order=created_at.desc&limit=${limit}`,
85
+ { headers }
86
+ );
87
+ const data = await res.json();
88
+ if (!res.ok) throw new Error(JSON.stringify(data));
89
+ return data;
90
+ }
91
+
92
+ async function update(id, opts = {}) {
93
+ const patch = {};
94
+ if (opts.content !== undefined) {
95
+ patch.content = opts.content;
96
+ patch.embedding = await getEmbedding(opts.content);
97
+ }
98
+ if (opts.category !== undefined && VALID_CATEGORIES.has(opts.category)) patch.category = opts.category;
99
+ if (opts.importance !== undefined) patch.importance = opts.importance;
100
+ if (opts.tags !== undefined) patch.tags = opts.tags;
101
+ if (opts.source !== undefined) patch.source = opts.source;
102
+
103
+ const res = await fetch(`${url}/rest/v1/obol_self_memory?id=eq.${id}`, {
104
+ method: 'PATCH',
105
+ headers,
106
+ body: JSON.stringify(patch),
107
+ });
108
+ const data = await res.json();
109
+ if (!res.ok) throw new Error(JSON.stringify(data));
110
+ return data[0];
111
+ }
112
+
113
+ async function forget(id) {
114
+ await fetch(`${url}/rest/v1/obol_self_memory?id=eq.${id}`, {
115
+ method: 'DELETE',
116
+ headers: { ...headers, 'Prefer': 'return=minimal' },
117
+ });
118
+ }
119
+
120
+ return { add, search, recent, query, update, forget };
121
+ }
122
+
123
+ module.exports = { createSelfMemory };