kernelbot 1.0.33 → 1.0.34

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 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
 
@@ -95,23 +101,53 @@ function viewLog(filename) {
95
101
  }
96
102
 
97
103
  async function runCheck(config) {
104
+ // Orchestrator check
105
+ const orchProviderKey = config.orchestrator.provider || 'anthropic';
106
+ const orchProviderDef = PROVIDERS[orchProviderKey];
107
+ const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
108
+ const orchEnvKey = orchProviderDef ? orchProviderDef.envKey : 'ANTHROPIC_API_KEY';
109
+
110
+ await showStartupCheck(`Orchestrator ${orchEnvKey}`, async () => {
111
+ const orchestratorKey = config.orchestrator.api_key
112
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
113
+ || process.env.ANTHROPIC_API_KEY;
114
+ if (!orchestratorKey) throw new Error('Not set');
115
+ });
116
+
117
+ await showStartupCheck(`Orchestrator (${orchLabel}) API connection`, async () => {
118
+ const orchestratorKey = config.orchestrator.api_key
119
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
120
+ || process.env.ANTHROPIC_API_KEY;
121
+ const provider = createProvider({
122
+ brain: {
123
+ provider: orchProviderKey,
124
+ model: config.orchestrator.model,
125
+ max_tokens: config.orchestrator.max_tokens,
126
+ temperature: config.orchestrator.temperature,
127
+ api_key: orchestratorKey,
128
+ },
129
+ });
130
+ await provider.ping();
131
+ });
132
+
133
+ // Worker brain check
98
134
  const providerDef = PROVIDERS[config.brain.provider];
99
135
  const providerLabel = providerDef ? providerDef.name : config.brain.provider;
100
136
  const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
101
137
 
102
- await showStartupCheck(envKeyLabel, async () => {
138
+ await showStartupCheck(`Worker ${envKeyLabel}`, async () => {
103
139
  if (!config.brain.api_key) throw new Error('Not set');
104
140
  });
105
141
 
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 () => {
142
+ await showStartupCheck(`Worker (${providerLabel}) API connection`, async () => {
111
143
  const provider = createProvider(config);
112
144
  await provider.ping();
113
145
  });
114
146
 
147
+ await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
148
+ if (!config.telegram.bot_token) throw new Error('Not set');
149
+ });
150
+
115
151
  await showStartupCheck('Telegram Bot API', async () => {
116
152
  const res = await fetch(
117
153
  `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
@@ -429,12 +465,15 @@ async function main() {
429
465
  await changeBrainModel(config, rl);
430
466
  break;
431
467
  case '6':
432
- await manageCustomSkills(rl);
468
+ await changeOrchestratorModel(config, rl);
433
469
  break;
434
470
  case '7':
435
- await manageAutomations(rl);
471
+ await manageCustomSkills(rl);
436
472
  break;
437
473
  case '8':
474
+ await manageAutomations(rl);
475
+ break;
476
+ case '9':
438
477
  running = false;
439
478
  break;
440
479
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
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 = {}) {
@@ -1021,6 +1005,151 @@ export class OrchestratorAgent {
1021
1005
  }
1022
1006
  }
1023
1007
 
1008
+ /**
1009
+ * Resume active chats after a restart.
1010
+ * Checks recent conversations for pending items and sends follow-up messages.
1011
+ * Called once from bot.js after startup.
1012
+ */
1013
+ async resumeActiveChats(sendMessageFn) {
1014
+ const logger = getLogger();
1015
+ const now = Date.now();
1016
+ const MAX_AGE_MS = 24 * 60 * 60_000; // 24 hours
1017
+
1018
+ logger.info('[Orchestrator] Checking for active chats to resume...');
1019
+
1020
+ let resumeCount = 0;
1021
+
1022
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1023
+ // Skip internal life engine chat
1024
+ if (chatId === '__life__') continue;
1025
+
1026
+ try {
1027
+ // Find the last message with a timestamp
1028
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1029
+ if (!lastMsg || !lastMsg.timestamp) continue;
1030
+
1031
+ const ageMs = now - lastMsg.timestamp;
1032
+ if (ageMs > MAX_AGE_MS) continue;
1033
+
1034
+ // Calculate time gap for context
1035
+ const gapMinutes = Math.floor(ageMs / 60_000);
1036
+ const gapText = gapMinutes >= 60
1037
+ ? `${Math.floor(gapMinutes / 60)} hour(s)`
1038
+ : `${gapMinutes} minute(s)`;
1039
+
1040
+ // Build summarized history
1041
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1042
+ if (history.length === 0) continue;
1043
+
1044
+ // Build resume prompt
1045
+ 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`;
1046
+
1047
+ // Use minimal user object (private TG chats: chatId == userId)
1048
+ const user = { id: chatId };
1049
+
1050
+ const response = await this.orchestratorProvider.chat({
1051
+ system: this._getSystemPrompt(chatId, user),
1052
+ messages: [
1053
+ ...history,
1054
+ { role: 'user', content: resumePrompt },
1055
+ ],
1056
+ });
1057
+
1058
+ const reply = (response.text || '').trim();
1059
+
1060
+ if (reply && reply !== 'NONE') {
1061
+ await sendMessageFn(chatId, reply);
1062
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1063
+ resumeCount++;
1064
+ logger.info(`[Orchestrator] Resume message sent to chat ${chatId}`);
1065
+ } else {
1066
+ logger.debug(`[Orchestrator] No resume needed for chat ${chatId}`);
1067
+ }
1068
+
1069
+ // Small delay between chats to avoid rate limiting
1070
+ await new Promise(r => setTimeout(r, 1000));
1071
+ } catch (err) {
1072
+ logger.error(`[Orchestrator] Resume failed for chat ${chatId}: ${err.message}`);
1073
+ }
1074
+ }
1075
+
1076
+ logger.info(`[Orchestrator] Resume check complete — ${resumeCount} message(s) sent`);
1077
+ }
1078
+
1079
+ /**
1080
+ * Deliver pending shares from the life engine to active chats proactively.
1081
+ * Called periodically from bot.js.
1082
+ */
1083
+ async deliverPendingShares(sendMessageFn) {
1084
+ const logger = getLogger();
1085
+
1086
+ if (!this.shareQueue) return;
1087
+
1088
+ const pending = this.shareQueue.getPending(null, 5);
1089
+ if (pending.length === 0) return;
1090
+
1091
+ const now = Date.now();
1092
+ const MAX_AGE_MS = 24 * 60 * 60_000;
1093
+
1094
+ // Find active chats (last message within 24h)
1095
+ const activeChats = [];
1096
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1097
+ if (chatId === '__life__') continue;
1098
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1099
+ if (lastMsg && lastMsg.timestamp && (now - lastMsg.timestamp) < MAX_AGE_MS) {
1100
+ activeChats.push(chatId);
1101
+ }
1102
+ }
1103
+
1104
+ if (activeChats.length === 0) {
1105
+ logger.debug('[Orchestrator] No active chats for share delivery');
1106
+ return;
1107
+ }
1108
+
1109
+ logger.info(`[Orchestrator] Delivering ${pending.length} pending share(s) to ${activeChats.length} active chat(s)`);
1110
+
1111
+ // Cap at 3 chats per cycle to avoid spam
1112
+ const targetChats = activeChats.slice(0, 3);
1113
+
1114
+ for (const chatId of targetChats) {
1115
+ try {
1116
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1117
+ const user = { id: chatId };
1118
+
1119
+ // Build shares into a prompt
1120
+ const sharesText = pending.map((s, i) => `${i + 1}. [${s.source}] ${s.content}`).join('\n');
1121
+
1122
+ 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`;
1123
+
1124
+ const response = await this.orchestratorProvider.chat({
1125
+ system: this._getSystemPrompt(chatId, user),
1126
+ messages: [
1127
+ ...history,
1128
+ { role: 'user', content: sharePrompt },
1129
+ ],
1130
+ });
1131
+
1132
+ const reply = (response.text || '').trim();
1133
+
1134
+ if (reply && reply !== 'NONE') {
1135
+ await sendMessageFn(chatId, reply);
1136
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1137
+ logger.info(`[Orchestrator] Proactive share delivered to chat ${chatId}`);
1138
+
1139
+ // Mark shares as delivered for this user
1140
+ for (const item of pending) {
1141
+ this.shareQueue.markShared(item.id, chatId);
1142
+ }
1143
+ }
1144
+
1145
+ // Delay between chats
1146
+ await new Promise(r => setTimeout(r, 2000));
1147
+ } catch (err) {
1148
+ logger.error(`[Orchestrator] Share delivery failed for chat ${chatId}: ${err.message}`);
1149
+ }
1150
+ }
1151
+ }
1152
+
1024
1153
  /** Background persona extraction. */
1025
1154
  async _extractPersonaBackground(userMessage, reply, user) {
1026
1155
  const logger = getLogger();
package/src/bot.js CHANGED
@@ -16,6 +16,33 @@ import { TTSService } from './services/tts.js';
16
16
  import { STTService } from './services/stt.js';
17
17
  import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
18
18
 
19
+ /**
20
+ * Simulate a human-like typing delay based on response length.
21
+ * Short replies (casual chat) get a brief pause; longer replies get more.
22
+ * Keeps the typing indicator alive during the delay so the user sees "typing...".
23
+ *
24
+ * @param {TelegramBot} bot - Telegram bot instance
25
+ * @param {number} chatId - Chat to show typing in
26
+ * @param {string} text - The reply text (used to calculate delay)
27
+ * @returns {Promise<void>}
28
+ */
29
+ async function simulateTypingDelay(bot, chatId, text) {
30
+ const length = (text || '').length;
31
+
32
+ // ~25ms per character, clamped between 0.4s and 4s
33
+ // Short "hey ❤️" (~6 chars) → 0.4s | Medium reply (~120 chars) → 3s | Long reply → 4s cap
34
+ const delay = Math.min(4000, Math.max(400, length * 25));
35
+
36
+ // Add a small random jitter (±15%) so it doesn't feel mechanical
37
+ const jitter = delay * (0.85 + Math.random() * 0.3);
38
+ const finalDelay = Math.round(jitter);
39
+
40
+ // Keep the typing indicator alive during the delay
41
+ bot.sendChatAction(chatId, 'typing').catch(() => {});
42
+
43
+ return new Promise((resolve) => setTimeout(resolve, finalDelay));
44
+ }
45
+
19
46
  function splitMessage(text, maxLength = 4096) {
20
47
  if (text.length <= maxLength) return [text];
21
48
 
@@ -35,6 +62,83 @@ function splitMessage(text, maxLength = 4096) {
35
62
  return chunks;
36
63
  }
37
64
 
65
+ /**
66
+ * Create an onUpdate callback that sends or edits Telegram messages.
67
+ * Tries Markdown first, falls back to plain text.
68
+ */
69
+ function createOnUpdate(bot, chatId) {
70
+ return async (update, opts = {}) => {
71
+ if (opts.editMessageId) {
72
+ try {
73
+ const edited = await bot.editMessageText(update, {
74
+ chat_id: chatId,
75
+ message_id: opts.editMessageId,
76
+ parse_mode: 'Markdown',
77
+ });
78
+ return edited.message_id;
79
+ } catch {
80
+ try {
81
+ const edited = await bot.editMessageText(update, {
82
+ chat_id: chatId,
83
+ message_id: opts.editMessageId,
84
+ });
85
+ return edited.message_id;
86
+ } catch {
87
+ // Edit failed — fall through to send new message
88
+ }
89
+ }
90
+ }
91
+ const parts = splitMessage(update);
92
+ let lastMsgId = null;
93
+ for (const part of parts) {
94
+ try {
95
+ const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
96
+ lastMsgId = sent.message_id;
97
+ } catch {
98
+ const sent = await bot.sendMessage(chatId, part);
99
+ lastMsgId = sent.message_id;
100
+ }
101
+ }
102
+ return lastMsgId;
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Create a sendPhoto callback that sends a photo with optional caption.
108
+ * Tries Markdown caption first, falls back to plain caption.
109
+ */
110
+ function createSendPhoto(bot, chatId, logger) {
111
+ return async (filePath, caption) => {
112
+ const fileOpts = { contentType: 'image/png' };
113
+ try {
114
+ await bot.sendPhoto(chatId, createReadStream(filePath), {
115
+ caption: caption || '',
116
+ parse_mode: 'Markdown',
117
+ }, fileOpts);
118
+ } catch {
119
+ try {
120
+ await bot.sendPhoto(chatId, createReadStream(filePath), {
121
+ caption: caption || '',
122
+ }, fileOpts);
123
+ } catch (err) {
124
+ logger.error(`Failed to send photo: ${err.message}`);
125
+ }
126
+ }
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Create a sendReaction callback for reacting to messages with emoji.
132
+ */
133
+ function createSendReaction(bot) {
134
+ return async (targetChatId, targetMsgId, emoji, isBig = false) => {
135
+ await bot.setMessageReaction(targetChatId, targetMsgId, {
136
+ reaction: [{ type: 'emoji', emoji }],
137
+ is_big: isBig,
138
+ });
139
+ };
140
+ }
141
+
38
142
  /**
39
143
  * Simple per-chat queue to serialize agent processing.
40
144
  * Each chat gets its own promise chain so messages are processed in order.
@@ -119,54 +223,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
119
223
  const sendAction = (chatId, action) => bot.sendChatAction(chatId, action).catch(() => {});
120
224
 
121
225
  const agentFactory = (chatId) => {
122
- const onUpdate = async (update, opts = {}) => {
123
- if (opts.editMessageId) {
124
- try {
125
- const edited = await bot.editMessageText(update, {
126
- chat_id: chatId,
127
- message_id: opts.editMessageId,
128
- parse_mode: 'Markdown',
129
- });
130
- return edited.message_id;
131
- } catch {
132
- try {
133
- const edited = await bot.editMessageText(update, {
134
- chat_id: chatId,
135
- message_id: opts.editMessageId,
136
- });
137
- return edited.message_id;
138
- } catch {
139
- // Edit failed — fall through to send new message
140
- }
141
- }
142
- }
143
- const parts = splitMessage(update);
144
- let lastMsgId = null;
145
- for (const part of parts) {
146
- try {
147
- const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
148
- lastMsgId = sent.message_id;
149
- } catch {
150
- const sent = await bot.sendMessage(chatId, part);
151
- lastMsgId = sent.message_id;
152
- }
153
- }
154
- return lastMsgId;
155
- };
156
-
157
- const sendPhoto = async (filePath, caption) => {
158
- const fileOpts = { contentType: 'image/png' };
159
- try {
160
- await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '', parse_mode: 'Markdown' }, fileOpts);
161
- } catch {
162
- try {
163
- await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '' }, fileOpts);
164
- } catch (err) {
165
- logger.error(`[Automation] Failed to send photo: ${err.message}`);
166
- }
167
- }
168
- };
169
-
226
+ const onUpdate = createOnUpdate(bot, chatId);
227
+ const sendPhoto = createSendPhoto(bot, chatId, logger);
170
228
  return { agent, onUpdate, sendPhoto };
171
229
  };
172
230
 
@@ -1580,68 +1638,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1580
1638
  bot.sendChatAction(chatId, 'typing').catch(() => {});
1581
1639
 
1582
1640
  try {
1583
- const onUpdate = async (update, opts = {}) => {
1584
- // Edit an existing message instead of sending a new one
1585
- if (opts.editMessageId) {
1586
- try {
1587
- const edited = await bot.editMessageText(update, {
1588
- chat_id: chatId,
1589
- message_id: opts.editMessageId,
1590
- parse_mode: 'Markdown',
1591
- });
1592
- return edited.message_id;
1593
- } catch {
1594
- try {
1595
- const edited = await bot.editMessageText(update, {
1596
- chat_id: chatId,
1597
- message_id: opts.editMessageId,
1598
- });
1599
- return edited.message_id;
1600
- } catch {
1601
- // Edit failed — fall through to send new message
1602
- }
1603
- }
1604
- }
1605
-
1606
- // Send new message(s) — also reached when edit fails
1607
- const parts = splitMessage(update);
1608
- let lastMsgId = null;
1609
- for (const part of parts) {
1610
- try {
1611
- const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
1612
- lastMsgId = sent.message_id;
1613
- } catch {
1614
- const sent = await bot.sendMessage(chatId, part);
1615
- lastMsgId = sent.message_id;
1616
- }
1617
- }
1618
- return lastMsgId;
1619
- };
1620
-
1621
- const sendPhoto = async (filePath, caption) => {
1622
- const fileOpts = { contentType: 'image/png' };
1623
- try {
1624
- await bot.sendPhoto(chatId, createReadStream(filePath), {
1625
- caption: caption || '',
1626
- parse_mode: 'Markdown',
1627
- }, fileOpts);
1628
- } catch {
1629
- try {
1630
- await bot.sendPhoto(chatId, createReadStream(filePath), {
1631
- caption: caption || '',
1632
- }, fileOpts);
1633
- } catch (err) {
1634
- logger.error(`Failed to send photo: ${err.message}`);
1635
- }
1636
- }
1637
- };
1638
-
1639
- const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
1640
- await bot.setMessageReaction(targetChatId, targetMsgId, {
1641
- reaction: [{ type: 'emoji', emoji }],
1642
- is_big: isBig,
1643
- });
1644
- };
1641
+ const onUpdate = createOnUpdate(bot, chatId);
1642
+ const sendPhoto = createSendPhoto(bot, chatId, logger);
1643
+ const sendReaction = createSendReaction(bot);
1645
1644
 
1646
1645
  logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
1647
1646
  const reply = await agent.processMessage(chatId, mergedText, {
@@ -1651,6 +1650,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1651
1650
 
1652
1651
  clearInterval(typingInterval);
1653
1652
 
1653
+ // Simulate human-like typing delay before sending the reply
1654
+ await simulateTypingDelay(bot, chatId, reply || '');
1655
+
1654
1656
  logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
1655
1657
  const chunks = splitMessage(reply || 'Done.');
1656
1658
  for (const chunk of chunks) {
@@ -1702,47 +1704,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1702
1704
 
1703
1705
  chatQueue.enqueue(chatId, async () => {
1704
1706
  try {
1705
- const onUpdate = async (update, opts = {}) => {
1706
- if (opts.editMessageId) {
1707
- try {
1708
- const edited = await bot.editMessageText(update, {
1709
- chat_id: chatId,
1710
- message_id: opts.editMessageId,
1711
- parse_mode: 'Markdown',
1712
- });
1713
- return edited.message_id;
1714
- } catch {
1715
- try {
1716
- const edited = await bot.editMessageText(update, {
1717
- chat_id: chatId,
1718
- message_id: opts.editMessageId,
1719
- });
1720
- return edited.message_id;
1721
- } catch {
1722
- // fall through
1723
- }
1724
- }
1725
- }
1726
- const parts = splitMessage(update);
1727
- let lastMsgId = null;
1728
- for (const part of parts) {
1729
- try {
1730
- const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
1731
- lastMsgId = sent.message_id;
1732
- } catch {
1733
- const sent = await bot.sendMessage(chatId, part);
1734
- lastMsgId = sent.message_id;
1735
- }
1736
- }
1737
- return lastMsgId;
1738
- };
1739
-
1740
- const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
1741
- await bot.setMessageReaction(targetChatId, targetMsgId, {
1742
- reaction: [{ type: 'emoji', emoji }],
1743
- is_big: isBig,
1744
- });
1745
- };
1707
+ const onUpdate = createOnUpdate(bot, chatId);
1708
+ const sendReaction = createSendReaction(bot);
1746
1709
 
1747
1710
  const reply = await agent.processMessage(chatId, reactionText, {
1748
1711
  id: userId,
@@ -1769,5 +1732,68 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1769
1732
  logger.error(`Telegram polling error: ${err.message}`);
1770
1733
  });
1771
1734
 
1735
+ // ── Resume active chats after restart ────────────────────────
1736
+ setTimeout(async () => {
1737
+ const sendMsg = async (chatId, text) => {
1738
+ try {
1739
+ await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
1740
+ } catch {
1741
+ await bot.sendMessage(chatId, text);
1742
+ }
1743
+ };
1744
+ try {
1745
+ await agent.resumeActiveChats(sendMsg);
1746
+ } catch (err) {
1747
+ logger.error(`[Bot] Resume active chats failed: ${err.message}`);
1748
+ }
1749
+ }, 5000);
1750
+
1751
+ // ── Proactive share delivery (randomized, self-rearming) ────
1752
+ const lifeConfig = config.life || {};
1753
+ const quietStart = lifeConfig.quiet_hours?.start ?? 2;
1754
+ const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
1755
+
1756
+ const armShareDelivery = (delivered) => {
1757
+ // If we just delivered something, wait longer (1–4h) before next check
1758
+ // If nothing was delivered, check again sooner (10–45min) in case new shares appear
1759
+ const minMin = delivered ? 60 : 10;
1760
+ const maxMin = delivered ? 240 : 45;
1761
+ const delayMs = (minMin + Math.random() * (maxMin - minMin)) * 60_000;
1762
+
1763
+ logger.debug(`[Bot] Next share check in ${Math.round(delayMs / 60_000)}m`);
1764
+
1765
+ setTimeout(async () => {
1766
+ // Respect quiet hours
1767
+ const hour = new Date().getHours();
1768
+ if (hour >= quietStart && hour < quietEnd) {
1769
+ armShareDelivery(false);
1770
+ return;
1771
+ }
1772
+
1773
+ const sendMsg = async (chatId, text) => {
1774
+ try {
1775
+ await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
1776
+ } catch {
1777
+ await bot.sendMessage(chatId, text);
1778
+ }
1779
+ };
1780
+
1781
+ let didDeliver = false;
1782
+ try {
1783
+ const before = shareQueue ? shareQueue.getPending(null, 1).length : 0;
1784
+ await agent.deliverPendingShares(sendMsg);
1785
+ const after = shareQueue ? shareQueue.getPending(null, 1).length : 0;
1786
+ didDeliver = before > 0 && after < before;
1787
+ } catch (err) {
1788
+ logger.error(`[Bot] Proactive share delivery failed: ${err.message}`);
1789
+ }
1790
+
1791
+ armShareDelivery(didDeliver);
1792
+ }, delayMs);
1793
+ };
1794
+
1795
+ // Start the first check after a random 10–30 min
1796
+ armShareDelivery(false);
1797
+
1772
1798
  return bot;
1773
1799
  }