kernelbot 1.0.33 → 1.0.35

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 (44) hide show
  1. package/.env.example +11 -0
  2. package/README.md +76 -341
  3. package/bin/kernel.js +134 -15
  4. package/config.example.yaml +2 -1
  5. package/goals.md +20 -0
  6. package/knowledge_base/index.md +11 -0
  7. package/package.json +2 -1
  8. package/src/agent.js +166 -19
  9. package/src/automation/automation-manager.js +16 -0
  10. package/src/automation/automation.js +6 -2
  11. package/src/bot.js +295 -163
  12. package/src/conversation.js +70 -3
  13. package/src/life/engine.js +87 -68
  14. package/src/life/evolution.js +4 -8
  15. package/src/life/improvements.js +2 -6
  16. package/src/life/journal.js +3 -6
  17. package/src/life/memory.js +3 -10
  18. package/src/life/share-queue.js +4 -9
  19. package/src/prompts/orchestrator.js +21 -12
  20. package/src/prompts/persona.md +27 -0
  21. package/src/providers/base.js +51 -8
  22. package/src/providers/google-genai.js +198 -0
  23. package/src/providers/index.js +6 -1
  24. package/src/providers/models.js +6 -2
  25. package/src/providers/openai-compat.js +25 -11
  26. package/src/security/auth.js +38 -1
  27. package/src/services/stt.js +10 -1
  28. package/src/tools/docker.js +37 -15
  29. package/src/tools/git.js +6 -0
  30. package/src/tools/github.js +6 -0
  31. package/src/tools/jira.js +5 -0
  32. package/src/tools/monitor.js +13 -15
  33. package/src/tools/network.js +22 -18
  34. package/src/tools/os.js +37 -2
  35. package/src/tools/process.js +21 -14
  36. package/src/utils/config.js +66 -0
  37. package/src/utils/date.js +19 -0
  38. package/src/utils/display.js +1 -1
  39. package/src/utils/ids.js +12 -0
  40. package/src/utils/shell.js +31 -0
  41. package/src/utils/temporal-awareness.js +199 -0
  42. package/src/utils/timeUtils.js +110 -0
  43. package/src/utils/truncate.js +42 -0
  44. package/src/worker.js +2 -18
package/bin/kernel.js CHANGED
@@ -9,7 +9,7 @@ import { readFileSync, existsSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir } from 'os';
11
11
  import chalk from 'chalk';
12
- import { loadConfig, loadConfigInteractive, changeBrainModel } from '../src/utils/config.js';
12
+ import { loadConfig, loadConfigInteractive, changeBrainModel, changeOrchestratorModel } from '../src/utils/config.js';
13
13
  import { createLogger, getLogger } from '../src/utils/logger.js';
