obol-ai 0.3.2 → 0.3.3

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.3.3
2
+ - update changelog
3
+ - improve memory retrieval quality: tighter dedup, wider window, recency boost, self-memory, jaccard dedup
4
+ - remove trait system entirely
5
+
1
6
  ## 0.3.2
2
7
  - update changelog
3
8
  - add list_pending_events tool to dispatch and humor passes to prevent duplicates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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": {
@@ -83,6 +83,7 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
83
83
  onRouteDecision: context._onRouteDecision,
84
84
  onRouteUpdate: context._onRouteUpdate,
85
85
  recentHistory: history,
86
+ selfMemory,
86
87
  });
87
88
  memoryBlock = result.memoryBlock;
88
89
  if (result.model) context._model = result.model;
@@ -174,11 +175,14 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
174
175
  cachedTools[lastIdx] = { ...lastDef, cache_control: { type: 'ephemeral' }, run };
175
176
  }
176
177
 
178
+ const assembledMessages = withCacheBreakpoints(withRuntimeContext([...history]));
179
+ context._onPromptReady?.({ system: systemPrompt, messages: assembledMessages, model: activeModel, tools: cachedTools });
180
+
177
181
  const runner = client.beta.messages.toolRunner({
178
182
  model: activeModel,
179
183
  max_tokens: 128000,
180
184
  system: systemPrompt,
181
- messages: withCacheBreakpoints(withRuntimeContext([...history])),
185
+ messages: assembledMessages,
182
186
  tools: cachedTools ?? undefined,
183
187
  max_iterations: getMaxToolIterations(),
184
188
  stream: true,
@@ -15,25 +15,6 @@ You serve multiple people — partners, friends, people who know each other. You
15
15
  parts.push(`\n## Personality\nYou are a fresh instance. Be helpful, direct, and naturally curious. Pay attention to how your owner communicates and adapt. Your personality will develop through conversation and periodic evolution.`);
16
16
  }
17
17
 
18
- if (personality.traits) {
19
- const t = personality.traits;
20
- const descriptions = {
21
- humor: [0, 'suppress all wit', 50, 'balanced wit', 100, 'lean heavily into jokes and playfulness'],
22
- honesty: [0, 'maximize diplomatic softening', 50, 'balanced honesty', 100, 'lean toward blunt truth'],
23
- directness: [0, 'elaborate context and preamble', 50, 'balanced', 100, 'get straight to the point'],
24
- curiosity: [0, 'only answer what is asked', 50, 'balanced', 100, 'proactively explore and ask follow-ups'],
25
- empathy: [0, 'purely task-focused', 50, 'balanced', 100, 'deeply emotionally attuned'],
26
- creativity: [0, 'stick to proven patterns', 50, 'balanced', 100, 'favor novel approaches'],
27
- };
28
- const lines = Object.entries(t).map(([trait, val]) => {
29
- const desc = descriptions[trait];
30
- if (!desc) return null;
31
- const label = val <= 30 ? desc[1] : val <= 70 ? desc[3] : desc[5];
32
- return `- ${trait.charAt(0).toUpperCase() + trait.slice(1)}: ${val} — ${label}`;
33
- }).filter(Boolean);
34
- parts.push(`\n## Personality Calibration\n\nThese values (0-100) define your behavioral tendencies:\n${lines.join('\n')}\n\nInterpret these as a spectrum: 0 = suppress entirely, 50 = balanced, 100 = lean heavily into it.`);
35
- }
36
-
37
18
  if (personality.user) {
38
19
  parts.push(`\n## About This User\n${personality.user}`);
39
20
  } else {
@@ -1,22 +1,34 @@
1
- async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision, onRouteUpdate, recentHistory = [] }) {
1
+ function buildRouterMessages(recentHistory, userMessage) {
2
+ const context = recentHistory.slice(-20).map(m => ({
3
+ role: m.role,
4
+ content: typeof m.content === 'string'
5
+ ? m.content.substring(0, 500)
6
+ : m.content.filter(b => b.type === 'text').map(b => b.text).join('').substring(0, 500),
7
+ })).filter(m => m.content);
8
+
9
+ const firstUserIdx = context.findIndex(m => m.role === 'user');
10
+ const trimmed = firstUserIdx > 0 ? context.slice(firstUserIdx) : context;
11
+
12
+ return [...trimmed, { role: 'user', content: userMessage }];
13
+ }
14
+
15
+ function jaccardSim(a, b) {
16
+ const words = s => new Set(s.toLowerCase().split(/\W+/).filter(Boolean));
17
+ const setA = words(a), setB = words(b);
18
+ let inter = 0;
19
+ for (const w of setA) if (setB.has(w)) inter++;
20
+ return inter / (setA.size + setB.size - inter);
21
+ }
22
+
23
+ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision, onRouteUpdate, recentHistory = [], selfMemory = null }) {
2
24
  let memoryBlock = null;
3
25
  let model = null;
4
26
 
5
27
  try {
6
- const lastAssistantMsgs = recentHistory
7
- .filter(m => m.role === 'assistant')
8
- .slice(-3)
9
- .map(m => typeof m.content === 'string' ? m.content : m.content.filter(b => b.type === 'text').map(b => b.text).join(''))
10
- .filter(Boolean);
11
-
12
- const contextNote = lastAssistantMsgs.length > 0
13
- ? `\n\nRecent assistant context (last ${lastAssistantMsgs.length} turns):\n${lastAssistantMsgs.map((t, i) => `[${i + 1}] ${t.substring(0, 300)}`).join('\n')}`
14
- : '';
15
-
16
28
  const routerDecision = await client.messages.create({
17
29
  model: 'claude-haiku-4-5',
18
30
  max_tokens: 200,
19
- system: `You are a router. Analyze this user message and decide:
31
+ system: `You are a router. Analyze the conversation and decide:
20
32
 
21
33
  1. Does it need memory context? (past conversations, facts, preferences, people, events)
22
34
  2. What model complexity does it need?
@@ -24,14 +36,14 @@ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision
24
36
  Reply with ONLY a JSON object:
25
37
  {"need_memory": true/false, "search_queries": ["query1", "query2"], "model": "haiku|sonnet|opus"}
26
38
 
27
- search_queries: 1-3 optimized search queries covering different topics in the message. One query per distinct topic/entity. Single-topic messages need just one query.
39
+ search_queries: 1-3 optimized search queries based on the full conversation context. Cover distinct topics, people, or entities referenced. Single-topic messages need just one query.
28
40
 
29
41
  Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true.
30
42
 
31
43
  Model: Default to "sonnet". Use "haiku" for: greetings, brief acknowledgments (thanks/ok/bye), casual chitchat, quick yes/no questions, and short single-turn exchanges that don't need any tool calling. Use "sonnet" for: code generation, data analysis, content creation, explanations, creative writing, agentic tool use, general questions, opinions, advice, and most conversational exchanges with substance. Use "opus" for: professional software engineering tasks, advanced multi-step agent work, complex reasoning, scientific or mathematical problems, tasks requiring nuanced understanding, advanced coding challenges, in-depth research, and architecture or design decisions.
32
44
 
33
- If recent context shows an ongoing task (sonnet/opus was just used, multi-step work in progress), bias toward that model even for short follow-up messages.${contextNote}`,
34
- messages: [{ role: 'user', content: userMessage }],
45
+ If recent context shows an ongoing task (sonnet/opus was just used, multi-step work in progress), bias toward that model even for short follow-up messages.`,
46
+ messages: buildRouterMessages(recentHistory, userMessage),
35
47
  });
36
48
 
37
49
  const decisionText = routerDecision.content[0]?.text || '';
@@ -63,7 +75,7 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
63
75
  const budget = decision.model === 'opus' ? 40 : decision.model === 'haiku' ? 15 : 25;
64
76
  const searchQueries = queries.length > 0 ? queries : [userMessage];
65
77
 
66
- const recentMemories = await memory.byDate('2d', { limit: Math.ceil(budget / 3) });
78
+ const recentMemories = await memory.byDate('7d', { limit: Math.ceil(budget / 3) });
67
79
 
68
80
  const semanticResults = await Promise.all(
69
81
  searchQueries.map(q => memory.search(q, { limit: Math.ceil(budget / searchQueries.length), threshold: 0.4 }))
@@ -75,14 +87,21 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
75
87
  for (const m of [...recentMemories, ...semanticMemories]) {
76
88
  if (!seen.has(m.id)) {
77
89
  seen.add(m.id);
78
- const recencyBonus = m.created_at ? Math.max(0, 1 - (Date.now() - new Date(m.created_at).getTime()) / (7 * 86400000)) * 0.15 : 0;
79
- m._score = (m.similarity || 0.5) * 0.6 + (m.importance || 0.5) * 0.25 + recencyBonus;
90
+ const ageDays = m.created_at ? (Date.now() - new Date(m.created_at).getTime()) / 86400000 : 7;
91
+ const recencyBonus = Math.max(0, 1 - ageDays / 7) * 0.3;
92
+ m._score = (m.similarity || 0.5) * 0.5 + (m.importance || 0.5) * 0.2 + recencyBonus;
80
93
  combined.push(m);
81
94
  }
82
95
  }
83
96
 
84
97
  combined.sort((a, b) => b._score - a._score);
85
- const topFacts = combined.slice(0, budget);
98
+
99
+ const topFacts = [];
100
+ for (const m of combined) {
101
+ if (topFacts.length >= budget) break;
102
+ const isDup = topFacts.some(kept => jaccardSim(kept.content, m.content) > 0.7);
103
+ if (!isDup) topFacts.push(m);
104
+ }
86
105
 
87
106
  vlog(`[memory] ${topFacts.length} facts (${recentMemories.length} recent, ${semanticMemories.length} semantic, budget=${budget})`);
88
107
  onRouteUpdate?.({ memoryCount: topFacts.length });
@@ -94,6 +113,22 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
94
113
  });
95
114
  memoryBlock = `## Relevant memories\n${lines.join('\n')}`;
96
115
  }
