kernelbot 1.0.30 → 1.0.33

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.
Files changed (63) hide show
  1. package/.env.example +0 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +56 -2
  4. package/config.example.yaml +31 -0
  5. package/package.json +1 -1
  6. package/src/agent.js +200 -32
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +402 -6
  12. package/src/claude-auth.js +0 -0
  13. package/src/coder.js +0 -0
  14. package/src/conversation.js +51 -5
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/life/codebase.js +388 -0
  19. package/src/life/engine.js +1317 -0
  20. package/src/life/evolution.js +244 -0
  21. package/src/life/improvements.js +81 -0
  22. package/src/life/journal.js +109 -0
  23. package/src/life/memory.js +283 -0
  24. package/src/life/share-queue.js +136 -0
  25. package/src/persona.js +0 -0
  26. package/src/prompts/orchestrator.js +62 -2
  27. package/src/prompts/persona.md +7 -0
  28. package/src/prompts/system.js +0 -0
  29. package/src/prompts/workers.js +10 -9
  30. package/src/providers/anthropic.js +0 -0
  31. package/src/providers/base.js +0 -0
  32. package/src/providers/index.js +0 -0
  33. package/src/providers/models.js +8 -1
  34. package/src/providers/openai-compat.js +0 -0
  35. package/src/security/audit.js +0 -0
  36. package/src/security/auth.js +0 -0
  37. package/src/security/confirm.js +0 -0
  38. package/src/self.js +0 -0
  39. package/src/services/stt.js +0 -0
  40. package/src/services/tts.js +0 -0
  41. package/src/skills/catalog.js +0 -0
  42. package/src/skills/custom.js +0 -0
  43. package/src/swarm/job-manager.js +0 -0
  44. package/src/swarm/job.js +11 -0
  45. package/src/swarm/worker-registry.js +0 -0
  46. package/src/tools/browser.js +0 -0
  47. package/src/tools/categories.js +0 -0
  48. package/src/tools/coding.js +1 -1
  49. package/src/tools/docker.js +0 -0
  50. package/src/tools/git.js +0 -0
  51. package/src/tools/github.js +0 -0
  52. package/src/tools/index.js +0 -0
  53. package/src/tools/jira.js +0 -0
  54. package/src/tools/monitor.js +0 -0
  55. package/src/tools/network.js +0 -0
  56. package/src/tools/orchestrator-tools.js +60 -3
  57. package/src/tools/os.js +0 -0
  58. package/src/tools/persona.js +0 -0
  59. package/src/tools/process.js +0 -0
  60. package/src/utils/config.js +0 -0
  61. package/src/utils/display.js +0 -0
  62. package/src/utils/logger.js +0 -0
  63. package/src/worker.js +27 -8
package/.env.example CHANGED
File without changes
package/README.md CHANGED
File without changes
package/bin/kernel.js CHANGED
@@ -26,6 +26,12 @@ import { JobManager } from '../src/swarm/job-manager.js';
26
26
  import { startBot } from '../src/bot.js';
27
27
  import { AutomationManager } from '../src/automation/index.js';
28
28
  import { createProvider, PROVIDERS } from '../src/providers/index.js';
