obol-ai 0.3.3 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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": {
@@ -267,8 +267,9 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
267
267
  }
268
268
 
269
269
  function reloadPersonality() {
270
+ const { PERSONALITY_DIR } = require('../soul');
270
271
  const pDir = userDir ? path.join(userDir, 'personality') : undefined;
271
- const newPersonality = require('../personality').loadPersonality(pDir);
272
+ const newPersonality = require('../personality').loadPersonality(PERSONALITY_DIR, pDir);
272
273
  for (const key of Object.keys(personality)) delete personality[key];
273
274
  Object.assign(personality, newPersonality);
274
275
  baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
@@ -36,7 +36,7 @@ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision
36
36
  Reply with ONLY a JSON object:
37
37
  {"need_memory": true/false, "search_queries": ["query1", "query2"], "model": "haiku|sonnet|opus"}
38
38
 
39
- search_queries: 1-3 optimized search queries based on the full conversation context. Cover distinct topics, people, or entities referenced. Single-topic messages need just one query.
39
+ search_queries: 1-5 optimized search queries based on the full conversation context. Cover distinct topics, people, entities, time periods, or projects referenced. Single-topic messages need just one query. Use more queries when the message references multiple people, projects, or threads.
40
40
 
41
41
  Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true.
42
42
 
@@ -73,12 +73,13 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
73
73
 
74
74
  if (decision.need_memory && memory) {
75
75
  const budget = decision.model === 'opus' ? 40 : decision.model === 'haiku' ? 15 : 25;
76
+ const poolPerQuery = decision.model === 'opus' ? 20 : decision.model === 'haiku' ? 10 : 15;
76
77
  const searchQueries = queries.length > 0 ? queries : [userMessage];
77
78
 
78
79
  const recentMemories = await memory.byDate('7d', { limit: Math.ceil(budget / 3) });
79
80
 
80
81
  const semanticResults = await Promise.all(
81
- searchQueries.map(q => memory.search(q, { limit: Math.ceil(budget / searchQueries.length), threshold: 0.4 }))
82
+ searchQueries.map(q => memory.search(q, { limit: poolPerQuery, threshold: 0.4 }))
82
83
  );
83
84
  const semanticMemories = semanticResults.flat();
84
85
 
@@ -96,7 +96,10 @@ const definitions = [
96
96
  const handlers = {
97
97
  async read_file(input, memory, context) {
98
98
  const { userDir } = context;
99
- const filePath = userDir ? resolveUserPath(input.path, userDir) : input.path;
99
+ const { PERSONALITY_DIR } = require('../../soul');
100
+ const filePath = path.basename(input.path) === 'SOUL.md'
101
+ ? path.join(PERSONALITY_DIR, 'SOUL.md')
102
+ : (userDir ? resolveUserPath(input.path, userDir) : input.path);
100
103
  if (filePath.toLowerCase().endsWith('.pdf')) {
101
104
  const pdfParse = require('pdf-parse');
102
105
  const { text } = await pdfParse(fs.readFileSync(filePath));
@@ -119,25 +122,36 @@ const handlers = {
119
122
 
120
123
  async write_file(input, memory, context) {
121
124
  const { userDir } = context;
122
- const filePath = userDir ? resolveUserPath(input.path, userDir) : input.path;
125
+ const { PERSONALITY_DIR, backup } = require('../../soul');
126
+ const isSoul = path.basename(input.path) === 'SOUL.md';
127
+ const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
123
128
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
124
129
  fs.writeFileSync(filePath, input.content);
125
130
  if (filePath.includes('personality/')) {
126
131
  context._reloadPersonality?.();
132
+ if (isSoul && context.config?.supabase) {
133
+ backup(context.config.supabase, 'soul', input.content).catch(() => {});
134
+ }
127
135
  }
128
136
  return `Written: ${filePath}`;
129
137
  },
130
138
 
131
139
  async edit_file(input, memory, context) {
132
140
  const { userDir } = context;
133
- const filePath = userDir ? resolveUserPath(input.path, userDir) : input.path;
141
+ const { PERSONALITY_DIR, backup } = require('../../soul');
142
+ const isSoul = path.basename(input.path) === 'SOUL.md';
143
+ const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
134
144
  const content = fs.readFileSync(filePath, 'utf-8');
135
145
  const count = content.split(input.old_string).length - 1;
136
146
  if (count === 0) return `Error: old_string not found in ${input.path}`;
137
147
  if (count > 1) return `Error: old_string matches ${count} times — add more context to make it unique`;
138
- fs.writeFileSync(filePath, content.replace(input.old_string, input.new_string));
148
+ const updated = content.replace(input.old_string, input.new_string);
149
+ fs.writeFileSync(filePath, updated);
139
150
  if (filePath.includes('personality/')) {
140
151
  context._reloadPersonality?.();
152
+ if (isSoul && context.config?.supabase) {
153
+ backup(context.config.supabase, 'soul', updated).catch(() => {});
154
+ }
141
155
  }
142
156
  return `Edited: ${filePath}`;
143
157
  },
package/src/cli/config.js CHANGED
@@ -353,38 +353,40 @@ async function runOAuthFlow(cfg) {
353
353
  }
354
354
 
355
355
  function updatePersonalityNames(oldBotName, newBotName, oldOwnerName, newOwnerName) {
356
+ const { PERSONALITY_DIR } = require('../soul');
357
+
358
+ if (oldBotName !== newBotName) {
359
+ const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
360
+ if (fs.existsSync(soulPath)) {
361
+ let content = fs.readFileSync(soulPath, 'utf-8');
362
+ content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
363
+ content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
364
+ fs.writeFileSync(soulPath, content, 'utf-8');
365
+ }
366
+ }
367
+
368
+ if (oldOwnerName !== newOwnerName) {
369
+ const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
370
+ if (fs.existsSync(soulPath)) {
371
+ let content = fs.readFileSync(soulPath, 'utf-8');
372
+ content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
373
+ fs.writeFileSync(soulPath, content, 'utf-8');
374
+ }
375
+ }
376
+
356
377
  if (!fs.existsSync(USERS_DIR)) return;
357
378
  const users = fs.readdirSync(USERS_DIR).filter(u => {
358
379
  try { return fs.statSync(path.join(USERS_DIR, u)).isDirectory(); } catch { return false; }
359
380
  });
360
381
  for (const userId of users) {
361
- const personalityDir = path.join(USERS_DIR, userId, 'personality');
362
- if (!fs.existsSync(personalityDir)) continue;
363
-
364
382
  if (oldBotName !== newBotName) {
365
- const soulPath = path.join(personalityDir, 'SOUL.md');
366
- if (fs.existsSync(soulPath)) {
367
- let content = fs.readFileSync(soulPath, 'utf-8');
368
- content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
369
- content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
370
- fs.writeFileSync(soulPath, content, 'utf-8');
371
- }
372
- const agentsPath = path.join(personalityDir, 'AGENTS.md');
383
+ const agentsPath = path.join(USERS_DIR, userId, 'personality', 'AGENTS.md');
373
384
  if (fs.existsSync(agentsPath)) {
374
385
  let content = fs.readFileSync(agentsPath, 'utf-8');
375
386
  content = content.replace(new RegExp(`# AGENTS\\.md — How ${oldBotName} Works`, 'g'), `# AGENTS.md — How ${newBotName} Works`);
376
387
  fs.writeFileSync(agentsPath, content, 'utf-8');
377
388
  }
378
389
  }
379
-
380
- if (oldOwnerName !== newOwnerName) {
381
- const soulPath = path.join(personalityDir, 'SOUL.md');
382
- if (fs.existsSync(soulPath)) {
383
- let content = fs.readFileSync(soulPath, 'utf-8');
384
- content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
385
- fs.writeFileSync(soulPath, content, 'utf-8');
386
- }
387
- }
388
390
  }
389
391
  }
390
392
 
package/src/cli/init.js CHANGED
@@ -752,13 +752,12 @@ function ensureDirs() {
752
752
  }
753
753
 
754
754
  function createPersonalityFiles(config) {
755
- for (const userId of config.telegram.allowedUsers) {
756
- const ownerName = config.users?.[String(userId)]?.name || config.owner.name;
757
- const personalityDir = path.join(OBOL_DIR, 'users', String(userId), 'personality');
758
- fs.mkdirSync(personalityDir, { recursive: true });
755
+ const { PERSONALITY_DIR } = require('../soul');
756
+ fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
757
+ const ownerName = config.users?.[String(config.telegram.allowedUsers[0])]?.name || config.owner.name;
759
758
 
760
- if (!fs.existsSync(path.join(personalityDir, 'SOUL.md'))) {
761
- fs.writeFileSync(path.join(personalityDir, 'SOUL.md'), `# SOUL.md — Who is ${config.bot.name}?
759
+ if (!fs.existsSync(path.join(PERSONALITY_DIR, 'SOUL.md'))) {
760
+ fs.writeFileSync(path.join(PERSONALITY_DIR, 'SOUL.md'), `# SOUL.md — Who is ${config.bot.name}?
762
761
 
763
762
  Write your bot's personality here. This shapes how it talks, thinks, and behaves.
764
763
 
@@ -781,7 +780,13 @@ Write your bot's personality here. This shapes how it talks, thinks, and behaves
781
780
  ---
782
781
  *Edit this file anytime to reshape your bot's personality.*
783
782
  `);
784
- }
783
+ }
784
+
785
+ for (const userId of config.telegram.allowedUsers) {
786
+ const ownerName = config.users?.[String(userId)]?.name || config.owner.name;
787
+ const personalityDir = path.join(OBOL_DIR, 'users', String(userId), 'personality');
788
+ fs.mkdirSync(personalityDir, { recursive: true });
789
+
785
790
  if (!fs.existsSync(path.join(personalityDir, 'USER.md'))) {
786
791
  fs.writeFileSync(path.join(personalityDir, 'USER.md'), `# USER.md — About ${ownerName}
787
792
 
@@ -76,13 +76,22 @@ function truncate(str, max = 500) {
76
76
  }
77
77
 
78
78
  function printFullPrompt(params) {
79
- label('FULL PROMPT → SYSTEM');
79
+ label(`FULL PROMPT → SYSTEM \x1b[2m[${params.model || '?'}]\x1b[0m`);
80
80
  for (const block of (params.system || [])) {
81
81
  const cached = block.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
82
82
  console.log(`${truncate(block.text, 800)}${cached}`);
83
83
  console.log(hr('·'));
84
84
  }
85
85
 
86
+ if (params.tools?.length) {
87
+ label(`FULL PROMPT → TOOLS [${params.tools.length}]`);
88
+ for (const tool of params.tools) {
89
+ const cached = tool.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
90
+ const desc = tool.description ? ` \x1b[2m${truncate(tool.description, 100)}\x1b[0m` : '';
91
+ console.log(` \x1b[35m${tool.name}\x1b[0m${cached}${desc}`);
92
+ }
93
+ }
94
+
86
95
  label(`FULL PROMPT → MESSAGES [${params.messages.length}]`);
87
96
  for (let i = 0; i < params.messages.length; i++) {
88
97
  const msg = params.messages[i];
@@ -92,7 +101,15 @@ function printFullPrompt(params) {
92
101
  : msg.content;
93
102
  for (const block of blocks) {
94
103
  if (block.type === 'text') {
95
- console.log(truncate(block.text, 1000));
104
+ if (block.text.startsWith('[Runtime context')) {
105
+ console.log(`\x1b[2m── runtime metadata ──\x1b[0m`);
106
+ } else if (block.text.startsWith('Current time:')) {
107
+ console.log(`\x1b[2m${block.text}\x1b[0m`);
108
+ } else if (block.text.includes('## Relevant memories') || block.text.includes('## Self-knowledge')) {
109
+ console.log(`\x1b[33m${block.text}\x1b[0m`);
110
+ } else {
111
+ console.log(truncate(block.text, 1000));
112
+ }
96
113
  } else if (block.type === 'tool_use') {
97
114
  console.log(`\x1b[35m[tool_use: ${block.name}]\x1b[0m ${truncate(JSON.stringify(block.input), 200)}`);
98
115
  } else if (block.type === 'tool_result') {
@@ -131,10 +148,11 @@ async function runInspect(opts, config) {
131
148
  }
132
149
  }
133
150
 
134
- label('RECENT MEMORY 20 entries');
151
+ const recentLimit = Math.ceil(25 / 3);
152
+ label(`RECENT MEMORY — ${recentLimit} entries \x1b[2m(budget/3, sonnet)\x1b[0m`);
135
153
  const memHeaders = makeSupabaseHeaders(config.supabase);
136
154
  const memRes = await fetch(
137
- `${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=20&select=content,category,importance,tags,created_at`,
155
+ `${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=${recentLimit}&select=content,category,importance,tags,created_at`,
138
156
  { headers: memHeaders }
139
157
  );
140
158
  const memories = await memRes.json();
@@ -148,6 +166,23 @@ async function runInspect(opts, config) {
148
166
  }
149
167
  }
150
168
 
169
+ label('OBOL SELF MEMORY — 20 entries');
170
+ const selfRes = await fetch(
171
+ `${config.supabase.url}/rest/v1/obol_self_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=20&select=content,category,importance,tags,source,created_at`,
172
+ { headers: memHeaders }
173
+ );
174
+ const selfMemories = await selfRes.json();
175
+ if (!selfMemories.length) {
176
+ console.log(' (none)');
177
+ } else {
178
+ for (const m of selfMemories) {
179
+ const tags = m.tags?.length ? ` \x1b[2m[${m.tags.join(', ')}]\x1b[0m` : '';
180
+ const src = m.source ? ` \x1b[2msrc=${m.source}\x1b[0m` : '';
181
+ console.log(`\x1b[2m${new Date(m.created_at).toLocaleString()}\x1b[0m \x1b[35m[${m.category}]\x1b[0m imp=${m.importance}${tags}${src}`);
182
+ console.log(` ${m.content}`);
183
+ }
184
+ }
185
+
151
186
  console.log('\n\x1b[2mTip: pass -m "message" to run the full production pipeline\x1b[0m\n');
152
187
  }
153
188
 
package/src/messages.js CHANGED
@@ -195,24 +195,32 @@ Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
195
195
  if (Array.isArray(facts) && facts.length > 0) {
196
196
  const validCategories = new Set(['fact','preference','decision','lesson','person','project','event','conversation','resource','pattern','context','email']);
197
197
  let stored = 0;
198
+ let updated = 0;
198
199
  let duped = 0;
199
200
 
200
201
  for (const fact of facts.slice(0, 5)) {
201
202
  if (!fact.content || fact.content.length <= 10) continue;
202
- try {
203
- const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.82 });
204
- if (existing.length > 0) { duped++; continue; }
205
- } catch {}
206
203
  const category = validCategories.has(fact.category) ? fact.category : 'fact';
207
204
  const importance = typeof fact.importance === 'number' ? Math.min(1, Math.max(0, fact.importance)) : 0.5;
208
205
  const tags = Array.isArray(fact.tags) ? fact.tags.slice(0, 5) : [];
206
+ try {
207
+ const related = await this.memory.search(fact.content, { limit: 1, threshold: 0.65 });
208
+ if (related.length > 0) {
209
+ const top = related[0];
210
+ if (top.similarity >= 0.82) { duped++; continue; }
211
+ await this.memory.update(top.id, { content: fact.content, category, importance, tags });
212
+ updated++;
213
+ vlog?.(`[extract] ~[${category}] ${fact.content}`);
214
+ continue;
215
+ }
216
+ } catch {}
209
217
  await this.memory.add(fact.content, { category, tags, importance, source: 'turn-extraction' });
210
218
  stored++;
211
219
  vlog?.(`[extract] +[${category}] ${fact.content}`);
212
220
  }
213
221
 
214
- if (stored > 0 || duped > 0) {
215
- vlog?.(`[extract] ${stored} stored, ${duped} duped, ${facts.length} extracted`);
222
+ if (stored > 0 || updated > 0 || duped > 0) {
223
+ vlog?.(`[extract] ${stored} stored, ${updated} updated, ${duped} duped, ${facts.length} extracted`);
216
224
  }
217
225
  } else {
218
226
  vlog?.('[extract] 0 facts (trivial exchange)');
@@ -84,7 +84,7 @@ Summarize what was cleaned and secrets migrated.`);
84
84
  const taskPrompt = promptParts.join('\n\n');
85
85
 
86
86
  const stopTyping = startTyping(ctx);
87
- const status = createStatusTracker(ctx);
87
+ const status = createStatusTracker(ctx, config.bot?.name);
88
88
  const chatContext = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
89
89
  chatContext._model = 'claude-sonnet-4-6';
90
90
  chatContext._onRouteDecision = (info) => { status.setRouteInfo(info); status.start(); };
@@ -113,7 +113,7 @@ Summarize what was cleaned and secrets migrated.`);
113
113
  const testsAfter = fs.existsSync(testsDir) && fs.readdirSync(testsDir).filter(f => !f.startsWith('.')).length > 0;
114
114
  if (!testsAfter && hasScripts) {
115
115
  const testPrompt = `Read every script in ${plan.baseDir}/scripts/. For each script, write a corresponding test file in ${plan.baseDir}/tests/. Name each test file test-<script-name> (e.g. scripts/gmail-send.py → tests/test-gmail-send.py). After writing all tests, run them and fix any failures until they all pass. Summarize the test results.`;
116
- const testStatus = createStatusTracker(ctx);
116
+ const testStatus = createStatusTracker(ctx, config.bot?.name);
117
117
  const testCtx = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
118
118
  testCtx._model = 'claude-sonnet-4-6';
119
119
  testCtx._onRouteDecision = (info) => { testStatus.setRouteInfo(info); testStatus.start(); };
@@ -19,7 +19,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
19
19
  if (!ctx.from) return;
20
20
  const userId = ctx.from.id;
21
21
  const stopTyping = startTyping(ctx);
22
- const status = createStatusTracker(ctx);
22
+ const status = createStatusTracker(ctx, config.bot?.name);
23
23
 
24
24
  try {
25
25
  const tenant = await getTenant(userId, config);
@@ -74,7 +74,7 @@ async function processSpecial(ctx, prompt, deps) {
74
74
  if (!ctx.from) return;
75
75
  const userId = ctx.from.id;
76
76
  const stopTyping = startTyping(ctx);
77
- const status = createStatusTracker(ctx);
77
+ const status = createStatusTracker(ctx, deps.config?.bot?.name);
78
78
 
79
79
  try {
80
80
  const tenant = await getTenant(userId, deps.config);
@@ -82,12 +82,13 @@ function createChatContext(ctx, tenant, config, { allowedUsers, bot, createAsk }
82
82
  };
83
83
  }
84
84
 
85
- function createStatusTracker(ctx) {
85
+ function createStatusTracker(ctx, botName) {
86
86
  let statusMsgId = null;
87
87
  let statusText = 'Processing';
88
88
  let statusTimer = null;
89
89
  let statusStart = null;
90
90
  let routeInfo = null;
91
+ const title = botName || 'OBOL';
91
92
  const stopBtn = new InlineKeyboard()
92
93
  .text('■ Stop', `stop:${ctx.chat.id}`)
93
94
  .text('■ Force Stop', `force:${ctx.chat.id}`);
@@ -100,14 +101,14 @@ function createStatusTracker(ctx) {
100
101
  const start = () => {
101
102
  if (statusTimer) return;
102
103
  statusStart = Date.now();
103
- const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText });
104
+ const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText, title });
104
105
  ctx.reply(html, { parse_mode: 'HTML', reply_markup: stopBtn }).then(sent => {
105
106
  if (sent) statusMsgId = sent.message_id;
106
107
  }).catch(() => {});
107
108
  statusTimer = setInterval(() => {
108
109
  if (!statusMsgId) return;
109
110
  const elapsed = Math.round((Date.now() - statusStart) / 1000);
110
- const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText });
111
+ const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText, title });
111
112
  ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML', reply_markup: stopBtn }).catch(() => {});
112
113
  }, 5000);
113
114
  };
@@ -126,7 +127,7 @@ function createStatusTracker(ctx) {
126
127
  updateFormatting() {
127
128
  if (!statusMsgId) return;
128
129
  const elapsed = statusStart ? Math.round((Date.now() - statusStart) / 1000) : 0;
129
- const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output' });
130
+ const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output', title });
130
131
  ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML' }).catch(() => {});
131
132
  },
132
133
  deleteMsg() {
@@ -151,7 +152,7 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
151
152
 
152
153
  const chatMessage = replyContext + fullMessage;
153
154
  const stopTyping = startTyping(ctx);
154
- const status = createStatusTracker(ctx);
155
+ const status = createStatusTracker(ctx, config.bot?.name);
155
156
 
156
157
  const batcher = tenant.verbose ? createVerboseBatcher(ctx) : null;
157
158
  try {
package/src/tenant.js CHANGED
@@ -60,7 +60,7 @@ async function createTenant(userId, config) {
60
60
 
61
61
  let personalityMtime = 0;
62
62
  try {
63
- personalityMtime = fs.statSync(path.join(personalityDir, 'SOUL.md')).mtimeMs;
63
+ personalityMtime = fs.statSync(path.join(PERSONALITY_DIR, 'SOUL.md')).mtimeMs;
64
64
  } catch {}
65
65
 
66
66
  return {