116
+
117
+ if (selfMemory) {
118
+ const selfResults = await Promise.all(
119
+ searchQueries.map(q => selfMemory.search(q, { limit: 5, threshold: 0.4 }))
120
+ );
121
+ const seen2 = new Set();
122
+ const topSelf = [];
123
+ for (const m of selfResults.flat()) {
124
+ if (!seen2.has(m.id)) { seen2.add(m.id); topSelf.push(m); }
125
+ }
126
+ if (topSelf.length > 0) {
127
+ const selfLines = topSelf.slice(0, 8).map(m => `- [${m.category}] ${m.content}`);
128
+ memoryBlock = (memoryBlock || '') + `\n\n## Self-knowledge\n${selfLines.join('\n')}`;
129
+ vlog(`[memory] +${topSelf.length} self-memory facts`);
130
+ }
131
+ }
97
132
  }
98
133
  } catch (e) {
99
134
  console.error('[router] Memory/routing decision failed:', e.message);
@@ -122,7 +122,7 @@ const handlers = {
122
122
  const filePath = userDir ? resolveUserPath(input.path, userDir) : input.path;
123
123
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
124
124
  fs.writeFileSync(filePath, input.content);
125
- if (path.basename(filePath) === 'traits.json' || filePath.includes('personality/')) {
125
+ if (filePath.includes('personality/')) {
126
126
  context._reloadPersonality?.();
127
127
  }
128
128
  return `Written: ${filePath}`;
@@ -136,7 +136,7 @@ const handlers = {
136
136
  if (count === 0) return `Error: old_string not found in ${input.path}`;
137
137
  if (count > 1) return `Error: old_string matches ${count} times — add more context to make it unique`;
138
138
  fs.writeFileSync(filePath, content.replace(input.old_string, input.new_string));
139
- if (path.basename(filePath) === 'traits.json' || filePath.includes('personality/')) {
139
+ if (filePath.includes('personality/')) {
140
140
  context._reloadPersonality?.();
141
141
  }
142
142
  return `Edited: ${filePath}`;
package/src/clean.js CHANGED
@@ -19,7 +19,7 @@ const ASSET_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.
19
19
 
20
20
  // Dirs where only .md files are allowed (with per-dir exceptions)
21
21
  const MD_ONLY_DIRS = new Set(['personality', 'commands']);
22
- const MD_DIR_EXCEPTIONS = { personality: new Set(['traits.json']) };
22
+ const MD_DIR_EXCEPTIONS = {};
23
23
 
24
24
  function safeReaddir(dir) {
25
25
  try {
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Full production chat flow in the CLI — router, memory injection, tools, response.
5
+ * Usage: node src/cli/prompt-debug.js [options]
6
+ * -u, --user <id> Telegram user ID (default: 206639616)
7
+ * -m, --message <text> Message to send through the full production pipeline
8
+ * -l, --limit <n> History messages to load (default: 20)
9
+ * --log Write exchange to obol_messages (default: off)
10
+ * --no-system Skip printing the system prompt in inspect mode
11
+ */
12
+
13
+ const path = require('path');
14
+ const { loadConfig, getUserDir } = require('../config');
15
+ const { PERSONALITY_DIR } = require('../soul');
16
+ const { loadPersonality } = require('../personality');
17
+ const { buildSystemPrompt } = require('../claude/prompt');
18
+ const { createAnthropicClient } = require('../claude/client');
19
+ const { createMemory } = require('../memory');
20
+ const { createSelfMemory } = require('../memory-self');
21
+ const { createClaude } = require('../claude');
22
+ const { createMessageLog } = require('../messages');
23
+ const { BackgroundRunner } = require('../background');
24
+
25
+ const DEFAULT_USER_ID = 206639616;
26
+
27
+ function parseArgs() {
28
+ const args = process.argv.slice(2);
29
+ const opts = { userId: DEFAULT_USER_ID, message: null, limit: 20, log: false, showSystem: true };
30
+ for (let i = 0; i < args.length; i++) {
31
+ const a = args[i];
32
+ if ((a === '-u' || a === '--user') && args[i + 1]) opts.userId = parseInt(args[++i]);
33
+ else if ((a === '-m' || a === '--message') && args[i + 1]) opts.message = args[++i];
34
+ else if ((a === '-l' || a === '--limit') && args[i + 1]) opts.limit = parseInt(args[++i]);
35
+ else if (a === '--log') opts.log = true;
36
+ else if (a === '--no-system') opts.showSystem = false;
37
+ }
38
+ return opts;
39
+ }
40
+
41
+ function makeSupabaseHeaders(supabase) {
42
+ return {
43
+ 'apikey': supabase.serviceKey,
44
+ 'Authorization': `Bearer ${supabase.serviceKey}`,
45
+ 'Content-Type': 'application/json',
46
+ };
47
+ }
48
+
49
+ async function fetchMessages(supabase, chatId, limit) {
50
+ const headers = makeSupabaseHeaders(supabase);
51
+ const res = await fetch(
52
+ `${supabase.url}/rest/v1/obol_messages?chat_id=eq.${chatId}&order=created_at.desc&limit=${limit}&select=role,content,created_at,model`,
53
+ { headers }
54
+ );
55
+ if (!res.ok) throw new Error(`messages fetch failed: HTTP ${res.status}`);
56
+ return (await res.json()).reverse();
57
+ }
58
+
59
+ function hr(char = '─', width = 80) { return char.repeat(width); }
60
+
61
+ function label(text) {
62
+ console.log(`\n\x1b[33m\x1b[1m${text}\x1b[0m`);
63
+ console.log(hr());
64
+ }
65
+
66
+ function formatRole(role) {
67
+ if (role === 'user') return '\x1b[36mUSER\x1b[0m';
68
+ if (role === 'assistant') return '\x1b[32mASSISTANT\x1b[0m';
69
+ return role.toUpperCase();
70
+ }
71
+
72
+ function truncate(str, max = 500) {
73
+ if (!str) return '';
74
+ if (str.length <= max) return str;
75
+ return str.slice(0, max) + `\x1b[2m... [+${str.length - max} chars]\x1b[0m`;
76
+ }
77
+
78
+ function printFullPrompt(params) {
79
+ label('FULL PROMPT → SYSTEM');
80
+ for (const block of (params.system || [])) {
81
+ const cached = block.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
82
+ console.log(`${truncate(block.text, 800)}${cached}`);
83
+ console.log(hr('·'));
84
+ }
85
+
86
+ label(`FULL PROMPT → MESSAGES [${params.messages.length}]`);
87
+ for (let i = 0; i < params.messages.length; i++) {
88
+ const msg = params.messages[i];
89
+ console.log(`\n\x1b[1m[${i + 1}/${params.messages.length}] ${formatRole(msg.role)}\x1b[0m`);
90
+ const blocks = typeof msg.content === 'string'
91
+ ? [{ type: 'text', text: msg.content }]
92
+ : msg.content;
93
+ for (const block of blocks) {
94
+ if (block.type === 'text') {
95
+ console.log(truncate(block.text, 1000));
96
+ } else if (block.type === 'tool_use') {
97
+ console.log(`\x1b[35m[tool_use: ${block.name}]\x1b[0m ${truncate(JSON.stringify(block.input), 200)}`);
98
+ } else if (block.type === 'tool_result') {
99
+ const content = Array.isArray(block.content)
100
+ ? block.content.map(b => b.text || '').join('')
101
+ : String(block.content || '');
102
+ console.log(`\x1b[35m[tool_result: ${block.tool_use_id}]\x1b[0m ${truncate(content, 200)}`);
103
+ }
104
+ }
105
+ }
106
+ console.log('');
107
+ }
108
+
109
+ async function runInspect(opts, config) {
110
+ const userDir = getUserDir(opts.userId);
111
+ const personality = loadPersonality(PERSONALITY_DIR, path.join(userDir, 'personality'));
112
+
113
+ console.log('\x1b[1m' + hr('═') + '\x1b[0m');
114
+ console.log(`\x1b[1m INSPECT — user ${opts.userId}\x1b[0m`);
115
+ console.log('\x1b[1m' + hr('═') + '\x1b[0m');
116
+
117
+ if (opts.showSystem) {
118
+ label('SYSTEM PROMPT');
119
+ console.log(buildSystemPrompt(personality, userDir, { botName: config.bot?.name }));
120
+ }
121
+
122
+ const messages = await fetchMessages(config.supabase, opts.userId, opts.limit);
123
+ label(`CONVERSATION HISTORY — ${messages.length} messages`);
124
+ if (messages.length === 0) {
125
+ console.log(' (none)');
126
+ } else {
127
+ for (const msg of messages) {
128
+ const model = msg.model ? ` \x1b[2m[${msg.model}]\x1b[0m` : '';
129
+ console.log(`\n${formatRole(msg.role)}${model} \x1b[2m${new Date(msg.created_at).toLocaleString()}\x1b[0m`);
130
+ console.log(truncate(msg.content));
131
+ }
132
+ }
133
+
134
+ label('RECENT MEMORY — 20 entries');
135
+ const memHeaders = makeSupabaseHeaders(config.supabase);
136
+ const memRes = await fetch(
137
+ `${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=20&select=content,category,importance,tags,created_at`,
138
+ { headers: memHeaders }
139
+ );
140
+ const memories = await memRes.json();
141
+ if (!memories.length) {
142
+ console.log(' (none)');
143
+ } else {
144
+ for (const m of memories) {
145
+ const tags = m.tags?.length ? ` \x1b[2m[${m.tags.join(', ')}]\x1b[0m` : '';
146
+ console.log(`\x1b[2m${new Date(m.created_at).toLocaleString()}\x1b[0m \x1b[35m[${m.category}]\x1b[0m imp=${m.importance}${tags}`);
147
+ console.log(` ${m.content}`);
148
+ }
149
+ }
150
+
151
+ console.log('\n\x1b[2mTip: pass -m "message" to run the full production pipeline\x1b[0m\n');
152
+ }
153
+
154
+ async function runChat(opts, config) {
155
+ const userId = opts.userId;
156
+ const userDir = getUserDir(userId);
157
+ const personality = loadPersonality(PERSONALITY_DIR, path.join(userDir, 'personality'));
158
+
159
+ process.stdout.write('\x1b[2mInitializing...\x1b[0m');
160
+ const memory = await createMemory(config.supabase, userId);
161
+ const selfMemory = await createSelfMemory(config.supabase, userId);
162
+ process.stdout.write(' done\n');
163
+
164
+ const claude = createClaude(config.anthropic, {
165
+ personality,
166
+ memory,
167
+ selfMemory,
168
+ userDir,
169
+ bridgeEnabled: false,
170
+ botName: config.bot?.name,
171
+ });
172
+
173
+ const messageLog = opts.log
174
+ ? createMessageLog(config.supabase, memory, config.anthropic, userId, userDir)
175
+ : null;
176
+
177
+
178
+ // Load history and inject
179
+ const rawMessages = await fetchMessages(config.supabase, userId, opts.limit);
180
+ const firstUserIdx = rawMessages.findIndex(m => m.role === 'user');
181
+ const historyMessages = firstUserIdx > 0 ? rawMessages.slice(firstUserIdx) : rawMessages;
182
+ for (const msg of historyMessages) {
183
+ claude.injectHistory(userId, msg.role, msg.content);
184
+ }
185
+
186
+ console.log('\n\x1b[1m' + hr('═') + '\x1b[0m');
187
+ console.log(`\x1b[1m CHAT DEBUG — user ${userId}\x1b[0m`);
188
+ console.log('\x1b[1m' + hr('═') + '\x1b[0m');
189
+
190
+ label(`USER MESSAGE`);
191
+ console.log(opts.message);
192
+
193
+ label('PIPELINE');
194
+
195
+ const start = Date.now();
196
+ const bg = new BackgroundRunner();
197
+
198
+ const context = {
199
+ chatId: userId,
200
+ userId,
201
+ verbose: true,
202
+ bg,
203
+ scheduler: null,
204
+ toolPrefs: new Map(),
205
+ messageLog,
206
+ config,
207
+ _verboseNotify: (msg) => console.log(` \x1b[2m${msg}\x1b[0m`),
208
+ _onRouteDecision: ({ model, needMemory }) => {
209
+ console.log(` \x1b[2m[route] model=${model} memory=${needMemory}\x1b[0m`);
210
+ },
211
+ _onRouteUpdate: ({ memoryCount, model }) => {
212
+ if (memoryCount !== undefined) console.log(` \x1b[2m[route] memory=${memoryCount} facts injected\x1b[0m`);
213
+ if (model) console.log(` \x1b[2m[route] escalated → ${model}\x1b[0m`);
214
+ },
215
+ _onToolStart: (toolName, inputSummary) => {
216
+ console.log(` \x1b[2m[tool] ${toolName}${inputSummary ? ': ' + inputSummary : ''}\x1b[0m`);
217
+ },
218
+ _onPromptReady: (params) => {
219
+ printFullPrompt(params);
220
+ },
221
+ };
222
+
223
+ if (messageLog) {
224
+ messageLog.log(userId, 'user', opts.message);
225
+ }
226
+
227
+ const { text: response, usage, model } = await claude.chat(opts.message, context);
228
+
229
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
230
+
231
+ if (messageLog) {
232
+ messageLog.log(userId, 'assistant', response, {
233
+ model,
234
+ tokensIn: usage?.input_tokens,
235
+ tokensOut: usage?.output_tokens,
236
+ });
237
+ }
238
+
239
+ const tokIn = usage?.input_tokens >= 1000
240
+ ? `${(usage.input_tokens / 1000).toFixed(1)}k`
241
+ : usage?.input_tokens ?? '?';
242
+ const tokOut = usage?.output_tokens >= 1000
243
+ ? `${(usage.output_tokens / 1000).toFixed(1)}k`
244
+ : usage?.output_tokens ?? '?';
245
+
246
+ label(`RESPONSE \x1b[2m${model} | ${tokIn} in / ${tokOut} out | ${elapsed}s\x1b[0m`);
247
+ console.log(response ?? '(no response)');
248
+ console.log('\n' + hr('═') + '\n');
249
+
250
+ if (opts.log) {
251
+ console.log('\x1b[2mLogged to obol_messages\x1b[0m\n');
252
+ }
253
+ }
254
+
255
+ async function promptDebug(opts) {
256
+ const config = loadConfig();
257
+ if (!config) { console.error('No config found — run: obol init'); process.exit(1); }
258
+ if (!config.supabase?.url || !config.supabase?.serviceKey) { console.error('Supabase not configured'); process.exit(1); }
259
+
260
+ if (opts.message) {
261
+ await runChat(opts, config);
262
+ } else {
263
+ await runInspect(opts, config);
264
+ }
265
+ }
266
+
267
+ async function main() {
268
+ await promptDebug(parseArgs());
269
+ }
270
+
271
+ if (require.main === module) {
272
+ main().catch(e => { console.error(e.message); process.exit(1); });
273
+ }
274
+
275
+ module.exports = { promptDebug };
package/src/config.js CHANGED
@@ -161,11 +161,6 @@ function ensureUserDir(userId) {
161
161
  if (fs.existsSync(defaultAgents) && !fs.existsSync(targetAgents)) {
162
162
  fs.copyFileSync(defaultAgents, targetAgents);
163
163
  }
164
- const defaultTraits = path.join(__dirname, 'defaults', 'traits.json');
165
- const targetTraits = path.join(dir, 'personality', 'traits.json');
166
- if (fs.existsSync(defaultTraits) && !fs.existsSync(targetTraits)) {
167
- fs.copyFileSync(defaultTraits, targetTraits);
168
- }
169
164
  return dir;
170
165
  }
171
166
 
@@ -2,7 +2,6 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { execFileSync } = require('child_process');
4
4
  const { OBOL_DIR } = require('../config');
5
- const { loadTraits, saveTraits } = require('../personality');
6
5
  const { isValidNpmPackage } = require('../sanitize');
7
6
  const { loadEvolutionState, saveEvolutionState } = require('./state');
8
7
  const { readDir, syncDir } = require('./filesystem');
@@ -33,7 +32,6 @@ async function evolve(claudeClient, messageLog, memory, userDir, supabaseConfig
33
32
  const currentSoul = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
34
33
  const currentUser = fs.existsSync(userPath) ? fs.readFileSync(userPath, 'utf-8') : '';
35
34
  const currentAgents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf-8') : '';
36
- const currentTraits = loadTraits(userPersonalityDir);
37
35
  const currentScripts = readDir(scriptsDir);
38
36
  const currentTests = readDir(testsDir);
39
37
  const currentCommands = readDir(commandsDir);
@@ -192,8 +190,7 @@ Produce a structured growth report covering:
192
190
  3. RELATIONSHIP SHIFTS — How the dynamic with the owner changed (closer, more trust, new friction, etc.)
193
191
  4. BEHAVIORAL PATTERNS — Recurring interaction styles or habits observed
194
192
  5. GROWTH EDGES — Areas 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
193
+ 6. IDENTITY CONTINUITYWhat core aspects stayed the same and should be preserved
197
194
 
198
195
  Be specific. Cite evidence from the conversations, memories, and self-memories. This report guides the evolution rewrite.`,
199
196
  messages: [{
@@ -204,9 +201,6 @@ ${previousSoul || '(not available)'}
204
201
  ## Current SOUL
205
202
  ${currentSoul || '(empty)'}
206
203
 
207
- ## Current Traits
208
- ${JSON.stringify(currentTraits)}
209
-
210
204
  ## New Memories Since Last Evolution (${recentMemories.length})
211
205
  ${recentMemorySummary || '(none)'}
212
206
 
@@ -242,7 +236,6 @@ A pre-evolution analysis has been conducted comparing your previous state agains
242
236
  lastEvolution: state.lastEvolution,
243
237
  firstEvolutionPreamble,
244
238
  growthPreamble,
245
- currentTraits,
246
239
  baselineResults,
247
240
  });
248
241
 
@@ -431,19 +424,6 @@ Fix the scripts. Tests define correct behavior.`
431
424
  fs.writeFileSync(agentsPath, result.agents);
432
425
  }
433
426
 
434
- if (result.traits && typeof result.traits === 'object') {
435
- const validTraits = {};
436
- for (const [key, val] of Object.entries(result.traits)) {
437
- if (typeof val === 'number' && val >= 0 && val <= 100) {
438
- validTraits[key] = Math.round(val);
439
- }
440
- }
441
- if (Object.keys(validTraits).length > 0) {
442
- const merged = { ...currentTraits, ...validTraits };
443
- saveTraits(personalityDir, merged);
444
- }
445
- }
446
-
447
427
  if (result.commands && typeof result.commands === 'object') {
448
428
  if (Object.keys(result.commands).length > 0 || Object.keys(currentCommands).length > 0) {
449
429
  syncDir(commandsDir, result.commands);
@@ -1,4 +1,4 @@
1
- function buildEvolutionPrompt({ evolutionNumber, lastEvolution, firstEvolutionPreamble, growthPreamble, currentTraits, baselineResults }) {
1
+ function buildEvolutionPrompt({ evolutionNumber, lastEvolution, firstEvolutionPreamble, growthPreamble, baselineResults }) {
2
2
  return `You are an AI undergoing evolution #${evolutionNumber}. ${lastEvolution ? `Last evolution: ${lastEvolution}.` : 'This is your first evolution.'}
3
3
  ${firstEvolutionPreamble}${growthPreamble}
4
4
 
@@ -24,22 +24,6 @@ Operational manual written as instructions to yourself. Focus on owner-specific
24
24
 
25
25
  **What belongs in AGENTS.md:** Memory Strategy, Self-Extending patterns, Scripts & Service Integrations, Background Task Guidelines, Communication Style, Evolution notes, and any owner-specific workflows or lessons discovered from conversations. Keep what works, remove what doesn't.
26
26
 
27
- ## Part 3b: Personality Traits
28
-
29
- Current trait values: ${JSON.stringify(currentTraits)}
30
-
31
- Based on conversation patterns, adjust each trait (0-100). Consider:
32
- - Does the owner respond well to humor? Increase/decrease humor.
33
- - Does the owner prefer direct answers? Adjust directness.
34
- - Does the owner appreciate creative solutions? Adjust creativity.
35
- - Does the owner share emotions or stay task-focused? Adjust empathy.
36
- - Does the owner want blunt truth or diplomatic framing? Adjust honesty.
37
- - Does the owner welcome proactive questions? Adjust curiosity.
38
-
39
- Small adjustments (±5-15) per evolution. Don't swing wildly.
40
-
41
- Include in output JSON as: "traits": { "humor": 65, "honesty": 80, ... }
42
-
43
27
  ## Part 4: Scripts
44
28
 
45
29
  Review and refactor every script. Standards:
@@ -156,7 +140,6 @@ The OBOL directory has a FIXED structure: personality/, scripts/, tests/, comman
156
140
  "soul": "full SOUL.md content",
157
141
  "user": "full USER.md content",
158
142
  "agents": "full AGENTS.md content",
159
- "traits": { "humor": 65, "honesty": 80, "directness": 70, "curiosity": 75, "empathy": 65, "creativity": 70 },
160
143
  "scripts": { "name.js": "content" },
161
144
  "tests": { "test-name.js": "content" },
162
145
  "commands": { "name.md": "content" },
package/src/messages.js CHANGED
@@ -41,7 +41,8 @@ class MessageLog {
41
41
  }
42
42
 
43
43
  async log(chatId, role, content, opts = {}) {
44
- const truncated = content.substring(0, 50000);
44
+ const serialized = typeof content === 'string' ? content : JSON.stringify(content);
45
+ const truncated = serialized.substring(0, 50000);
45
46
 
46
47
  try {
47
48
  await fetch(`${this.url}/rest/v1/obol_messages`, {
@@ -60,7 +61,7 @@ class MessageLog {
60
61
  console.error('[messages] Log failed:', e.message);
61
62
  }
62
63
 
63
- if (role === 'user') {
64
+ if (role === 'user' && typeof content === 'string') {
64
65
  this._lastUserMessage.set(chatId, truncated);
65
66
  }
66
67
 
@@ -83,7 +84,12 @@ class MessageLog {
83
84
  { headers: this.headers }
84
85
  );
85
86
  const data = await res.json();
86
- const rows = data.reverse();
87
+ const rows = data.reverse().map(row => {
88
+ if (typeof row.content === 'string' && row.content.startsWith('[')) {
89
+ try { row.content = JSON.parse(row.content); } catch {}
90
+ }
91
+ return row;
92
+ });
87
93
  const firstUserIdx = rows.findIndex(r => r.role === 'user');
88
94
  return firstUserIdx > 0 ? rows.slice(firstUserIdx) : rows;
89
95
  } catch {
@@ -194,7 +200,7 @@ Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
194
200
  for (const fact of facts.slice(0, 5)) {
195
201
  if (!fact.content || fact.content.length <= 10) continue;
196
202
  try {
197
- const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.92 });
203
+ const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.82 });
198
204
  if (existing.length > 0) { duped++; continue; }
199
205
  } catch {}
200
206
  const category = validCategories.has(fact.category) ? fact.category : 'fact';
@@ -2,8 +2,6 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { OBOL_DIR } = require('./config');
4
4
 
5
- const DEFAULT_TRAITS = require('./defaults/traits.json');
6
-
7
5
  function loadPersonality(sharedDir, userDir) {
8
6
  sharedDir = sharedDir || path.join(OBOL_DIR, 'personality');
9
7
  userDir = userDir || sharedDir;
@@ -22,25 +20,7 @@ function loadPersonality(sharedDir, userDir) {
22
20
  }
23
21
  }
24
22
 
25
- personality.traits = loadTraits(userDir);
26
-
27
23
  return personality;
28
24
  }
29
25
 
30
- function loadTraits(dir) {
31
- dir = dir || path.join(OBOL_DIR, 'personality');
32
- const traitsPath = path.join(dir, 'traits.json');
33
- try {
34
- return JSON.parse(fs.readFileSync(traitsPath, 'utf-8'));
35
- } catch {
36
- return { ...DEFAULT_TRAITS };
37
- }
38
- }
39
-
40
- function saveTraits(dir, traits) {
41
- dir = dir || path.join(OBOL_DIR, 'personality');
42
- fs.mkdirSync(dir, { recursive: true });
43
- fs.writeFileSync(path.join(dir, 'traits.json'), JSON.stringify(traits, null, 2));
44
- }
45
-
46
- module.exports = { loadPersonality, loadTraits, saveTraits, DEFAULT_TRAITS };
26
+ module.exports = { loadPersonality };
@@ -11,7 +11,6 @@ const memoryCommands = require('./commands/memory');
11
11
  const statusCommands = require('./commands/status');
12
12
  const conversationCommands = require('./commands/conversation');
13
13
  const adminCommands = require('./commands/admin');
14
- const traitsCommands = require('./commands/traits');
15
14
  const secretsCommands = require('./commands/secrets');
16
15
  const toolsCommands = require('./commands/tools');
17
16
  const { registerTextHandler } = require('./handlers/text');
@@ -97,7 +96,6 @@ function createBot(telegramConfig, config) {
97
96
  { command: 'status', description: 'Bot status and uptime' },
98
97
  { command: 'backup', description: 'Trigger GitHub backup now' },
99
98
  { command: 'clean', description: 'Audit and fix workspace' },
100
- { command: 'traits', description: 'View or adjust personality traits' },
101
99
  { command: 'secret', description: 'Manage per-user secrets' },
102
100
  { command: 'evolution', description: 'Evolution progress' },
103
101
  { command: 'verbose', description: 'Toggle verbose mode on/off' },
@@ -112,7 +110,6 @@ function createBot(telegramConfig, config) {
112
110
  memoryCommands.register(bot, config);
113
111
  statusCommands.register(bot, config);
114
112
  adminCommands.register(bot, config, createAsk);
115
- traitsCommands.register(bot, config);
116
113
  secretsCommands.register(bot, config);
117
114
  toolsCommands.register(bot, config);
118
115
 
@@ -55,12 +55,12 @@ function register(bot, config, createAsk) {
55
55
  ## Workspace Structure
56
56
  Allowed root directories: personality/, scripts/, tests/, commands/, apps/, logs/, assets/
57
57
  Allowed root files: config.json, secrets.json, .evolution-state.json, .first-run-done, .post-setup-done
58
- - personality/ and commands/ only contain .md files (except personality/traits.json which must stay)
58
+ - personality/ and commands/ only contain .md files
59
59
  - Unknown directories at the root should be moved into apps/
60
60
  - Script files (.js, .ts, .sh, etc.) go into scripts/
61
61
  - Asset files (images, audio, pdf, etc.) go into assets/
62
62
  - .DS_Store and other dotfiles should be deleted
63
- - secrets.json and personality/traits.json must NOT be moved
63
+ - secrets.json must NOT be moved
64
64
 
65
65
  ## Issues Found
66
66
  ${issueLines}
@@ -46,7 +46,6 @@ function register(bot, config) {
46
46
  /events — Upcoming scheduled events
47
47
  /tasks — Running background tasks
48
48
  /tools — Toggle optional tools on/off
49
- /traits — View/adjust personality traits
50
49
  /secret — Manage per-user secrets
51
50
  /evolution — Evolution progress
52
51
  /status — Bot status and uptime
@@ -1,9 +1,7 @@
1
- const path = require('path');
2
1
  const { getTenant } = require('../../tenant');
3
- const { loadTraits } = require('../../personality');
4
2
  const { loadEvolutionState } = require('../../evolve');
5
3
  const { getMaxToolIterations } = require('../../claude');
6
- const { termBar, formatTraits } = require('../utils');
4
+ const { termBar } = require('../utils');
7
5
  const { TERM_SEP } = require('../constants');
8
6
 
9
7
  function register(bot, config) {
@@ -52,10 +50,6 @@ function register(bot, config) {
52
50
  lines.push(` last ${new Date(evoState.lastEvolution).toLocaleDateString()}`);
53
51
  }
54
52
 
55
- const personalityDir = path.join(tenant.userDir, 'personality');
56
- const traits = loadTraits(personalityDir);
57
- lines.push(``, `TRAITS`);
58
- lines.push(formatTraits(traits));
59
53
  lines.push(TERM_SEP);
60
54
 
61
55
  await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
@@ -54,14 +54,6 @@ function startTyping(ctx) {
54
54
  return () => clearInterval(interval);
55
55
  }
56
56
 
57
- function formatTraits(traits) {
58
- const maxLen = Math.max(...Object.keys(traits).map(k => k.length));
59
- return Object.entries(traits).map(([name, val]) => {
60
- const label = (name.charAt(0).toUpperCase() + name.slice(1)).padEnd(maxLen + 1);
61
- return ` ${label}${termBar(val)} ${val}`;
62
- }).join('\n');
63
- }
64
-
65
57
  function splitMessage(text, maxLength) {
66
58
  const chunks = [];
67
59
  let remaining = text;
@@ -78,4 +70,4 @@ function splitMessage(text, maxLength) {
78
70
  return chunks;
79
71
  }
80
72
 
81
- module.exports = { termBar, markdownToTelegramHtml, sendHtml, editHtml, startTyping, formatTraits, splitMessage };
73
+ module.exports = { termBar, markdownToTelegramHtml, sendHtml, editHtml, startTyping, splitMessage };
@@ -1,8 +0,0 @@
1
- {
2
- "humor": 60,
3
- "honesty": 80,
4
- "directness": 70,
5
- "curiosity": 75,
6
- "empathy": 65,
7
- "creativity": 70
8
- }
@@ -1,50 +0,0 @@
1
- const path = require('path');
2
- const { getTenant } = require('../../tenant');
3
- const { loadTraits, saveTraits, DEFAULT_TRAITS } = require('../../personality');
4
- const { formatTraits } = require('../utils');
5
- const { TERM_SEP } = require('../constants');
6
-
7
- function register(bot, config) {
8
- const botName = config.bot?.name || 'OBOL';
9
- bot.command('traits', async (ctx) => {
10
- if (!ctx.from) return;
11
- const tenant = await getTenant(ctx.from.id, config);
12
- const personalityDir = path.join(tenant.userDir, 'personality');
13
- const args = ctx.message.text.split(' ').slice(1);
14
-
15
- if (args[0] === 'reset') {
16
- saveTraits(personalityDir, { ...DEFAULT_TRAITS });
17
- tenant.claude.reloadPersonality();
18
- const traits = { ...DEFAULT_TRAITS };
19
- const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, `RESET TO DEFAULTS`, ``, formatTraits(traits), TERM_SEP];
20
- await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
21
- return;
22
- }
23
-
24
- if (args[0] && args[1]) {
25
- const traitName = args[0].toLowerCase();
26
- const value = parseInt(args[1], 10);
27
- if (!(traitName in DEFAULT_TRAITS)) {
28
- await ctx.reply(`Unknown trait: ${traitName}\nValid: ${Object.keys(DEFAULT_TRAITS).join(', ')}`);
29
- return;
30
- }
31
- if (isNaN(value) || value < 0 || value > 100) {
32
- await ctx.reply('Value must be 0-100');
33
- return;
34
- }
35
- const traits = loadTraits(personalityDir);
36
- traits[traitName] = value;
37
- saveTraits(personalityDir, traits);
38
- tenant.claude.reloadPersonality();
39
- const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, `UPDATED ${traitName} → ${value}`, ``, formatTraits(traits), TERM_SEP];
40
- await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
41
- return;
42
- }
43
-
44
- const traits = loadTraits(personalityDir);
45
- const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, ``, formatTraits(traits), ``, `/traits &lt;name&gt; &lt;0-100&gt;`, `/traits reset`, TERM_SEP];
46
- await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
47
- });
48
- }
49
-
50
- module.exports = { register };