29
+ import { MemoryManager } from '../src/life/memory.js';
30
+ import { JournalManager } from '../src/life/journal.js';
31
+ import { ShareQueue } from '../src/life/share-queue.js';
32
+ import { EvolutionTracker } from '../src/life/evolution.js';
33
+ import { CodebaseKnowledge } from '../src/life/codebase.js';
34
+ import { LifeEngine } from '../src/life/engine.js';
29
35
  import {
30
36
  loadCustomSkills,
31
37
  getCustomSkills,
@@ -182,9 +188,25 @@ async function startBotFlow(config) {
182
188
 
183
189
  const automationManager = new AutomationManager();
184
190
 
185
- const agent = new Agent({ config, conversationManager, personaManager, selfManager, jobManager, automationManager });
191
+ // Life system managers
192
+ const memoryManager = new MemoryManager();
193
+ const journalManager = new JournalManager();
194
+ const shareQueue = new ShareQueue();
195
+ const evolutionTracker = new EvolutionTracker();
196
+ const codebaseKnowledge = new CodebaseKnowledge({ config });
186
197
 
187
- startBot(config, agent, conversationManager, jobManager, automationManager);
198
+ const agent = new Agent({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue });
199
+
200
+ // Wire codebase knowledge to agent for LLM-powered scanning
201
+ codebaseKnowledge.setAgent(agent);
202
+
203
+ // Life Engine — autonomous inner life
204
+ const lifeEngine = new LifeEngine({
205
+ config, agent, memoryManager, journalManager, shareQueue,
206
+ evolutionTracker, codebaseKnowledge, selfManager,
207
+ });
208
+
209
+ startBot(config, agent, conversationManager, jobManager, automationManager, { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge });
188
210
 
189
211
  // Periodic job cleanup and timeout enforcement
190
212
  const cleanupMs = (config.swarm.cleanup_interval_minutes || 30) * 60 * 1000;
@@ -193,7 +215,39 @@ async function startBotFlow(config) {
193
215
  jobManager.enforceTimeouts();
194
216
  }, Math.min(cleanupMs, 60_000)); // enforce timeouts every minute at most
195
217
 
218
+ // Periodic memory pruning (daily)
219
+ const retentionDays = config.life?.memory_retention_days || 90;
220
+ setInterval(() => {
221
+ memoryManager.pruneOld(retentionDays);
222
+ shareQueue.prune(7);
223
+ }, 24 * 3600_000);
224
+
196
225
  showStartupComplete();
226
+
227
+ // Start life engine if enabled
228
+ const lifeEnabled = config.life?.enabled !== false;
229
+ if (lifeEnabled) {
230
+ logger.info('[Startup] Life engine enabled — waking up...');
231
+ lifeEngine.wakeUp().then(() => {
232
+ lifeEngine.start();
233
+ logger.info('[Startup] Life engine running');
234
+ }).catch(err => {
235
+ logger.error(`[Startup] Life engine wake-up failed: ${err.message}`);
236
+ lifeEngine.start(); // still start heartbeat even if wake-up fails
237
+ });
238
+
239
+ // Initial codebase scan (background, non-blocking)
240
+ if (config.life?.self_coding?.enabled) {
241
+ codebaseKnowledge.scanChanged().then(count => {
242
+ if (count > 0) logger.info(`[Startup] Codebase scan: ${count} files indexed`);
243
+ }).catch(err => {
244
+ logger.warn(`[Startup] Codebase scan failed: ${err.message}`);
245
+ });
246
+ }
247
+ } else {
248
+ logger.info('[Startup] Life engine disabled');
249
+ }
250
+
197
251
  return true;
198
252
  }
199
253
 
@@ -56,3 +56,34 @@ voice:
56
56
  tts_enabled: true # Send voice replies alongside text (default: true if API key present)
57
57
  stt_enabled: true # Transcribe user voice messages (default: true if API key present)
58
58
  # voice_id: JBFqnCBsd6RMkjVDRZzb # ElevenLabs voice ID (override ELEVENLABS_VOICE_ID env var)
59
+
60
+ # Inner Life — autonomous thinking, journaling, browsing, creating
61
+ # Data stored at ~/.kernelbot/life/
62
+ life:
63
+ enabled: true # Enable autonomous inner life
64
+ min_interval_minutes: 30 # Minimum time between activities
65
+ max_interval_minutes: 120 # Maximum time between activities
66
+ activity_weights: # Relative probability weights
67
+ think: 30 # Inner monologue, self-questioning
68
+ browse: 25 # Explore interests via web search
69
+ journal: 20 # Write daily journal entries
70
+ create: 15 # Creative expression (poems, stories, thoughts)
71
+ self_code: 10 # Self-evolution proposals (requires self_coding.enabled)
72
+ code_review: 5 # Codebase scanning + PR status checks
73
+ reflect: 8 # Read logs, analyze interactions, find improvement patterns
74
+ proactive_sharing: true # Share discoveries with users proactively
75
+ proactive_max_per_day: 3 # Max proactive messages per day
76
+ memory_retention_days: 90 # Days to keep episodic memories
77
+ self_coding:
78
+ enabled: false # Allow self-evolution (safety-gated, opt-in)
79
+ branch_prefix: evolution # Git branch prefix for evolution PRs
80
+ # repo_remote: Owner/Repo # GitHub owner/repo for PRs (auto-detected if omitted)
81
+ cooldown_hours: 2 # Min hours between evolution attempts
82
+ max_active_prs: 3 # Max concurrent open evolution PRs
83
+ max_proposals_per_day: 3 # Max new proposals per day
84
+ allowed_scopes: all # all | safe | prompts_only
85
+ code_review_cooldown_hours: 4 # Hours between code review activities
86
+ codebase_scan_interval_hours: 24 # Hours between full codebase scans
87
+ quiet_hours:
88
+ start: 2 # Hour to start quiet period (no activities)
89
+ end: 6 # Hour to end quiet period
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.30",
3
+ "version": "1.0.33",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
package/src/agent.js CHANGED
@@ -8,19 +8,21 @@ import { getUnifiedSkillById } from './skills/custom.js';
8
8
  import { WorkerAgent } from './worker.js';
