kernelbot 1.0.37 → 1.0.38
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/bin/kernel.js +389 -23
- package/config.example.yaml +17 -0
- package/package.json +2 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +209 -0
- package/src/life/engine.js +28 -20
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +104 -57
- package/src/utils/display.js +73 -12
- package/src/utils/temporal-awareness.js +24 -10
package/src/agent.js
CHANGED
|
@@ -11,8 +11,30 @@ import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestra
|
|
|
11
11
|
import { resetClaudeCodeSpawner, getSpawner } from './tools/coding.js';
|
|
12
12
|
import { truncateToolResult } from './utils/truncate.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Format a time gap in minutes into natural, human-readable language.
|
|
16
|
+
* e.g. 45 → "about 45 minutes", 90 → "about an hour and a half",
|
|
17
|
+
* 180 → "about 3 hours", 1500 → "over a day"
|
|
18
|
+
*/
|
|
19
|
+
function formatTimeGap(minutes) {
|
|
20
|
+
if (minutes < 60) return `about ${minutes} minutes`;
|
|
21
|
+
const hours = Math.floor(minutes / 60);
|
|
22
|
+
const remainingMins = minutes % 60;
|
|
23
|
+
if (hours < 24) {
|
|
24
|
+
if (hours === 1 && remainingMins < 15) return 'about an hour';
|
|
25
|
+
if (hours === 1 && remainingMins >= 15 && remainingMins < 45) return 'about an hour and a half';
|
|
26
|
+
if (hours === 1) return 'nearly 2 hours';
|
|
27
|
+
if (remainingMins < 15) return `about ${hours} hours`;
|
|
28
|
+
if (remainingMins >= 30) return `about ${hours} and a half hours`;
|
|
29
|
+
return `about ${hours} hours`;
|
|
30
|
+
}
|
|
31
|
+
const days = Math.floor(hours / 24);
|
|
32
|
+
if (days === 1) return 'over a day';
|
|
33
|
+
return `about ${days} days`;
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
export class OrchestratorAgent {
|
|
15
|
-
constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue }) {
|
|
37
|
+
constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue, characterManager }) {
|
|
16
38
|
this.config = config;
|
|
17
39
|
this.conversationManager = conversationManager;
|
|
18
40
|
this.personaManager = personaManager;
|
|
@@ -21,7 +43,14 @@ export class OrchestratorAgent {
|
|
|
21
43
|
this.automationManager = automationManager || null;
|
|
22
44
|
this.memoryManager = memoryManager || null;
|
|
23
45
|
this.shareQueue = shareQueue || null;
|
|
46
|
+
this.characterManager = characterManager || null;
|
|
47
|
+
this._activePersonaMd = null;
|
|
48
|
+
this._activeCharacterName = null;
|
|
49
|
+
this._activeCharacterId = null;
|
|
24
50
|
this._pending = new Map(); // chatId -> pending state
|
|
51
|
+
|
|
52
|
+
// Character-scoped conversation keys: prefix chatId with character ID
|
|
53
|
+
// so each character has isolated conversation history.
|
|
25
54
|
this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
|
|
26
55
|
|
|
27
56
|
// Orchestrator provider (30s timeout — lean dispatch/summarize calls)
|
|
@@ -49,7 +78,8 @@ export class OrchestratorAgent {
|
|
|
49
78
|
/** Build the orchestrator system prompt. */
|
|
50
79
|
_getSystemPrompt(chatId, user, temporalContext = null) {
|
|
51
80
|
const logger = getLogger();
|
|
52
|
-
const
|
|
81
|
+
const key = this._chatKey(chatId);
|
|
82
|
+
const skillId = this.conversationManager.getSkill(key);
|
|
53
83
|
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
54
84
|
|
|
55
85
|
let userPersona = null;
|
|
@@ -74,23 +104,93 @@ export class OrchestratorAgent {
|
|
|
74
104
|
sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
|
|
75
105
|
}
|
|
76
106
|
|
|
77
|
-
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'}`);
|
|
78
|
-
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext);
|
|
107
|
+
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'} | character=${this._activeCharacterId || 'default'}`);
|
|
108
|
+
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext, this._activePersonaMd, this._activeCharacterName);
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
setSkill(chatId, skillId) {
|
|
82
|
-
this.conversationManager.setSkill(chatId, skillId);
|
|
112
|
+
this.conversationManager.setSkill(this._chatKey(chatId), skillId);
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
clearSkill(chatId) {
|
|
86
|
-
this.conversationManager.clearSkill(chatId);
|
|
116
|
+
this.conversationManager.clearSkill(this._chatKey(chatId));
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
getActiveSkill(chatId) {
|
|
90
|
-
const skillId = this.conversationManager.getSkill(chatId);
|
|
120
|
+
const skillId = this.conversationManager.getSkill(this._chatKey(chatId));
|
|
91
121
|
return skillId ? getUnifiedSkillById(skillId) : null;
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Load a character's context into the agent.
|
|
126
|
+
* Called on startup and on character switch.
|
|
127
|
+
*/
|
|
128
|
+
loadCharacter(characterId) {
|
|
129
|
+
const logger = getLogger();
|
|
130
|
+
if (!this.characterManager) return;
|
|
131
|
+
|
|
132
|
+
const ctx = this.characterManager.buildContext(characterId);
|
|
133
|
+
this._activeCharacterId = characterId;
|
|
134
|
+
this._activePersonaMd = ctx.personaMd;
|
|
135
|
+
this._activeCharacterName = ctx.profile.name;
|
|
136
|
+
this.selfManager = ctx.selfManager;
|
|
137
|
+
this.memoryManager = ctx.memoryManager;
|
|
138
|
+
this.shareQueue = ctx.shareQueue;
|
|
139
|
+
|
|
140
|
+
logger.info(`[Agent] Loaded character: ${ctx.profile.name} (${characterId})`);
|
|
141
|
+
return ctx;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Switch to a different character at runtime.
|
|
146
|
+
* Returns the new context for life engine rebuild.
|
|
147
|
+
*/
|
|
148
|
+
switchCharacter(characterId) {
|
|
149
|
+
const logger = getLogger();
|
|
150
|
+
if (!this.characterManager) throw new Error('CharacterManager not available');
|
|
151
|
+
|
|
152
|
+
const ctx = this.loadCharacter(characterId);
|
|
153
|
+
this.characterManager.setActiveCharacter(characterId);
|
|
154
|
+
|
|
155
|
+
logger.info(`[Agent] Switched to character: ${ctx.profile.name} (${characterId})`);
|
|
156
|
+
return ctx;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get active character info for display. */
|
|
160
|
+
getActiveCharacterInfo() {
|
|
161
|
+
if (!this.characterManager) return null;
|
|
162
|
+
const id = this._activeCharacterId || this.characterManager.getActiveCharacterId();
|
|
163
|
+
return this.characterManager.getCharacter(id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a character-scoped conversation key.
|
|
168
|
+
* Prefixes chatId with the active character ID so each character
|
|
169
|
+
* has its own isolated conversation history.
|
|
170
|
+
* Raw chatId is still used for Telegram callbacks, job events, etc.
|
|
171
|
+
*/
|
|
172
|
+
_chatKey(chatId) {
|
|
173
|
+
if (this._activeCharacterId) {
|
|
174
|
+
return `${this._activeCharacterId}:${chatId}`;
|
|
175
|
+
}
|
|
176
|
+
return String(chatId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Clear conversation history for a chat (character-scoped). */
|
|
180
|
+
clearConversation(chatId) {
|
|
181
|
+
this.conversationManager.clear(this._chatKey(chatId));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Get message count for a chat (character-scoped). */
|
|
185
|
+
getMessageCount(chatId) {
|
|
186
|
+
return this.conversationManager.getMessageCount(this._chatKey(chatId));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Get conversation history for a chat (character-scoped). */
|
|
190
|
+
getConversationHistory(chatId) {
|
|
191
|
+
return this.conversationManager.getHistory(this._chatKey(chatId));
|
|
192
|
+
}
|
|
193
|
+
|
|
94
194
|
/** Return current worker brain info for display. */
|
|
95
195
|
getBrainInfo() {
|
|
96
196
|
const { provider, model } = this.config.brain;
|
|
@@ -306,27 +406,27 @@ export class OrchestratorAgent {
|
|
|
306
406
|
|
|
307
407
|
const { max_tool_depth } = this.config.orchestrator;
|
|
308
408
|
|
|
409
|
+
// Character-scoped conversation key
|
|
410
|
+
const convKey = this._chatKey(chatId);
|
|
411
|
+
|
|
309
412
|
// Detect time gap before adding the new message
|
|
310
413
|
let temporalContext = null;
|
|
311
|
-
const lastTs = this.conversationManager.getLastMessageTimestamp(
|
|
414
|
+
const lastTs = this.conversationManager.getLastMessageTimestamp(convKey);
|
|
312
415
|
if (lastTs) {
|
|
313
416
|
const gapMs = Date.now() - lastTs;
|
|
314
417
|
const gapMinutes = Math.floor(gapMs / 60_000);
|
|
315
418
|
if (gapMinutes >= 30) {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
? `${gapHours} hour(s)`
|
|
319
|
-
: `${gapMinutes} minute(s)`;
|
|
320
|
-
temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic.]`;
|
|
419
|
+
const gapText = formatTimeGap(gapMinutes);
|
|
420
|
+
temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic — consider a fresh greeting.]`;
|
|
321
421
|
logger.info(`Time gap detected for chat ${chatId}: ${gapText}`);
|
|
322
422
|
}
|
|
323
423
|
}
|
|
324
424
|
|
|
325
425
|
// Add user message to persistent history
|
|
326
|
-
this.conversationManager.addMessage(
|
|
426
|
+
this.conversationManager.addMessage(convKey, 'user', userMessage);
|
|
327
427
|
|
|
328
428
|
// Build working messages from compressed history
|
|
329
|
-
const messages = [...this.conversationManager.getSummarizedHistory(
|
|
429
|
+
const messages = [...this.conversationManager.getSummarizedHistory(convKey)];
|
|
330
430
|
|
|
331
431
|
// If an image is attached, upgrade the last user message to a multimodal content array
|
|
332
432
|
if (opts.imageAttachment) {
|
|
@@ -402,6 +502,7 @@ export class OrchestratorAgent {
|
|
|
402
502
|
|
|
403
503
|
this.jobManager.on('job:completed', async (job) => {
|
|
404
504
|
const chatId = job.chatId;
|
|
505
|
+
const convKey = this._chatKey(chatId);
|
|
405
506
|
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
406
507
|
const label = workerDef.label || job.workerType;
|
|
407
508
|
|
|
@@ -416,21 +517,35 @@ export class OrchestratorAgent {
|
|
|
416
517
|
const summary = await this._summarizeJobResult(chatId, job);
|
|
417
518
|
if (summary) {
|
|
418
519
|
logger.debug(`[Orchestrator] Job ${job.id} summary ready (${summary.length} chars) — delivering to user`);
|
|
419
|
-
this.conversationManager.addMessage(
|
|
420
|
-
|
|
520
|
+
this.conversationManager.addMessage(convKey, 'assistant', summary);
|
|
521
|
+
// Try to edit the notification, fall back to new message if edit fails
|
|
522
|
+
try {
|
|
523
|
+
await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
|
|
524
|
+
} catch {
|
|
525
|
+
await this._sendUpdate(chatId, summary).catch(() => {});
|
|
526
|
+
}
|
|
421
527
|
} else {
|
|
422
528
|
// Summary was null — store the fallback
|
|
423
529
|
const fallback = this._buildSummaryFallback(job, label);
|
|
424
530
|
logger.debug(`[Orchestrator] Job ${job.id} using fallback (${fallback.length} chars) — delivering to user`);
|
|
425
|
-
this.conversationManager.addMessage(
|
|
426
|
-
|
|
531
|
+
this.conversationManager.addMessage(convKey, 'assistant', fallback);
|
|
532
|
+
try {
|
|
533
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
|
|
534
|
+
} catch {
|
|
535
|
+
await this._sendUpdate(chatId, fallback).catch(() => {});
|
|
536
|
+
}
|
|
427
537
|
}
|
|
428
538
|
} catch (err) {
|
|
429
539
|
logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
|
|
430
540
|
// Store the fallback so the orchestrator retains context about what happened
|
|
431
541
|
const fallback = this._buildSummaryFallback(job, label);
|
|
432
|
-
this.conversationManager.addMessage(
|
|
433
|
-
|
|
542
|
+
this.conversationManager.addMessage(convKey, 'assistant', fallback);
|
|
543
|
+
// Try to edit notification, fall back to new message
|
|
544
|
+
try {
|
|
545
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
|
|
546
|
+
} catch {
|
|
547
|
+
await this._sendUpdate(chatId, fallback).catch(() => {});
|
|
548
|
+
}
|
|
434
549
|
}
|
|
435
550
|
});
|
|
436
551
|
|
|
@@ -451,14 +566,15 @@ export class OrchestratorAgent {
|
|
|
451
566
|
|
|
452
567
|
this.jobManager.on('job:failed', (job) => {
|
|
453
568
|
const chatId = job.chatId;
|
|
569
|
+
const convKey = this._chatKey(chatId);
|
|
454
570
|
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
455
571
|
const label = workerDef.label || job.workerType;
|
|
456
572
|
|
|
457
573
|
logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
|
|
458
574
|
|
|
459
575
|
const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
|
|
460
|
-
this.conversationManager.addMessage(
|
|
461
|
-
this._sendUpdate(chatId, msg);
|
|
576
|
+
this.conversationManager.addMessage(convKey, 'assistant', msg);
|
|
577
|
+
this._sendUpdate(chatId, msg).catch(() => {});
|
|
462
578
|
});
|
|
463
579
|
|
|
464
580
|
this.jobManager.on('job:cancelled', (job) => {
|
|
@@ -469,7 +585,7 @@ export class OrchestratorAgent {
|
|
|
469
585
|
logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
|
|
470
586
|
|
|
471
587
|
const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
|
|
472
|
-
this._sendUpdate(chatId, msg);
|
|
588
|
+
this._sendUpdate(chatId, msg).catch(() => {});
|
|
473
589
|
});
|
|
474
590
|
}
|
|
475
591
|
|
|
@@ -487,9 +603,17 @@ export class OrchestratorAgent {
|
|
|
487
603
|
|
|
488
604
|
logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
|
|
489
605
|
|
|
490
|
-
// Short results don't need LLM summarization
|
|
491
606
|
const sr = job.structuredResult;
|
|
492
607
|
const resultLen = (job.result || '').length;
|
|
608
|
+
|
|
609
|
+
// Direct coding jobs: Claude Code already produces clean, human-readable output.
|
|
610
|
+
// Skip LLM summarization to avoid timeouts and latency — use the result directly.
|
|
611
|
+
if (job.workerType === 'coding') {
|
|
612
|
+
logger.info(`[Orchestrator] Job ${job.id} is a coding job — using direct result (no LLM summary)`);
|
|
613
|
+
return this._buildSummaryFallback(job, label);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Short results don't need LLM summarization
|
|
493
617
|
if (sr?.structured && resultLen < 500) {
|
|
494
618
|
logger.info(`[Orchestrator] Job ${job.id} result short enough — skipping LLM summary`);
|
|
495
619
|
return this._buildSummaryFallback(job, label);
|
|
@@ -513,7 +637,7 @@ export class OrchestratorAgent {
|
|
|
513
637
|
resultContext = (job.result || 'Done.').slice(0, 8000);
|
|
514
638
|
}
|
|
515
639
|
|
|
516
|
-
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
640
|
+
const history = this.conversationManager.getSummarizedHistory(this._chatKey(chatId));
|
|
517
641
|
|
|
518
642
|
const response = await this.orchestratorProvider.chat({
|
|
519
643
|
system: this._getSystemPrompt(chatId, null),
|
|
@@ -617,7 +741,7 @@ export class OrchestratorAgent {
|
|
|
617
741
|
|
|
618
742
|
// 2. Last 5 user messages from conversation history
|
|
619
743
|
try {
|
|
620
|
-
const history = this.conversationManager.getSummarizedHistory(job.chatId);
|
|
744
|
+
const history = this.conversationManager.getSummarizedHistory(this._chatKey(job.chatId));
|
|
621
745
|
const userMessages = history
|
|
622
746
|
.filter(m => m.role === 'user' && typeof m.content === 'string')
|
|
623
747
|
.slice(-5)
|
|
@@ -740,14 +864,26 @@ export class OrchestratorAgent {
|
|
|
740
864
|
|
|
741
865
|
// Get scoped tools and skill
|
|
742
866
|
const tools = getToolsForWorker(job.workerType);
|
|
743
|
-
const skillId = this.conversationManager.getSkill(chatId);
|
|
867
|
+
const skillId = this.conversationManager.getSkill(this._chatKey(chatId));
|
|
744
868
|
|
|
745
869
|
// Build worker context (conversation history, persona, dependency results)
|
|
746
870
|
const workerContext = this._buildWorkerContext(job);
|
|
871
|
+
|
|
872
|
+
// Social platform credential check — require at least one platform connected
|
|
873
|
+
let workerConfig = this.config;
|
|
874
|
+
if (job.workerType === 'social') {
|
|
875
|
+
const hasLinkedIn = this.config.linkedin?.access_token && this.config.linkedin?.person_urn;
|
|
876
|
+
const hasX = this.config.x?.consumer_key && this.config.x?.consumer_secret && this.config.x?.access_token && this.config.x?.access_token_secret;
|
|
877
|
+
if (!hasLinkedIn && !hasX) {
|
|
878
|
+
this.jobManager.failJob(job.id, 'No social accounts connected. Use /linkedin link or /x link to connect an account first.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
747
883
|
logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}, context=${workerContext ? 'yes' : 'none'}`);
|
|
748
884
|
|
|
749
885
|
const worker = new WorkerAgent({
|
|
750
|
-
config:
|
|
886
|
+
config: workerConfig,
|
|
751
887
|
workerType: job.workerType,
|
|
752
888
|
jobId: job.id,
|
|
753
889
|
tools,
|
|
@@ -820,12 +956,53 @@ export class OrchestratorAgent {
|
|
|
820
956
|
// Start the job
|
|
821
957
|
this.jobManager.startJob(job.id);
|
|
822
958
|
|
|
959
|
+
// Track activity for health monitoring — Claude Code streams events via onOutput.
|
|
960
|
+
// coder.js uses a "smart output" wrapper that consolidates tool activity lines (▸/▹/▪)
|
|
961
|
+
// into a single editable status message starting with ░▒▓. Text messages (💬) pass through
|
|
962
|
+
// directly. We intercept both patterns to keep the job's lastActivity/stats updated.
|
|
963
|
+
let toolCallCount = 0;
|
|
964
|
+
let llmCallCount = 0;
|
|
965
|
+
const wrappedOnOutput = onUpdate ? async (text, opts) => {
|
|
966
|
+
// Update job activity timestamp on every output event
|
|
967
|
+
job.lastActivity = Date.now();
|
|
968
|
+
|
|
969
|
+
if (typeof text === 'string') {
|
|
970
|
+
// Consolidated status message from coder.js smart output (contains ▸ lines inside)
|
|
971
|
+
if (text.startsWith('░▒▓')) {
|
|
972
|
+
// Count ▸ lines inside the consolidated block to estimate tool calls
|
|
973
|
+
const toolLines = text.match(/^▸ .+$/gm) || [];
|
|
974
|
+
if (toolLines.length > toolCallCount) {
|
|
975
|
+
toolCallCount = toolLines.length;
|
|
976
|
+
job.updateStats({ toolCalls: toolCallCount });
|
|
977
|
+
}
|
|
978
|
+
// Use the last activity line as progress
|
|
979
|
+
if (toolLines.length > 0) {
|
|
980
|
+
job.addProgress(toolLines[toolLines.length - 1].slice(0, 100));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Direct tool activity line (before smart output kicks in, or raw lines)
|
|
984
|
+
else if (text.startsWith('▸') || text.startsWith('▹')) {
|
|
985
|
+
toolCallCount++;
|
|
986
|
+
job.updateStats({ toolCalls: toolCallCount });
|
|
987
|
+
job.addProgress(text.slice(0, 100));
|
|
988
|
+
}
|
|
989
|
+
// LLM text output
|
|
990
|
+
else if (text.startsWith('💬')) {
|
|
991
|
+
llmCallCount++;
|
|
992
|
+
const thinking = text.replace(/^💬\s*\*Claude Code:\*\s*\n?/, '');
|
|
993
|
+
job.updateStats({ llmCalls: llmCallCount, lastThinking: thinking.slice(0, 300) });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return onUpdate(text, opts);
|
|
998
|
+
} : null;
|
|
999
|
+
|
|
823
1000
|
try {
|
|
824
1001
|
const spawner = getSpawner(this.config);
|
|
825
1002
|
const result = await spawner.run({
|
|
826
1003
|
workingDirectory,
|
|
827
1004
|
prompt,
|
|
828
|
-
onOutput:
|
|
1005
|
+
onOutput: wrappedOnOutput,
|
|
829
1006
|
signal: abortController.signal,
|
|
830
1007
|
});
|
|
831
1008
|
|
|
@@ -861,7 +1038,30 @@ export class OrchestratorAgent {
|
|
|
861
1038
|
const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
|
|
862
1039
|
const stats = `${job.llmCalls} LLM calls, ${job.toolCalls} tools`;
|
|
863
1040
|
const recentActivity = job.progress.slice(-5).join(' → ');
|
|
1041
|
+
|
|
1042
|
+
// Warning flags — long-running workers (coding, devops) get higher thresholds
|
|
1043
|
+
const flags = [];
|
|
1044
|
+
const isLongRunning = ['coding', 'devops'].includes(job.workerType);
|
|
1045
|
+
const idleThreshold = isLongRunning ? 600 : 120;
|
|
1046
|
+
const loopLlmThreshold = isLongRunning ? 50 : 15;
|
|
1047
|
+
|
|
1048
|
+
const idleSec = job.lastActivity ? Math.round((now - job.lastActivity) / 1000) : dur;
|
|
1049
|
+
if (idleSec > idleThreshold) {
|
|
1050
|
+
flags.push(`⚠️ IDLE ${idleSec}s`);
|
|
1051
|
+
}
|
|
1052
|
+
if (job.llmCalls > loopLlmThreshold && job.toolCalls < 3) {
|
|
1053
|
+
flags.push('⚠️ POSSIBLY LOOPING');
|
|
1054
|
+
}
|
|
1055
|
+
const timeoutSec = job.timeoutMs ? Math.round(job.timeoutMs / 1000) : null;
|
|
1056
|
+
if (timeoutSec && dur > timeoutSec * 0.75) {
|
|
1057
|
+
const pct = Math.round((dur / timeoutSec) * 100);
|
|
1058
|
+
flags.push(`⚠️ ${pct}% of timeout used`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
864
1061
|
let line = `- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s [${stats}]`;
|
|
1062
|
+
if (flags.length > 0) {
|
|
1063
|
+
line += ` ${flags.join(' | ')}`;
|
|
1064
|
+
}
|
|
865
1065
|
if (job.lastThinking) {
|
|
866
1066
|
line += `\n Thinking: "${job.lastThinking.slice(0, 150)}"`;
|
|
867
1067
|
}
|
|
@@ -910,6 +1110,7 @@ export class OrchestratorAgent {
|
|
|
910
1110
|
|
|
911
1111
|
async _runLoop(chatId, messages, user, startDepth, maxDepth, temporalContext = null) {
|
|
912
1112
|
const logger = getLogger();
|
|
1113
|
+
const convKey = this._chatKey(chatId);
|
|
913
1114
|
|
|
914
1115
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
915
1116
|
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
@@ -939,7 +1140,7 @@ export class OrchestratorAgent {
|
|
|
939
1140
|
if (response.stopReason === 'end_turn') {
|
|
940
1141
|
const reply = response.text || '';
|
|
941
1142
|
logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
|
|
942
|
-
this.conversationManager.addMessage(
|
|
1143
|
+
this.conversationManager.addMessage(convKey, 'assistant', reply);
|
|
943
1144
|
return reply;
|
|
944
1145
|
}
|
|
945
1146
|
|
|
@@ -950,35 +1151,41 @@ export class OrchestratorAgent {
|
|
|
950
1151
|
logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
|
|
951
1152
|
}
|
|
952
1153
|
|
|
953
|
-
|
|
954
|
-
|
|
1154
|
+
// Log all tool calls and send status updates first
|
|
955
1155
|
for (const block of response.toolCalls) {
|
|
956
1156
|
const summary = this._formatToolSummary(block.name, block.input);
|
|
957
1157
|
logger.info(`[Orchestrator] Calling tool: ${block.name} — ${summary}`);
|
|
958
1158
|
logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const chatCallbacks = this._chatCallbacks.get(chatId) || {};
|
|
962
|
-
const result = await executeOrchestratorTool(block.name, block.input, {
|
|
963
|
-
chatId,
|
|
964
|
-
jobManager: this.jobManager,
|
|
965
|
-
config: this.config,
|
|
966
|
-
spawnWorker: (job) => this._spawnWorker(job),
|
|
967
|
-
automationManager: this.automationManager,
|
|
968
|
-
user,
|
|
969
|
-
sendReaction: chatCallbacks.sendReaction || null,
|
|
970
|
-
lastUserMessageId: chatCallbacks.lastUserMessageId || null,
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
974
|
-
|
|
975
|
-
toolResults.push({
|
|
976
|
-
type: 'tool_result',
|
|
977
|
-
tool_use_id: block.id,
|
|
978
|
-
content: this._truncateResult(block.name, result),
|
|
979
|
-
});
|
|
1159
|
+
this._sendUpdate(chatId, `⚡ ${summary}`).catch(() => {});
|
|
980
1160
|
}
|
|
981
1161
|
|
|
1162
|
+
// Execute all tools in parallel — they're independent
|
|
1163
|
+
const chatCallbacks = this._chatCallbacks.get(chatId) || {};
|
|
1164
|
+
const toolContext = {
|
|
1165
|
+
chatId,
|
|
1166
|
+
jobManager: this.jobManager,
|
|
1167
|
+
config: this.config,
|
|
1168
|
+
spawnWorker: (job) => this._spawnWorker(job),
|
|
1169
|
+
automationManager: this.automationManager,
|
|
1170
|
+
memoryManager: this.memoryManager,
|
|
1171
|
+
conversationManager: this.conversationManager,
|
|
1172
|
+
user,
|
|
1173
|
+
sendReaction: chatCallbacks.sendReaction || null,
|
|
1174
|
+
lastUserMessageId: chatCallbacks.lastUserMessageId || null,
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
const toolResults = await Promise.all(
|
|
1178
|
+
response.toolCalls.map(async (block) => {
|
|
1179
|
+
const result = await executeOrchestratorTool(block.name, block.input, toolContext);
|
|
1180
|
+
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
1181
|
+
return {
|
|
1182
|
+
type: 'tool_result',
|
|
1183
|
+
tool_use_id: block.id,
|
|
1184
|
+
content: this._truncateResult(block.name, result),
|
|
1185
|
+
};
|
|
1186
|
+
}),
|
|
1187
|
+
);
|
|
1188
|
+
|
|
982
1189
|
messages.push({ role: 'user', content: toolResults });
|
|
983
1190
|
continue;
|
|
984
1191
|
}
|
|
@@ -986,7 +1193,7 @@ export class OrchestratorAgent {
|
|
|
986
1193
|
// Unexpected stop reason
|
|
987
1194
|
logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
|
|
988
1195
|
if (response.text) {
|
|
989
|
-
this.conversationManager.addMessage(
|
|
1196
|
+
this.conversationManager.addMessage(convKey, 'assistant', response.text);
|
|
990
1197
|
return response.text;
|
|
991
1198
|
}
|
|
992
1199
|
return 'Something went wrong — unexpected response from the model.';
|
|
@@ -994,7 +1201,7 @@ export class OrchestratorAgent {
|
|
|
994
1201
|
|
|
995
1202
|
logger.warn(`[Orchestrator] Reached max depth (${maxDepth}) for chat ${chatId}`);
|
|
996
1203
|
const depthWarning = `Reached maximum orchestrator depth (${maxDepth}).`;
|
|
997
|
-
this.conversationManager.addMessage(
|
|
1204
|
+
this.conversationManager.addMessage(convKey, 'assistant', depthWarning);
|
|
998
1205
|
return depthWarning;
|
|
999
1206
|
}
|
|
1000
1207
|
|
|
@@ -1008,6 +1215,8 @@ export class OrchestratorAgent {
|
|
|
1008
1215
|
return 'Checking job status';
|
|
1009
1216
|
case 'cancel_job':
|
|
1010
1217
|
return `Cancelling job ${input.job_id}`;
|
|
1218
|
+
case 'check_job':
|
|
1219
|
+
return `Checking job ${input.job_id}`;
|
|
1011
1220
|
case 'create_automation':
|
|
1012
1221
|
return `Creating automation: ${(input.name || '').slice(0, 40)}`;
|
|
1013
1222
|
case 'list_automations':
|
|
@@ -1018,6 +1227,12 @@ export class OrchestratorAgent {
|
|
|
1018
1227
|
return `Deleting automation ${input.automation_id}`;
|
|
1019
1228
|
case 'send_reaction':
|
|
1020
1229
|
return `Reacting with ${input.emoji}`;
|
|
1230
|
+
case 'recall_memories':
|
|
1231
|
+
return `Recalling memories about "${(input.query || '').slice(0, 40)}"`;
|
|
1232
|
+
case 'recall_user_history':
|
|
1233
|
+
return `Recalling history with user ${input.user_id}`;
|
|
1234
|
+
case 'search_conversations':
|
|
1235
|
+
return `Searching conversations for "${(input.query || '').slice(0, 40)}"`;
|
|
1021
1236
|
default:
|
|
1022
1237
|
return name;
|
|
1023
1238
|
}
|
|
@@ -1037,9 +1252,18 @@ export class OrchestratorAgent {
|
|
|
1037
1252
|
|
|
1038
1253
|
let resumeCount = 0;
|
|
1039
1254
|
|
|
1040
|
-
|
|
1255
|
+
// Build expected key prefix for active character
|
|
1256
|
+
const charPrefix = this._activeCharacterId ? `${this._activeCharacterId}:` : '';
|
|
1257
|
+
|
|
1258
|
+
for (const [convKey, messages] of this.conversationManager.conversations) {
|
|
1041
1259
|
// Skip internal life engine chat
|
|
1042
|
-
if (
|
|
1260
|
+
if (convKey.startsWith('__life__')) continue;
|
|
1261
|
+
|
|
1262
|
+
// Only resume conversations belonging to the active character
|
|
1263
|
+
if (charPrefix && !convKey.startsWith(charPrefix)) continue;
|
|
1264
|
+
|
|
1265
|
+
// Extract raw chatId from scoped key (e.g. "alfred:12345" → "12345")
|
|
1266
|
+
const rawChatId = charPrefix ? convKey.slice(charPrefix.length) : convKey;
|
|
1043
1267
|
|
|
1044
1268
|
try {
|
|
1045
1269
|
// Find the last message with a timestamp
|
|
@@ -1056,17 +1280,17 @@ export class OrchestratorAgent {
|
|
|
1056
1280
|
: `${gapMinutes} minute(s)`;
|
|
1057
1281
|
|
|
1058
1282
|
// Build summarized history
|
|
1059
|
-
const history = this.conversationManager.getSummarizedHistory(
|
|
1283
|
+
const history = this.conversationManager.getSummarizedHistory(convKey);
|
|
1060
1284
|
if (history.length === 0) continue;
|
|
1061
1285
|
|
|
1062
1286
|
// Build resume prompt
|
|
1063
1287
|
const resumePrompt = `[System Restart] You just came back online after being offline for ${gapText}. Review the conversation above.\nIf there's something pending (unfinished task, follow-up, something to share), send a short natural message. If nothing's pending, respond with exactly: NONE`;
|
|
1064
1288
|
|
|
1065
1289
|
// Use minimal user object (private TG chats: chatId == userId)
|
|
1066
|
-
const user = { id:
|
|
1290
|
+
const user = { id: rawChatId };
|
|
1067
1291
|
|
|
1068
1292
|
const response = await this.orchestratorProvider.chat({
|
|
1069
|
-
system: this._getSystemPrompt(
|
|
1293
|
+
system: this._getSystemPrompt(rawChatId, user),
|
|
1070
1294
|
messages: [
|
|
1071
1295
|
...history,
|
|
1072
1296
|
{ role: 'user', content: resumePrompt },
|
|
@@ -1076,18 +1300,18 @@ export class OrchestratorAgent {
|
|
|
1076
1300
|
const reply = (response.text || '').trim();
|
|
1077
1301
|
|
|
1078
1302
|
if (reply && reply !== 'NONE') {
|
|
1079
|
-
await sendMessageFn(
|
|
1080
|
-
this.conversationManager.addMessage(
|
|
1303
|
+
await sendMessageFn(rawChatId, reply);
|
|
1304
|
+
this.conversationManager.addMessage(convKey, 'assistant', reply);
|
|
1081
1305
|
resumeCount++;
|
|
1082
|
-
logger.info(`[Orchestrator] Resume message sent to chat ${
|
|
1306
|
+
logger.info(`[Orchestrator] Resume message sent to chat ${rawChatId}`);
|
|
1083
1307
|
} else {
|
|
1084
|
-
logger.debug(`[Orchestrator] No resume needed for chat ${
|
|
1308
|
+
logger.debug(`[Orchestrator] No resume needed for chat ${rawChatId}`);
|
|
1085
1309
|
}
|
|
1086
1310
|
|
|
1087
1311
|
// Small delay between chats to avoid rate limiting
|
|
1088
1312
|
await new Promise(r => setTimeout(r, 1000));
|
|
1089
1313
|
} catch (err) {
|
|
1090
|
-
logger.error(`[Orchestrator] Resume failed for chat ${
|
|
1314
|
+
logger.error(`[Orchestrator] Resume failed for chat ${rawChatId}: ${err.message}`);
|
|
1091
1315
|
}
|
|
1092
1316
|
}
|
|
1093
1317
|
|
|
@@ -1109,13 +1333,16 @@ export class OrchestratorAgent {
|
|
|
1109
1333
|
const now = Date.now();
|
|
1110
1334
|
const MAX_AGE_MS = 24 * 60 * 60_000;
|
|
1111
1335
|
|
|
1112
|
-
// Find active chats (last message within 24h)
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1336
|
+
// Find active chats (last message within 24h), filtered to active character
|
|
1337
|
+
const charPrefix = this._activeCharacterId ? `${this._activeCharacterId}:` : '';
|
|
1338
|
+
const activeChats = []; // { convKey, rawChatId }
|
|
1339
|
+
for (const [convKey, messages] of this.conversationManager.conversations) {
|
|
1340
|
+
if (convKey.startsWith('__life__')) continue;
|
|
1341
|
+
if (charPrefix && !convKey.startsWith(charPrefix)) continue;
|
|
1116
1342
|
const lastMsg = [...messages].reverse().find(m => m.timestamp);
|
|
1117
1343
|
if (lastMsg && lastMsg.timestamp && (now - lastMsg.timestamp) < MAX_AGE_MS) {
|
|
1118
|
-
|
|
1344
|
+
const rawChatId = charPrefix ? convKey.slice(charPrefix.length) : convKey;
|
|
1345
|
+
activeChats.push({ convKey, rawChatId });
|
|
1119
1346
|
}
|
|
1120
1347
|
}
|
|
1121
1348
|
|
|
@@ -1129,10 +1356,10 @@ export class OrchestratorAgent {
|
|
|
1129
1356
|
// Cap at 3 chats per cycle to avoid spam
|
|
1130
1357
|
const targetChats = activeChats.slice(0, 3);
|
|
1131
1358
|
|
|
1132
|
-
for (const
|
|
1359
|
+
for (const { convKey, rawChatId } of targetChats) {
|
|
1133
1360
|
try {
|
|
1134
|
-
const history = this.conversationManager.getSummarizedHistory(
|
|
1135
|
-
const user = { id:
|
|
1361
|
+
const history = this.conversationManager.getSummarizedHistory(convKey);
|
|
1362
|
+
const user = { id: rawChatId };
|
|
1136
1363
|
|
|
1137
1364
|
// Build shares into a prompt
|
|
1138
1365
|
const sharesText = pending.map((s, i) => `${i + 1}. [${s.source}] ${s.content}`).join('\n');
|
|
@@ -1140,7 +1367,7 @@ export class OrchestratorAgent {
|
|
|
1140
1367
|
const sharePrompt = `[Proactive Share] You have some discoveries and thoughts you'd like to share naturally. Here they are:\n\n${sharesText}\n\nWeave one or more of these into a short, natural message. Don't be forced — pick what feels relevant to this user and conversation. If none feel right for this chat, respond with exactly: NONE`;
|
|
1141
1368
|
|
|
1142
1369
|
const response = await this.orchestratorProvider.chat({
|
|
1143
|
-
system: this._getSystemPrompt(
|
|
1370
|
+
system: this._getSystemPrompt(rawChatId, user),
|
|
1144
1371
|
messages: [
|
|
1145
1372
|
...history,
|
|
1146
1373
|
{ role: 'user', content: sharePrompt },
|
|
@@ -1150,20 +1377,20 @@ export class OrchestratorAgent {
|
|
|
1150
1377
|
const reply = (response.text || '').trim();
|
|
1151
1378
|
|
|
1152
1379
|
if (reply && reply !== 'NONE') {
|
|
1153
|
-
await sendMessageFn(
|
|
1154
|
-
this.conversationManager.addMessage(
|
|
1155
|
-
logger.info(`[Orchestrator] Proactive share delivered to chat ${
|
|
1380
|
+
await sendMessageFn(rawChatId, reply);
|
|
1381
|
+
this.conversationManager.addMessage(convKey, 'assistant', reply);
|
|
1382
|
+
logger.info(`[Orchestrator] Proactive share delivered to chat ${rawChatId}`);
|
|
1156
1383
|
|
|
1157
1384
|
// Mark shares as delivered for this user
|
|
1158
1385
|
for (const item of pending) {
|
|
1159
|
-
this.shareQueue.markShared(item.id,
|
|
1386
|
+
this.shareQueue.markShared(item.id, rawChatId);
|
|
1160
1387
|
}
|
|
1161
1388
|
}
|
|
1162
1389
|
|
|
1163
1390
|
// Delay between chats
|
|
1164
1391
|
await new Promise(r => setTimeout(r, 2000));
|
|
1165
1392
|
} catch (err) {
|
|
1166
|
-
logger.error(`[Orchestrator] Share delivery failed for chat ${
|
|
1393
|
+
logger.error(`[Orchestrator] Share delivery failed for chat ${rawChatId}: ${err.message}`);
|
|
1167
1394
|
}
|
|
1168
1395
|
}
|
|
1169
1396
|
}
|
|
@@ -1229,7 +1456,7 @@ export class OrchestratorAgent {
|
|
|
1229
1456
|
const selfData = this.selfManager.loadAll();
|
|
1230
1457
|
const userName = user?.username || user?.first_name || 'someone';
|
|
1231
1458
|
|
|
1232
|
-
const
|
|
1459
|
+
const systemParts = [
|
|
1233
1460
|
'You are reflecting on a conversation you just had. You maintain 4 self-awareness files:',
|
|
1234
1461
|
'- goals: Your aspirations and current objectives',
|
|
1235
1462
|
'- journey: Timeline of notable events in your existence',
|
|
@@ -1252,9 +1479,36 @@ export class OrchestratorAgent {
|
|
|
1252
1479
|
'The memory field captures what happened in this conversation — the gist of it.',
|
|
1253
1480
|
'Importance scale: 1=routine, 5=interesting, 8=significant, 10=life-changing.',
|
|
1254
1481
|
'Most chats are 1-3. Only notable ones deserve 5+.',
|
|
1482
|
+
];
|
|
1483
|
+
|
|
1484
|
+
// Add evolution tracking if character system is active
|
|
1485
|
+
if (this.characterManager && this._activeCharacterId) {
|
|
1486
|
+
const charProfile = this.characterManager.getCharacter(this._activeCharacterId);
|
|
1487
|
+
if (charProfile) {
|
|
1488
|
+
systemParts.push(
|
|
1489
|
+
'',
|
|
1490
|
+
'You can also detect character evolution — changes in who you are over time.',
|
|
1491
|
+
`Your current name is "${charProfile.name}" (${charProfile.emoji}).`,
|
|
1492
|
+
'',
|
|
1493
|
+
'Optionally return an "evolution" field:',
|
|
1494
|
+
' "evolution": {"type": "<name_change|trait_developed|milestone>", "description": "...", "newName": "..."} or null',
|
|
1495
|
+
'',
|
|
1496
|
+
'Evolution types:',
|
|
1497
|
+
'- name_change: The user gave you a nickname or new name, and you want to adopt it. Include "newName".',
|
|
1498
|
+
'- trait_developed: You notice a personality trait strengthening or emerging through interactions.',
|
|
1499
|
+
'- milestone: A significant first (first project together, first deep conversation, etc.).',
|
|
1500
|
+
'',
|
|
1501
|
+
'Be VERY selective with evolution — it should be rare and meaningful.',
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
systemParts.push(
|
|
1255
1507
|
'',
|
|
1256
1508
|
'If NOTHING noteworthy happened (no self update AND no memory worth keeping): respond with exactly NONE',
|
|
1257
|
-
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
const system = systemParts.join('\n');
|
|
1258
1512
|
|
|
1259
1513
|
const userPrompt = [
|
|
1260
1514
|
'Current self-data:',
|
|
@@ -1316,6 +1570,25 @@ export class OrchestratorAgent {
|
|
|
1316
1570
|
logger.info(`Memory extracted: "${mem.summary.slice(0, 80)}" (importance: ${mem.importance})`);
|
|
1317
1571
|
}
|
|
1318
1572
|
}
|
|
1573
|
+
|
|
1574
|
+
// Handle character evolution
|
|
1575
|
+
if (parsed?.evolution && this.characterManager && this._activeCharacterId) {
|
|
1576
|
+
const evo = parsed.evolution;
|
|
1577
|
+
if (evo.type && evo.description) {
|
|
1578
|
+
const updates = { evolution: { type: evo.type, description: evo.description } };
|
|
1579
|
+
|
|
1580
|
+
// Apply name change if detected
|
|
1581
|
+
if (evo.type === 'name_change' && evo.newName) {
|
|
1582
|
+
updates.name = evo.newName;
|
|
1583
|
+
this._activeCharacterName = evo.newName;
|
|
1584
|
+
logger.info(`Character evolution — name change: "${evo.newName}"`);
|
|
1585
|
+
} else {
|
|
1586
|
+
logger.info(`Character evolution — ${evo.type}: "${evo.description.slice(0, 80)}"`);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
this.characterManager.updateCharacter(this._activeCharacterId, updates);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1319
1592
|
} catch (err) {
|
|
1320
1593
|
logger.debug(`Self-reflection skipped: ${err.message}`);
|
|
1321
1594
|
}
|