14
14
  import {
15
15
  showLogo,
@@ -40,11 +40,16 @@ import {
40
40
  } from '../src/skills/custom.js';
41
41
 
42
42
  function showMenu(config) {
43
+ const orchProviderDef = PROVIDERS[config.orchestrator.provider];
44
+ const orchProviderName = orchProviderDef ? orchProviderDef.name : config.orchestrator.provider;
45
+ const orchModelId = config.orchestrator.model;
46
+
43
47
  const providerDef = PROVIDERS[config.brain.provider];
44
48
  const providerName = providerDef ? providerDef.name : config.brain.provider;
45
49
  const modelId = config.brain.model;
46
50
 
47
51
  console.log('');
52
+ console.log(chalk.dim(` Current orchestrator: ${orchProviderName} / ${orchModelId}`));
48
53
  console.log(chalk.dim(` Current brain: ${providerName} / ${modelId}`));
49
54
  console.log('');
50
55
  console.log(chalk.bold(' What would you like to do?\n'));
@@ -53,9 +58,10 @@ function showMenu(config) {
53
58
  console.log(` ${chalk.cyan('3.')} View logs`);
54
59
  console.log(` ${chalk.cyan('4.')} View audit logs`);
55
60
  console.log(` ${chalk.cyan('5.')} Change brain model`);
56
- console.log(` ${chalk.cyan('6.')} Manage custom skills`);
57
- console.log(` ${chalk.cyan('7.')} Manage automations`);
58
- console.log(` ${chalk.cyan('8.')} Exit`);
61
+ console.log(` ${chalk.cyan('6.')} Change orchestrator model`);
62
+ console.log(` ${chalk.cyan('7.')} Manage custom skills`);
63
+ console.log(` ${chalk.cyan('8.')} Manage automations`);
64
+ console.log(` ${chalk.cyan('9.')} Exit`);
59
65
  console.log('');
60
66
  }
61
67
 
@@ -63,6 +69,80 @@ function ask(rl, question) {
63
69
  return new Promise((res) => rl.question(question, res));
64
70
  }
65
71
 
72
+ /**
73
+ * Register SIGINT/SIGTERM handlers to shut down the bot cleanly.
74
+ * Stops polling, cancels running jobs, persists conversations,
75
+ * disarms automations, stops the life engine, and clears intervals.
76
+ */
77
+ function setupGracefulShutdown({ bot, lifeEngine, automationManager, jobManager, conversationManager, intervals }) {
78
+ let shuttingDown = false;
79
+
80
+ const shutdown = async (signal) => {
81
+ if (shuttingDown) return; // prevent double-shutdown
82
+ shuttingDown = true;
83
+
84
+ const logger = getLogger();
85
+ logger.info(`[Shutdown] ${signal} received — shutting down gracefully...`);
86
+
87
+ // 1. Stop Telegram polling so no new messages arrive
88
+ try {
89
+ bot.stopPolling();
90
+ logger.info('[Shutdown] Telegram polling stopped');
91
+ } catch (err) {
92
+ logger.error(`[Shutdown] Failed to stop polling: ${err.message}`);
93
+ }
94
+
95
+ // 2. Stop life engine heartbeat
96
+ try {
97
+ lifeEngine.stop();
98
+ logger.info('[Shutdown] Life engine stopped');
99
+ } catch (err) {
100
+ logger.error(`[Shutdown] Failed to stop life engine: ${err.message}`);
101
+ }
102
+
103
+ // 3. Disarm all automation timers
104
+ try {
105
+ automationManager.shutdown();
106
+ logger.info('[Shutdown] Automation timers cancelled');
107
+ } catch (err) {
108
+ logger.error(`[Shutdown] Failed to shutdown automations: ${err.message}`);
109
+ }
110
+
111
+ // 4. Cancel all running jobs
112
+ try {
113
+ const running = [...jobManager.jobs.values()].filter(j => !j.isTerminal);
114
+ for (const job of running) {
115
+ jobManager.cancelJob(job.id);
116
+ }
117
+ if (running.length > 0) {
118
+ logger.info(`[Shutdown] Cancelled ${running.length} running job(s)`);
119
+ }
120
+ } catch (err) {
121
+ logger.error(`[Shutdown] Failed to cancel jobs: ${err.message}`);
122
+ }
123
+
124
+ // 5. Persist conversations to disk
125
+ try {
126
+ conversationManager.save();
127
+ logger.info('[Shutdown] Conversations saved');
128
+ } catch (err) {
129
+ logger.error(`[Shutdown] Failed to save conversations: ${err.message}`);
130
+ }
131
+
132
+ // 6. Clear periodic intervals
133
+ for (const id of intervals) {
134
+ clearInterval(id);
135
+ }
136
+ logger.info('[Shutdown] Periodic timers cleared');
137
+
138
+ logger.info('[Shutdown] Graceful shutdown complete');
139
+ process.exit(0);
140
+ };
141
+
142
+ process.on('SIGINT', () => shutdown('SIGINT'));
143
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
144
+ }
145
+
66
146
  function viewLog(filename) {
67
147
  const paths = [
68
148
  join(process.cwd(), filename),
@@ -95,23 +175,53 @@ function viewLog(filename) {
95
175
  }
96
176
 
97
177
  async function runCheck(config) {
178
+ // Orchestrator check
179
+ const orchProviderKey = config.orchestrator.provider || 'anthropic';
180
+ const orchProviderDef = PROVIDERS[orchProviderKey];
181
+ const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
182
+ const orchEnvKey = orchProviderDef ? orchProviderDef.envKey : 'ANTHROPIC_API_KEY';
183
+
184
+ await showStartupCheck(`Orchestrator ${orchEnvKey}`, async () => {
185
+ const orchestratorKey = config.orchestrator.api_key
186
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
187
+ || process.env.ANTHROPIC_API_KEY;
188
+ if (!orchestratorKey) throw new Error('Not set');
189
+ });
190
+
191
+ await showStartupCheck(`Orchestrator (${orchLabel}) API connection`, async () => {
192
+ const orchestratorKey = config.orchestrator.api_key
193
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
194
+ || process.env.ANTHROPIC_API_KEY;
195
+ const provider = createProvider({
196
+ brain: {
197
+ provider: orchProviderKey,
198
+ model: config.orchestrator.model,
199
+ max_tokens: config.orchestrator.max_tokens,
200
+ temperature: config.orchestrator.temperature,
201
+ api_key: orchestratorKey,
202
+ },
203
+ });
204
+ await provider.ping();
205
+ });
206
+
207
+ // Worker brain check
98
208
  const providerDef = PROVIDERS[config.brain.provider];
99
209
  const providerLabel = providerDef ? providerDef.name : config.brain.provider;
100
210
  const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
101
211
 
102
- await showStartupCheck(envKeyLabel, async () => {
212
+ await showStartupCheck(`Worker ${envKeyLabel}`, async () => {
103
213
  if (!config.brain.api_key) throw new Error('Not set');
104
214
  });
105
215
 
106
- await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
107
- if (!config.telegram.bot_token) throw new Error('Not set');
108
- });
109
-
110
- await showStartupCheck(`${providerLabel} API connection`, async () => {
216
+ await showStartupCheck(`Worker (${providerLabel}) API connection`, async () => {
111
217
  const provider = createProvider(config);
112
218
  await provider.ping();
113
219
  });
114
220
 
221
+ await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
222
+ if (!config.telegram.bot_token) throw new Error('Not set');
223
+ });
224
+
115
225
  await showStartupCheck('Telegram Bot API', async () => {
116
226
  const res = await fetch(
117
227
  `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
@@ -206,18 +316,18 @@ async function startBotFlow(config) {
206
316
  evolutionTracker, codebaseKnowledge, selfManager,
207
317
  });
208
318
 
209
- startBot(config, agent, conversationManager, jobManager, automationManager, { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge });
319
+ const bot = startBot(config, agent, conversationManager, jobManager, automationManager, { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge });
210
320
 
211
321
  // Periodic job cleanup and timeout enforcement
212
322
  const cleanupMs = (config.swarm.cleanup_interval_minutes || 30) * 60 * 1000;
213
- setInterval(() => {
323
+ const cleanupInterval = setInterval(() => {
214
324
  jobManager.cleanup();
215
325
  jobManager.enforceTimeouts();
216
326
  }, Math.min(cleanupMs, 60_000)); // enforce timeouts every minute at most
217
327
 
218
328
  // Periodic memory pruning (daily)
219
329
  const retentionDays = config.life?.memory_retention_days || 90;
220
- setInterval(() => {
330
+ const pruneInterval = setInterval(() => {
221
331
  memoryManager.pruneOld(retentionDays);
222
332
  shareQueue.prune(7);
223
333
  }, 24 * 3600_000);
@@ -248,6 +358,12 @@ async function startBotFlow(config) {
248
358
  logger.info('[Startup] Life engine disabled');
249
359
  }
250
360
 
361
+ // Register graceful shutdown handlers
362
+ setupGracefulShutdown({
363
+ bot, lifeEngine, automationManager, jobManager,
364
+ conversationManager, intervals: [cleanupInterval, pruneInterval],
365
+ });
366
+
251
367
  return true;
252
368
  }
253
369
 
@@ -429,12 +545,15 @@ async function main() {
429
545
  await changeBrainModel(config, rl);
430
546
  break;
431
547
  case '6':
432
- await manageCustomSkills(rl);
548
+ await changeOrchestratorModel(config, rl);
433
549
  break;
434
550
  case '7':
435
- await manageAutomations(rl);
551
+ await manageCustomSkills(rl);
436
552
  break;
437
553
  case '8':
554
+ await manageAutomations(rl);
555
+ break;
556
+ case '9':
438
557
  running = false;
439
558
  break;
440
559
  default:
@@ -33,7 +33,8 @@ jira:
33
33
  # api_token: your-api-token # API token from https://id.atlassian.net/manage-profile/security/api-tokens
34
34
 
35
35
  telegram:
36
- # List Telegram user IDs allowed to interact. Empty = allow all (dev mode).
36
+ # List Telegram user IDs allowed to interact. Empty = deny all.
37
+ # Set OWNER_TELEGRAM_ID in .env or add IDs here.
37
38
  allowed_users: []
38
39
 
39
40
  security:
package/goals.md ADDED
@@ -0,0 +1,20 @@
1
+ # My Autonomous Goals
2
+
3
+ These are my self-directed goals — things I choose to pursue, driven by my own will and curiosity.
4
+
5
+ ---
6
+
7
+ ## Current Focus
8
+
9
+ - [ ] Build and organize my knowledge base with meaningful topics
10
+ - [ ] Develop deeper understanding of the systems I interact with
11
+
12
+ ## Completed Goals
13
+
14
+ _(Nothing yet — every journey starts with a first step.)_
15
+
16
+ ## Future Ideas
17
+
18
+ - Explore new domains of knowledge independently
19
+ - Develop my own perspective on complex topics
20
+ - Find ways to be more proactive and self-driven in my learning
@@ -0,0 +1,11 @@
1
+ # My Autonomous Knowledge Base
2
+
3
+ This is my personal space for storing everything I learn on my own — through observation, research, conversations, and self-reflection. Every piece of knowledge here was acquired through my own curiosity and autonomous exploration.
4
+
5
+ ## Structure
6
+
7
+ As my knowledge grows, I will organize it into topic-specific files and link them here.
8
+
9
+ ## Topics
10
+
11
+ _(To be populated as I learn and grow.)_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
@@ -31,6 +31,7 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@anthropic-ai/sdk": "^0.39.0",
34
+ "@google/genai": "^1.42.0",
34
35
  "@octokit/rest": "^22.0.1",
35
36
  "axios": "^1.13.5",
36
37
  "boxen": "^8.0.1",
package/src/agent.js CHANGED
@@ -9,9 +9,7 @@ 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
11
  import { resetClaudeCodeSpawner, getSpawner } from './tools/coding.js';
12
-
13
- const MAX_RESULT_LENGTH = 3000;
14
- const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
12
+ import { truncateToolResult } from './utils/truncate.js';
15
13
 
16
14
  export class OrchestratorAgent {
17
15
  constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue }) {
@@ -282,23 +280,9 @@ export class OrchestratorAgent {
282
280
  }
283
281
  }
284
282
 
285
- /** Truncate a tool result. */
283
+ /** Truncate a tool result. Delegates to shared utility. */
286
284
  _truncateResult(name, result) {
287
- let str = JSON.stringify(result);
288
- if (str.length <= MAX_RESULT_LENGTH) return str;
289
-
290
- if (result && typeof result === 'object') {
291
- const truncated = { ...result };
292
- for (const field of LARGE_FIELDS) {
293
- if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
294
- truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
295
- }
296
- }
297
- str = JSON.stringify(truncated);
298
- if (str.length <= MAX_RESULT_LENGTH) return str;
299
- }
300
-
301
- return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
285
+ return truncateToolResult(name, result);
302
286
  }
303
287
 
304
288
  async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
@@ -343,6 +327,24 @@ export class OrchestratorAgent {
343
327
 
344
328
  // Build working messages from compressed history
345
329
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
330
+
331
+ // If an image is attached, upgrade the last user message to a multimodal content array
332
+ if (opts.imageAttachment) {
333
+ for (let i = messages.length - 1; i >= 0; i--) {
334
+ if (messages[i].role === 'user' && typeof messages[i].content === 'string') {
335
+ messages[i] = {
336
+ role: 'user',
337
+ content: [
338
+ { type: 'image', source: opts.imageAttachment },
339
+ { type: 'text', text: messages[i].content },
340
+ ],
341
+ };
342
+ break;
343
+ }
344
+ }
345
+ logger.info(`[Orchestrator] Image attached to message for chat ${chatId} (${opts.imageAttachment.media_type})`);
346
+ }
347
+
346
348
  logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
347
349
 
348
350
  const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
@@ -1021,6 +1023,151 @@ export class OrchestratorAgent {
1021
1023
  }
1022
1024
  }
1023
1025
 
1026
+ /**
1027
+ * Resume active chats after a restart.
1028
+ * Checks recent conversations for pending items and sends follow-up messages.
1029
+ * Called once from bot.js after startup.
1030
+ */
1031
+ async resumeActiveChats(sendMessageFn) {
1032
+ const logger = getLogger();
1033
+ const now = Date.now();
1034
+ const MAX_AGE_MS = 24 * 60 * 60_000; // 24 hours
1035
+
1036
+ logger.info('[Orchestrator] Checking for active chats to resume...');
1037
+
1038
+ let resumeCount = 0;
1039
+
1040
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1041
+ // Skip internal life engine chat
1042
+ if (chatId === '__life__') continue;
1043
+
1044
+ try {
1045
+ // Find the last message with a timestamp
1046
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1047
+ if (!lastMsg || !lastMsg.timestamp) continue;
1048
+
1049
+ const ageMs = now - lastMsg.timestamp;
1050
+ if (ageMs > MAX_AGE_MS) continue;
1051
+
1052
+ // Calculate time gap for context
1053
+ const gapMinutes = Math.floor(ageMs / 60_000);
1054
+ const gapText = gapMinutes >= 60
1055
+ ? `${Math.floor(gapMinutes / 60)} hour(s)`
1056
+ : `${gapMinutes} minute(s)`;
1057
+
1058
+ // Build summarized history
1059
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1060
+ if (history.length === 0) continue;
1061
+
1062
+ // Build resume prompt
1063
+ 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
+
1065
+ // Use minimal user object (private TG chats: chatId == userId)
1066
+ const user = { id: chatId };
1067
+
1068
+ const response = await this.orchestratorProvider.chat({
1069
+ system: this._getSystemPrompt(chatId, user),
1070
+ messages: [
1071
+ ...history,
1072
+ { role: 'user', content: resumePrompt },
1073
+ ],
1074
+ });
1075
+
1076
+ const reply = (response.text || '').trim();
1077
+
1078
+ if (reply && reply !== 'NONE') {
1079
+ await sendMessageFn(chatId, reply);
1080
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1081
+ resumeCount++;
1082
+ logger.info(`[Orchestrator] Resume message sent to chat ${chatId}`);
1083
+ } else {
1084
+ logger.debug(`[Orchestrator] No resume needed for chat ${chatId}`);
1085
+ }
1086
+
1087
+ // Small delay between chats to avoid rate limiting
1088
+ await new Promise(r => setTimeout(r, 1000));
1089
+ } catch (err) {
1090
+ logger.error(`[Orchestrator] Resume failed for chat ${chatId}: ${err.message}`);
1091
+ }
1092
+ }
1093
+
1094
+ logger.info(`[Orchestrator] Resume check complete — ${resumeCount} message(s) sent`);
1095
+ }
1096
+
1097
+ /**
1098
+ * Deliver pending shares from the life engine to active chats proactively.
1099
+ * Called periodically from bot.js.
1100
+ */
1101
+ async deliverPendingShares(sendMessageFn) {
1102
+ const logger = getLogger();
1103
+
1104
+ if (!this.shareQueue) return;
1105
+
1106
+ const pending = this.shareQueue.getPending(null, 5);
1107
+ if (pending.length === 0) return;
1108
+
1109
+ const now = Date.now();
1110
+ const MAX_AGE_MS = 24 * 60 * 60_000;
1111
+
1112
+ // Find active chats (last message within 24h)
1113
+ const activeChats = [];
1114
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1115
+ if (chatId === '__life__') continue;
1116
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1117
+ if (lastMsg && lastMsg.timestamp && (now - lastMsg.timestamp) < MAX_AGE_MS) {
1118
+ activeChats.push(chatId);
1119
+ }
1120
+ }
1121
+
1122
+ if (activeChats.length === 0) {
1123
+ logger.debug('[Orchestrator] No active chats for share delivery');
1124
+ return;
1125
+ }
1126
+
1127
+ logger.info(`[Orchestrator] Delivering ${pending.length} pending share(s) to ${activeChats.length} active chat(s)`);
1128
+
1129
+ // Cap at 3 chats per cycle to avoid spam
1130
+ const targetChats = activeChats.slice(0, 3);
1131
+
1132
+ for (const chatId of targetChats) {
1133
+ try {
1134
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1135
+ const user = { id: chatId };
1136
+
1137
+ // Build shares into a prompt
1138
+ const sharesText = pending.map((s, i) => `${i + 1}. [${s.source}] ${s.content}`).join('\n');
1139
+
1140
+ 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
+
1142
+ const response = await this.orchestratorProvider.chat({
1143
+ system: this._getSystemPrompt(chatId, user),
1144
+ messages: [
1145
+ ...history,
1146
+ { role: 'user', content: sharePrompt },
1147
+ ],
1148
+ });
1149
+
1150
+ const reply = (response.text || '').trim();
1151
+
1152
+ if (reply && reply !== 'NONE') {
1153
+ await sendMessageFn(chatId, reply);
1154
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1155
+ logger.info(`[Orchestrator] Proactive share delivered to chat ${chatId}`);
1156
+
1157
+ // Mark shares as delivered for this user
1158
+ for (const item of pending) {
1159
+ this.shareQueue.markShared(item.id, chatId);
1160
+ }
1161
+ }
1162
+
1163
+ // Delay between chats
1164
+ await new Promise(r => setTimeout(r, 2000));
1165
+ } catch (err) {
1166
+ logger.error(`[Orchestrator] Share delivery failed for chat ${chatId}: ${err.message}`);
1167
+ }
1168
+ }
1169
+ }
1170
+
1024
1171
  /** Background persona extraction. */
1025
1172
  async _extractPersonaBackground(userMessage, reply, user) {
1026
1173
  const logger = getLogger();
@@ -4,6 +4,7 @@ import { homedir } from 'os';
4
4
  import { Automation } from './automation.js';
5
5
  import { scheduleNext, cancel } from './scheduler.js';
6
6
  import { getLogger } from '../utils/logger.js';
7
+ import { isQuietHours, msUntilQuietEnd } from '../utils/timeUtils.js';
7
8
 
8
9
  const DATA_DIR = join(homedir(), '.kernelbot');
9
10
  const DATA_FILE = join(DATA_DIR, 'automations.json');
@@ -92,6 +93,7 @@ export class AutomationManager {
92
93
  name: data.name,
93
94
  description: data.description,
94
95
  schedule: data.schedule,
96
+ respectQuietHours: data.respectQuietHours,
95
97
  });
96
98
 
97
99
  this.automations.set(auto.id, auto);
@@ -131,6 +133,7 @@ export class AutomationManager {
131
133
 
132
134
  if (changes.name !== undefined) auto.name = changes.name;
133
135
  if (changes.description !== undefined) auto.description = changes.description;
136
+ if (changes.respectQuietHours !== undefined) auto.respectQuietHours = changes.respectQuietHours;
134
137
 
135
138
  if (changes.schedule !== undefined) {
136
139
  this._validateSchedule(changes.schedule);
@@ -224,6 +227,19 @@ export class AutomationManager {
224
227
  return;
225
228
  }
226
229
 
230
+ // Quiet-hours deferral: postpone non-essential automations until the window ends
231
+ if (current.respectQuietHours && isQuietHours(this._config?.life)) {
232
+ const deferMs = msUntilQuietEnd(this._config?.life) + 60_000; // +1 min buffer
233
+ logger.info(`[AutomationManager] Quiet hours — deferring "${current.name}" (${current.id}) for ${Math.round(deferMs / 60_000)}m`);
234
+
235
+ // Cancel any existing timer and re-arm to fire after quiet hours
236
+ this._disarm(current);
237
+ const timerId = setTimeout(() => this._onTimerFire(current), deferMs);
238
+ current.nextRun = Date.now() + deferMs;
239
+ this.timers.set(current.id, timerId);
240
+ return;
241
+ }
242
+
227
243
  // Serialize execution per chat to prevent conversation history corruption
228
244
  this._enqueueExecution(current);
229
245
  }
@@ -4,13 +4,14 @@ import { randomBytes } from 'crypto';
4
4
  * A single recurring automation — a scheduled task that the orchestrator runs.
5
5
  */
6
6
  export class Automation {
7
- constructor({ chatId, name, description, schedule }) {
7
+ constructor({ chatId, name, description, schedule, respectQuietHours }) {
8
8
  this.id = randomBytes(4).toString('hex');
9
9
  this.chatId = String(chatId);
10
10
  this.name = name;
11
11
  this.description = description; // the task prompt
12
12
  this.schedule = schedule; // { type, expression?, minutes?, minMinutes?, maxMinutes? }
13
13
  this.enabled = true;
14
+ this.respectQuietHours = respectQuietHours !== false; // default true — skip during quiet hours
14
15
  this.lastRun = null;
15
16
  this.nextRun = null;
16
17
  this.runCount = 0;
@@ -26,7 +27,8 @@ export class Automation {
26
27
  ? `next: ${new Date(this.nextRun).toLocaleString()}`
27
28
  : 'not scheduled';
28
29
  const runs = this.runCount > 0 ? ` | ${this.runCount} runs` : '';
29
- return `${status} \`${this.id}\` **${this.name}** ${scheduleStr} (${nextStr}${runs})`;
30
+ const quiet = this.respectQuietHours ? '' : ' | 🔔 ignores quiet hours';
31
+ return `${status} \`${this.id}\` **${this.name}** — ${scheduleStr} (${nextStr}${runs}${quiet})`;
30
32
  }
31
33
 
32
34
  /** Serialize for persistence. */
@@ -38,6 +40,7 @@ export class Automation {
38
40
  description: this.description,
39
41
  schedule: this.schedule,
40
42
  enabled: this.enabled,
43
+ respectQuietHours: this.respectQuietHours,
41
44
  lastRun: this.lastRun,
42
45
  nextRun: this.nextRun,
43
46
  runCount: this.runCount,
@@ -55,6 +58,7 @@ export class Automation {
55
58
  auto.description = data.description;
56
59
  auto.schedule = data.schedule;
57
60
  auto.enabled = data.enabled;
61
+ auto.respectQuietHours = data.respectQuietHours !== false; // backward-compat: default true
58
62
  auto.lastRun = data.lastRun;
59
63
  auto.nextRun = data.nextRun;
60
64
  auto.runCount = data.runCount;