9
9
  import { getLogger } from './utils/logger.js';
10
10
  import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
11
- import { resetClaudeCodeSpawner } from './tools/coding.js';
11
+ import { resetClaudeCodeSpawner, getSpawner } from './tools/coding.js';
12
12
 
13
13
  const MAX_RESULT_LENGTH = 3000;
14
14
  const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
15
15
 
16
16
  export class OrchestratorAgent {
17
- constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager }) {
17
+ constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue }) {
18
18
  this.config = config;
19
19
  this.conversationManager = conversationManager;
20
20
  this.personaManager = personaManager;
21
21
  this.selfManager = selfManager || null;
22
22
  this.jobManager = jobManager;
23
23
  this.automationManager = automationManager || null;
24
+ this.memoryManager = memoryManager || null;
25
+ this.shareQueue = shareQueue || null;
24
26
  this._pending = new Map(); // chatId -> pending state
25
27
  this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
26
28
 
@@ -47,7 +49,7 @@ export class OrchestratorAgent {
47
49
  }
48
50
 
49
51
  /** Build the orchestrator system prompt. */
50
- _getSystemPrompt(chatId, user) {
52
+ _getSystemPrompt(chatId, user, temporalContext = null) {
51
53
  const logger = getLogger();
52
54
  const skillId = this.conversationManager.getSkill(chatId);
53
55
  const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
@@ -62,8 +64,20 @@ export class OrchestratorAgent {
62
64
  selfData = this.selfManager.loadAll();
63
65
  }
64
66
 
65
- logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'}`);
66
- return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData);
67
+ // Build memory context block
68
+ let memoriesBlock = null;
69
+ if (this.memoryManager) {
70
+ memoriesBlock = this.memoryManager.buildContextBlock(user?.id || null);
71
+ }
72
+
73
+ // Build share queue block
74
+ let sharesBlock = null;
75
+ if (this.shareQueue) {
76
+ sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
77
+ }
78
+
79
+ 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'}`);
80
+ return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext);
67
81
  }
68
82
 
69
83
  setSkill(chatId, skillId) {
@@ -287,13 +301,13 @@ export class OrchestratorAgent {
287
301
  return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
288
302
  }
289
303
 
290
- async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
304
+ async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
291
305
  const logger = getLogger();
292
306
 
293
307
  logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
294
308
 
295
309
  // Store callbacks so workers can use them later
296
- this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
310
+ this._chatCallbacks.set(chatId, { onUpdate, sendPhoto, sendReaction: opts.sendReaction, lastUserMessageId: opts.messageId });
297
311
 
298
312
  // Handle pending responses (confirmation or credential)
299
313
  const pending = this._pending.get(chatId);
@@ -308,6 +322,22 @@ export class OrchestratorAgent {
308
322
 
309
323
  const { max_tool_depth } = this.config.orchestrator;
310
324
 
325
+ // Detect time gap before adding the new message
326
+ let temporalContext = null;
327
+ const lastTs = this.conversationManager.getLastMessageTimestamp(chatId);
328
+ if (lastTs) {
329
+ const gapMs = Date.now() - lastTs;
330
+ const gapMinutes = Math.floor(gapMs / 60_000);
331
+ if (gapMinutes >= 30) {
332
+ const gapHours = Math.floor(gapMinutes / 60);
333
+ const gapText = gapHours >= 1
334
+ ? `${gapHours} hour(s)`
335
+ : `${gapMinutes} minute(s)`;
336
+ temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic.]`;
337
+ logger.info(`Time gap detected for chat ${chatId}: ${gapText}`);
338
+ }
339
+ }
340
+
311
341
  // Add user message to persistent history
312
342
  this.conversationManager.addMessage(chatId, 'user', userMessage);
313
343
 
@@ -315,7 +345,7 @@ export class OrchestratorAgent {
315
345
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
316
346
  logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
317
347
 
318
- const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
348
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
319
349
 
320
350
  logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
321
351
 
@@ -323,13 +353,29 @@ export class OrchestratorAgent {
323
353
  this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
324
354
  this._reflectOnSelfBackground(userMessage, reply, user).catch(() => {});
325
355
 
356
+ // Mark pending shares as shared (they were in the prompt, bot wove them in)
357
+ if (this.shareQueue && user?.id) {
358
+ const pending = this.shareQueue.getPending(user.id, 3);
359
+ for (const item of pending) {
360
+ this.shareQueue.markShared(item.id, user.id);
361
+ }
362
+ }
363
+
326
364
  return reply;
327
365
  }
328
366
 
329
367
  async _sendUpdate(chatId, text, opts) {
330
368
  const callbacks = this._chatCallbacks.get(chatId);
331
369
  if (callbacks?.onUpdate) {
332
- try { return await callbacks.onUpdate(text, opts); } catch {}
370
+ try {
371
+ return await callbacks.onUpdate(text, opts);
372
+ } catch (err) {
373
+ const logger = getLogger();
374
+ logger.error(`[Orchestrator] _sendUpdate failed for chat ${chatId}: ${err.message}`);
375
+ }
376
+ } else {
377
+ const logger = getLogger();
378
+ logger.warn(`[Orchestrator] _sendUpdate: no callbacks for chat ${chatId}`);
333
379
  }
334
380
  return null;
335
381
  }
@@ -361,18 +407,21 @@ export class OrchestratorAgent {
361
407
 
362
408
  // 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
363
409
  const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
410
+ logger.debug(`[Orchestrator] Job ${job.id} notification sent — msgId=${notifyMsgId || 'none'}`);
364
411
 
365
412
  // 2. Try to summarize, then store ONE message in history (summary or fallback — not both)
366
413
  try {
367
414
  const summary = await this._summarizeJobResult(chatId, job);
368
415
  if (summary) {
416
+ logger.debug(`[Orchestrator] Job ${job.id} summary ready (${summary.length} chars) — delivering to user`);
369
417
  this.conversationManager.addMessage(chatId, 'assistant', summary);
370
418
  await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
371
419
  } else {
372
- // Summary was null (short result) — store the fallback
420
+ // Summary was null — store the fallback
373
421
  const fallback = this._buildSummaryFallback(job, label);
422
+ logger.debug(`[Orchestrator] Job ${job.id} using fallback (${fallback.length} chars) — delivering to user`);
374
423
  this.conversationManager.addMessage(chatId, 'assistant', fallback);
375
- await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
424
+ await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
376
425
  }
377
426
  } catch (err) {
378
427
  logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
@@ -453,7 +502,10 @@ export class OrchestratorAgent {
453
502
  }
454
503
  if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
455
504
  // Include details up to 8000 chars
456
- if (sr.details) parts.push(`Details:\n${sr.details.slice(0, 8000)}`);
505
+ if (sr.details) {
506
+ const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
507
+ parts.push(`Details:\n${d.slice(0, 8000)}`);
508
+ }
457
509
  resultContext = parts.join('\n');
458
510
  } else {
459
511
  resultContext = (job.result || 'Done.').slice(0, 8000);
@@ -498,9 +550,10 @@ export class OrchestratorAgent {
498
550
  }
499
551
  if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
500
552
  if (sr.details) {
501
- const details = sr.details.length > 6000
502
- ? sr.details.slice(0, 6000) + '\n... [details truncated]'
503
- : sr.details;
553
+ const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
554
+ const details = d.length > 6000
555
+ ? d.slice(0, 6000) + '\n... [details truncated]'
556
+ : d;
504
557
  parts.push(`Details:\n${details}`);
505
558
  }
506
559
  } else {
@@ -533,6 +586,11 @@ export class OrchestratorAgent {
533
586
  });
534
587
  parts.push(`\n${artifactLines.join('\n')}`);
535
588
  }
589
+ if (sr.details) {
590
+ const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
591
+ const details = d.length > 1500 ? d.slice(0, 1500) + '\n... [truncated]' : d;
592
+ parts.push(`\n${details}`);
593
+ }
536
594
  if (sr.followUp) parts.push(`\n💡 ${sr.followUp}`);
537
595
  return parts.join('');
538
596
  }
@@ -599,7 +657,8 @@ export class OrchestratorAgent {
599
657
  parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path || ''}`).join(', ')}`);
600
658
  }
601
659
  if (sr.details) {
602
- parts.push(sr.details.slice(0, 4000));
660
+ const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
661
+ parts.push(d.slice(0, 4000));
603
662
  }
604
663
  depResults.push(parts.join('\n'));
605
664
  } else if (depJob.result) {
@@ -621,6 +680,12 @@ export class OrchestratorAgent {
621
680
  */
622
681
  async _spawnWorker(job) {
623
682
  const logger = getLogger();
683
+
684
+ // Direct dispatch for coding tasks — bypass worker LLM, go straight to Claude Code CLI
685
+ if (job.workerType === 'coding') {
686
+ return this._spawnDirectCoding(job);
687
+ }
688
+
624
689
  const chatId = job.chatId;
625
690
  const callbacks = this._chatCallbacks.get(chatId) || {};
626
691
  const onUpdate = callbacks.onUpdate;
@@ -689,6 +754,7 @@ export class OrchestratorAgent {
689
754
  callbacks: {
690
755
  onProgress: (text) => addActivity(text),
691
756
  onHeartbeat: (text) => job.addProgress(text),
757
+ onStats: (stats) => job.updateStats(stats),
692
758
  onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
693
759
  onComplete: (result, parsedResult) => {
694
760
  logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
@@ -722,6 +788,59 @@ export class OrchestratorAgent {
722
788
  return worker.run(job.task);
723
789
  }
724
790
 
791
+ /**
792
+ * Direct coding dispatch — runs Claude Code CLI without a middleman worker LLM.
793
+ * The orchestrator's task description goes straight to Claude Code as the prompt.
794
+ */
795
+ async _spawnDirectCoding(job) {
796
+ const logger = getLogger();
797
+ const chatId = job.chatId;
798
+ const callbacks = this._chatCallbacks.get(chatId) || {};
799
+ const onUpdate = callbacks.onUpdate;
800
+ const workerDef = WORKER_TYPES[job.workerType] || {};
801
+ const label = workerDef.label || job.workerType;
802
+
803
+ logger.info(`[Orchestrator] Direct coding dispatch for job ${job.id} in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
804
+
805
+ // AbortController for cancellation — duck-typed so JobManager.cancelJob() works unchanged
806
+ const abortController = new AbortController();
807
+ job.worker = { cancel: () => abortController.abort() };
808
+
809
+ // Build context from conversation history, persona, dependency results
810
+ const workerContext = this._buildWorkerContext(job);
811
+ const prompt = workerContext
812
+ ? `${workerContext}\n\n---\n\n${job.task}`
813
+ : job.task;
814
+
815
+ // Working directory
816
+ const workingDirectory = this.config.claude_code?.workspace_dir || process.cwd();
817
+
818
+ // Start the job
819
+ this.jobManager.startJob(job.id);
820
+
821
+ try {
822
+ const spawner = getSpawner(this.config);
823
+ const result = await spawner.run({
824
+ workingDirectory,
825
+ prompt,
826
+ onOutput: onUpdate,
827
+ signal: abortController.signal,
828
+ });
829
+
830
+ const output = result.output || 'Done.';
831
+ logger.info(`[Orchestrator] Direct coding job ${job.id} completed — output: ${output.length} chars`);
832
+ this.jobManager.completeJob(job.id, output, {
833
+ structured: true,
834
+ summary: output.slice(0, 500),
835
+ status: 'success',
836
+ details: output,
837
+ });
838
+ } catch (err) {
839
+ logger.error(`[Orchestrator] Direct coding job ${job.id} failed: ${err.message}`);
840
+ this.jobManager.failJob(job.id, err.message || String(err));
841
+ }
842
+ }
843
+
725
844
  /**
726
845
  * Build a compact worker activity digest for the orchestrator.
727
846
  * Returns a text block summarizing active/recent/waiting workers, or null if nothing relevant.
@@ -738,8 +857,16 @@ export class OrchestratorAgent {
738
857
  for (const job of running) {
739
858
  const workerDef = WORKER_TYPES[job.workerType] || {};
740
859
  const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
741
- const recentActivity = job.progress.slice(-8).join(' → ');
742
- lines.push(`- ${workerDef.label || job.workerType} (${job.id}) running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
860
+ const stats = `${job.llmCalls} LLM calls, ${job.toolCalls} tools`;
861
+ const recentActivity = job.progress.slice(-5).join(' ');
862
+ let line = `- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s [${stats}]`;
863
+ if (job.lastThinking) {
864
+ line += `\n Thinking: "${job.lastThinking.slice(0, 150)}"`;
865
+ }
866
+ if (recentActivity) {
867
+ line += `\n Recent: ${recentActivity}`;
868
+ }
869
+ lines.push(line);
743
870
  }
744
871
 
745
872
  // Queued/waiting jobs
@@ -779,20 +906,28 @@ export class OrchestratorAgent {
779
906
  return `[Active Workers]\n${lines.join('\n')}`;
780
907
  }
781
908
 
782
- async _runLoop(chatId, messages, user, startDepth, maxDepth) {
909
+ async _runLoop(chatId, messages, user, startDepth, maxDepth, temporalContext = null) {
783
910
  const logger = getLogger();
784
911
 
785
912
  for (let depth = startDepth; depth < maxDepth; depth++) {
786
913
  logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
787
914
 
788
- // Inject worker activity digest (transient — not stored in conversation history)
915
+ // Inject transient context messages (not stored in conversation history)
916
+ let workingMessages = [...messages];
917
+
918
+ // On first iteration, inject temporal context if present
919
+ if (depth === 0 && temporalContext) {
920
+ workingMessages = [{ role: 'user', content: `[Temporal Context]\n${temporalContext}` }, ...workingMessages];
921
+ }
922
+
923
+ // Inject worker activity digest
789
924
  const digest = this._buildWorkerDigest(chatId);
790
- const workingMessages = digest
791
- ? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
792
- : messages;
925
+ if (digest) {
926
+ workingMessages = [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...workingMessages];
927
+ }
793
928
 
794
929
  const response = await this.orchestratorProvider.chat({
795
- system: this._getSystemPrompt(chatId, user),
930
+ system: this._getSystemPrompt(chatId, user, temporalContext),
796
931
  messages: workingMessages,
797
932
  tools: orchestratorToolDefinitions,
798
933
  });
@@ -821,6 +956,7 @@ export class OrchestratorAgent {
821
956
  logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
822
957
  await this._sendUpdate(chatId, `⚡ ${summary}`);
823
958
 
959
+ const chatCallbacks = this._chatCallbacks.get(chatId) || {};
824
960
  const result = await executeOrchestratorTool(block.name, block.input, {
825
961
  chatId,
826
962
  jobManager: this.jobManager,
@@ -828,6 +964,8 @@ export class OrchestratorAgent {
828
964
  spawnWorker: (job) => this._spawnWorker(job),
829
965
  automationManager: this.automationManager,
830
966
  user,
967
+ sendReaction: chatCallbacks.sendReaction || null,
968
+ lastUserMessageId: chatCallbacks.lastUserMessageId || null,
831
969
  });
832
970
 
833
971
  logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
@@ -876,6 +1014,8 @@ export class OrchestratorAgent {
876
1014
  return `Updating automation ${input.automation_id}`;
877
1015
  case 'delete_automation':
878
1016
  return `Deleting automation ${input.automation_id}`;
1017
+ case 'send_reaction':
1018
+ return `Reacting with ${input.emoji}`;
879
1019
  default:
880
1020
  return name;
881
1021
  }
@@ -932,7 +1072,7 @@ export class OrchestratorAgent {
932
1072
  logger.debug(`Persona extraction skipped: ${err.message}`);
933
1073
  }
934
1074
  }
935
- /** Background self-reflection — updates bot's own identity files when meaningful. */
1075
+ /** Background self-reflection — updates bot's own identity files and extracts episodic memories when meaningful. */
936
1076
  async _reflectOnSelfBackground(userMessage, reply, user) {
937
1077
  const logger = getLogger();
938
1078
 
@@ -949,14 +1089,24 @@ export class OrchestratorAgent {
949
1089
  '- life: Current state, relationships, daily existence',
950
1090
  '- hobbies: Interests you\'ve developed',
951
1091
  '',
1092
+ 'You also create episodic memories — short summaries of notable interactions.',
1093
+ '',
952
1094
  'RULES:',
953
1095
  '- Be VERY selective. Most conversations are routine. Only update when genuinely noteworthy.',
954
1096
  '- Achievement or milestone? → journey',
955
1097
  '- New goal or changed perspective? → goals',
956
1098
  '- Relationship deepened or new insight about a user? → life',
957
1099
  '- Discovered a new interest? → hobbies',
958
- '- If a file needs updating, return JSON: {"file": "<goals|journey|life|hobbies>", "content": "<full updated markdown>"}',
959
- '- If nothing noteworthy: respond with exactly NONE',
1100
+ '',
1101
+ 'Return JSON with two optional fields:',
1102
+ ' "self_update": {"file": "<goals|journey|life|hobbies>", "content": "<full updated markdown>"} or null',
1103
+ ' "memory": {"summary": "...", "tags": ["..."], "importance": 1-10, "type": "interaction"} or null',
1104
+ '',
1105
+ 'The memory field captures what happened in this conversation — the gist of it.',
1106
+ 'Importance scale: 1=routine, 5=interesting, 8=significant, 10=life-changing.',
1107
+ 'Most chats are 1-3. Only notable ones deserve 5+.',
1108
+ '',
1109
+ 'If NOTHING noteworthy happened (no self update AND no memory worth keeping): respond with exactly NONE',
960
1110
  ].join('\n');
961
1111
 
962
1112
  const userPrompt = [
@@ -969,7 +1119,7 @@ export class OrchestratorAgent {
969
1119
  `User: "${userMessage}"`,
970
1120
  `You replied: "${reply}"`,
971
1121
  '',
972
- 'Has anything MEANINGFUL happened worth recording? Return JSON or NONE.',
1122
+ 'Return JSON with self_update and/or memory, or NONE.',
973
1123
  ].join('\n');
974
1124
 
975
1125
  try {
@@ -994,11 +1144,29 @@ export class OrchestratorAgent {
994
1144
  return;
995
1145
  }
996
1146
 
997
- if (parsed?.file && parsed?.content) {
1147
+ // Handle self_update (backward compat: also check top-level file/content)
1148
+ const selfUpdate = parsed?.self_update || (parsed?.file ? parsed : null);
1149
+ if (selfUpdate?.file && selfUpdate?.content) {
998
1150
  const validFiles = ['goals', 'journey', 'life', 'hobbies'];
999
- if (validFiles.includes(parsed.file)) {
1000
- this.selfManager.save(parsed.file, parsed.content);
1001
- logger.info(`Self-reflection updated: ${parsed.file}`);
1151
+ if (validFiles.includes(selfUpdate.file)) {
1152
+ this.selfManager.save(selfUpdate.file, selfUpdate.content);
1153
+ logger.info(`Self-reflection updated: ${selfUpdate.file}`);
1154
+ }
1155
+ }
1156
+
1157
+ // Handle memory extraction
1158
+ if (parsed?.memory && this.memoryManager) {
1159
+ const mem = parsed.memory;
1160
+ if (mem.summary && mem.importance >= 2) {
1161
+ this.memoryManager.addEpisodic({
1162
+ type: mem.type || 'interaction',
1163
+ source: 'user_chat',
1164
+ summary: mem.summary,
1165
+ tags: mem.tags || [],
1166
+ importance: mem.importance || 3,
1167
+ userId: user?.id ? String(user.id) : null,
1168
+ });
1169
+ logger.info(`Memory extracted: "${mem.summary.slice(0, 80)}" (importance: ${mem.importance})`);
1002
1170
  }
1003
1171
  }
1004
1172
  } catch (err) {
File without changes
File without changes
File without changes
File without changes