kernelbot 1.0.34 → 1.0.36
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/.env.example +11 -0
- package/README.md +48 -318
- package/bin/kernel.js +89 -16
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +1 -1
- package/src/agent.js +19 -1
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +129 -23
- package/src/life/engine.js +87 -68
- package/src/life/evolution.js +4 -8
- package/src/life/improvements.js +2 -6
- package/src/life/journal.js +3 -6
- package/src/life/memory.js +3 -10
- package/src/life/share-queue.js +4 -9
- package/src/prompts/orchestrator.js +21 -12
- package/src/providers/base.js +36 -4
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +34 -5
- package/src/tools/git.js +6 -0
- package/src/tools/github.js +6 -0
- package/src/tools/jira.js +5 -0
- package/src/tools/monitor.js +10 -3
- package/src/tools/network.js +12 -1
- package/src/tools/process.js +17 -3
- package/src/utils/config.js +50 -14
- package/src/utils/date.js +19 -0
- package/src/utils/display.js +1 -1
- package/src/utils/ids.js +12 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -27,7 +27,7 @@ export class OrchestratorAgent {
|
|
|
27
27
|
// Orchestrator provider (30s timeout — lean dispatch/summarize calls)
|
|
28
28
|
const orchProviderKey = config.orchestrator.provider || 'anthropic';
|
|
29
29
|
const orchProviderDef = PROVIDERS[orchProviderKey];
|
|
30
|
-
const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey])
|
|
30
|
+
const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]);
|
|
31
31
|
this.orchestratorProvider = createProvider({
|
|
32
32
|
brain: {
|
|
33
33
|
provider: orchProviderKey,
|
|
@@ -327,6 +327,24 @@ export class OrchestratorAgent {
|
|
|
327
327
|
|
|
328
328
|
// Build working messages from compressed history
|
|
329
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
|
+
|
|
330
348
|
logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
|
|
331
349
|
|
|
332
350
|
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
|
|
@@ -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
|
-
|
|
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;
|
package/src/bot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import TelegramBot from 'node-telegram-bot-api';
|
|
2
2
|
import { createReadStream, readFileSync } from 'fs';
|
|
3
|
-
import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
|
|
3
|
+
import { isAllowedUser, getUnauthorizedMessage, alertAdmin } from './security/auth.js';
|
|
4
4
|
import { getLogger } from './utils/logger.js';
|
|
5
5
|
import { PROVIDERS } from './providers/models.js';
|
|
6
6
|
import {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { TTSService } from './services/tts.js';
|
|
16
16
|
import { STTService } from './services/stt.js';
|
|
17
17
|
import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
|
|
18
|
+
import { isQuietHours } from './utils/timeUtils.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Simulate a human-like typing delay based on response length.
|
|
@@ -43,6 +44,27 @@ async function simulateTypingDelay(bot, chatId, text) {
|
|
|
43
44
|
return new Promise((resolve) => setTimeout(resolve, finalDelay));
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Simulate a brief pause between consecutive message chunks.
|
|
49
|
+
* When a long reply is split into multiple Telegram messages, firing them
|
|
50
|
+
* all instantly feels robotic. This adds a short, natural delay with a
|
|
51
|
+
* typing indicator so multi-part replies feel more human.
|
|
52
|
+
*
|
|
53
|
+
* @param {TelegramBot} bot - Telegram bot instance
|
|
54
|
+
* @param {number} chatId - Chat to show typing in
|
|
55
|
+
* @param {string} nextChunk - The upcoming chunk (used to scale the pause)
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async function simulateInterChunkDelay(bot, chatId, nextChunk) {
|
|
59
|
+
// Shorter than the initial typing delay: 0.3s – 1.5s based on chunk length
|
|
60
|
+
const length = (nextChunk || '').length;
|
|
61
|
+
const base = Math.min(1500, Math.max(300, length * 8));
|
|
62
|
+
const jitter = base * (0.85 + Math.random() * 0.3);
|
|
63
|
+
|
|
64
|
+
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, Math.round(jitter)));
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
function splitMessage(text, maxLength = 4096) {
|
|
47
69
|
if (text.length <= maxLength) return [text];
|
|
48
70
|
|
|
@@ -67,6 +89,7 @@ function splitMessage(text, maxLength = 4096) {
|
|
|
67
89
|
* Tries Markdown first, falls back to plain text.
|
|
68
90
|
*/
|
|
69
91
|
function createOnUpdate(bot, chatId) {
|
|
92
|
+
const logger = getLogger();
|
|
70
93
|
return async (update, opts = {}) => {
|
|
71
94
|
if (opts.editMessageId) {
|
|
72
95
|
try {
|
|
@@ -76,15 +99,16 @@ function createOnUpdate(bot, chatId) {
|
|
|
76
99
|
parse_mode: 'Markdown',
|
|
77
100
|
});
|
|
78
101
|
return edited.message_id;
|
|
79
|
-
} catch {
|
|
102
|
+
} catch (mdErr) {
|
|
103
|
+
logger.debug(`[Bot] Markdown edit failed for chat ${chatId}, retrying plain: ${mdErr.message}`);
|
|
80
104
|
try {
|
|
81
105
|
const edited = await bot.editMessageText(update, {
|
|
82
106
|
chat_id: chatId,
|
|
83
107
|
message_id: opts.editMessageId,
|
|
84
108
|
});
|
|
85
109
|
return edited.message_id;
|
|
86
|
-
} catch {
|
|
87
|
-
|
|
110
|
+
} catch (plainErr) {
|
|
111
|
+
logger.debug(`[Bot] Plain-text edit also failed for chat ${chatId}, sending new message: ${plainErr.message}`);
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
114
|
}
|
|
@@ -94,7 +118,8 @@ function createOnUpdate(bot, chatId) {
|
|
|
94
118
|
try {
|
|
95
119
|
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
96
120
|
lastMsgId = sent.message_id;
|
|
97
|
-
} catch {
|
|
121
|
+
} catch (mdErr) {
|
|
122
|
+
logger.debug(`[Bot] Markdown send failed for chat ${chatId}, falling back to plain: ${mdErr.message}`);
|
|
98
123
|
const sent = await bot.sendMessage(chatId, part);
|
|
99
124
|
lastMsgId = sent.message_id;
|
|
100
125
|
}
|
|
@@ -142,6 +167,7 @@ function createSendReaction(bot) {
|
|
|
142
167
|
/**
|
|
143
168
|
* Simple per-chat queue to serialize agent processing.
|
|
144
169
|
* Each chat gets its own promise chain so messages are processed in order.
|
|
170
|
+
* Automatically cleans up finished queues to avoid unbounded Map growth.
|
|
145
171
|
*/
|
|
146
172
|
class ChatQueue {
|
|
147
173
|
constructor() {
|
|
@@ -149,9 +175,21 @@ class ChatQueue {
|
|
|
149
175
|
}
|
|
150
176
|
|
|
151
177
|
enqueue(chatId, fn) {
|
|
178
|
+
const logger = getLogger();
|
|
152
179
|
const key = String(chatId);
|
|
153
180
|
const prev = this.queues.get(key) || Promise.resolve();
|
|
154
|
-
const next = prev
|
|
181
|
+
const next = prev
|
|
182
|
+
.then(() => fn())
|
|
183
|
+
.catch((err) => {
|
|
184
|
+
logger.error(`[ChatQueue] Error processing message for chat ${key}: ${err.message}`);
|
|
185
|
+
})
|
|
186
|
+
.finally(() => {
|
|
187
|
+
// Clean up the queue entry once this is the last item in the chain,
|
|
188
|
+
// preventing the Map from growing unboundedly over long-running sessions.
|
|
189
|
+
if (this.queues.get(key) === next) {
|
|
190
|
+
this.queues.delete(key);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
155
193
|
this.queues.set(key, next);
|
|
156
194
|
return next;
|
|
157
195
|
}
|
|
@@ -252,6 +290,13 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
252
290
|
|
|
253
291
|
if (!isAllowedUser(query.from.id, config)) {
|
|
254
292
|
await bot.answerCallbackQuery(query.id, { text: 'Unauthorized' });
|
|
293
|
+
await alertAdmin(bot, {
|
|
294
|
+
userId: query.from.id,
|
|
295
|
+
username: query.from.username,
|
|
296
|
+
firstName: query.from.first_name,
|
|
297
|
+
text: `🔘 زر: ${query.data || 'unknown'}`,
|
|
298
|
+
type: 'callback',
|
|
299
|
+
});
|
|
255
300
|
return;
|
|
256
301
|
}
|
|
257
302
|
|
|
@@ -741,6 +786,13 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
741
786
|
if (msg.text || msg.document) {
|
|
742
787
|
logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
|
|
743
788
|
await bot.sendMessage(chatId, getUnauthorizedMessage());
|
|
789
|
+
await alertAdmin(bot, {
|
|
790
|
+
userId,
|
|
791
|
+
username: msg.from.username,
|
|
792
|
+
firstName: msg.from.first_name,
|
|
793
|
+
text: msg.text || (msg.document ? `📎 ملف: ${msg.document.file_name || 'unknown'}` : undefined),
|
|
794
|
+
type: 'رسالة',
|
|
795
|
+
});
|
|
744
796
|
}
|
|
745
797
|
return;
|
|
746
798
|
}
|
|
@@ -806,6 +858,37 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
806
858
|
}
|
|
807
859
|
}
|
|
808
860
|
|
|
861
|
+
// Handle photo messages — download, convert to base64, and pass to LLM for vision analysis
|
|
862
|
+
let imageAttachment = null;
|
|
863
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
864
|
+
logger.info(`[Bot] Photo message from ${username} (${userId}) in chat ${chatId}`);
|
|
865
|
+
try {
|
|
866
|
+
// Use highest resolution (last item in array)
|
|
867
|
+
const photo = msg.photo[msg.photo.length - 1];
|
|
868
|
+
const fileLink = await bot.getFileLink(photo.file_id);
|
|
869
|
+
const response = await fetch(fileLink);
|
|
870
|
+
if (!response.ok) throw new Error(`Failed to download photo: ${response.statusText}`);
|
|
871
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
872
|
+
const base64Data = buffer.toString('base64');
|
|
873
|
+
|
|
874
|
+
// Determine media type from URL extension, default to jpeg
|
|
875
|
+
const ext = fileLink.split('.').pop().split('?')[0].toLowerCase();
|
|
876
|
+
const extToMime = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' };
|
|
877
|
+
const mediaType = extToMime[ext] || 'image/jpeg';
|
|
878
|
+
|
|
879
|
+
imageAttachment = { type: 'base64', media_type: mediaType, data: base64Data };
|
|
880
|
+
// Use caption as text, or default prompt
|
|
881
|
+
if (!msg.text) {
|
|
882
|
+
msg.text = msg.caption || 'What do you see in this image? Describe it in detail.';
|
|
883
|
+
}
|
|
884
|
+
logger.info(`[Bot] Photo downloaded and encoded (${Math.round(base64Data.length / 1024)}KB base64, ${mediaType})`);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
logger.error(`[Bot] Photo processing failed: ${err.message}`);
|
|
887
|
+
await bot.sendMessage(chatId, 'Failed to process the image. Please try again.');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
809
892
|
if (!msg.text) return; // ignore non-text (and non-document) messages
|
|
810
893
|
|
|
811
894
|
let text = msg.text.trim();
|
|
@@ -1646,7 +1729,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1646
1729
|
const reply = await agent.processMessage(chatId, mergedText, {
|
|
1647
1730
|
id: userId,
|
|
1648
1731
|
username,
|
|
1649
|
-
}, onUpdate, sendPhoto, { sendReaction, messageId: msg.message_id });
|
|
1732
|
+
}, onUpdate, sendPhoto, { sendReaction, messageId: msg.message_id, imageAttachment });
|
|
1650
1733
|
|
|
1651
1734
|
clearInterval(typingInterval);
|
|
1652
1735
|
|
|
@@ -1655,17 +1738,21 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1655
1738
|
|
|
1656
1739
|
logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
|
|
1657
1740
|
const chunks = splitMessage(reply || 'Done.');
|
|
1658
|
-
for (
|
|
1741
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1742
|
+
// Brief pause between consecutive chunks so multi-part replies feel natural
|
|
1743
|
+
if (i > 0) await simulateInterChunkDelay(bot, chatId, chunks[i]);
|
|
1659
1744
|
try {
|
|
1660
|
-
await bot.sendMessage(chatId,
|
|
1745
|
+
await bot.sendMessage(chatId, chunks[i], { parse_mode: 'Markdown' });
|
|
1661
1746
|
} catch {
|
|
1662
1747
|
// Fallback to plain text if Markdown fails
|
|
1663
|
-
await bot.sendMessage(chatId,
|
|
1748
|
+
await bot.sendMessage(chatId, chunks[i]);
|
|
1664
1749
|
}
|
|
1665
1750
|
}
|
|
1666
1751
|
|
|
1667
|
-
// Send voice reply
|
|
1668
|
-
|
|
1752
|
+
// Send voice reply only when the user explicitly requests it
|
|
1753
|
+
const voiceKeywords = ['صوت', 'صوتك', 'صوتية', 'صوتي', 'voice', 'speak', 'hear you'];
|
|
1754
|
+
const wantsVoice = voiceKeywords.some((kw) => mergedText.toLowerCase().includes(kw));
|
|
1755
|
+
if (wantsVoice && ttsService.isAvailable() && reply && reply.length > 5) {
|
|
1669
1756
|
try {
|
|
1670
1757
|
const audioPath = await ttsService.synthesize(reply);
|
|
1671
1758
|
if (audioPath) {
|
|
@@ -1689,7 +1776,18 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1689
1776
|
const userId = reaction.user?.id;
|
|
1690
1777
|
const username = reaction.user?.username || reaction.user?.first_name || 'unknown';
|
|
1691
1778
|
|
|
1692
|
-
if (!userId || !isAllowedUser(userId, config))
|
|
1779
|
+
if (!userId || !isAllowedUser(userId, config)) {
|
|
1780
|
+
if (userId) {
|
|
1781
|
+
await alertAdmin(bot, {
|
|
1782
|
+
userId,
|
|
1783
|
+
username: reaction.user?.username,
|
|
1784
|
+
firstName: reaction.user?.first_name,
|
|
1785
|
+
text: `${(reaction.new_reaction || []).filter(r => r.type === 'emoji').map(r => r.emoji).join(' ') || 'reaction'}`,
|
|
1786
|
+
type: 'تفاعل',
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1693
1791
|
|
|
1694
1792
|
const newReactions = reaction.new_reaction || [];
|
|
1695
1793
|
const emojis = newReactions
|
|
@@ -1703,6 +1801,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1703
1801
|
const reactionText = `[User reacted with ${emojis.join(' ')} to your message]`;
|
|
1704
1802
|
|
|
1705
1803
|
chatQueue.enqueue(chatId, async () => {
|
|
1804
|
+
// Show typing indicator while processing the reaction
|
|
1805
|
+
const typingInterval = setInterval(() => {
|
|
1806
|
+
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
1807
|
+
}, 4000);
|
|
1808
|
+
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
1809
|
+
|
|
1706
1810
|
try {
|
|
1707
1811
|
const onUpdate = createOnUpdate(bot, chatId);
|
|
1708
1812
|
const sendReaction = createSendReaction(bot);
|
|
@@ -1712,17 +1816,24 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1712
1816
|
username,
|
|
1713
1817
|
}, onUpdate, null, { sendReaction, messageId: reaction.message_id });
|
|
1714
1818
|
|
|
1819
|
+
clearInterval(typingInterval);
|
|
1820
|
+
|
|
1715
1821
|
if (reply && reply.trim()) {
|
|
1822
|
+
// Simulate human-like typing delay before responding to the reaction
|
|
1823
|
+
await simulateTypingDelay(bot, chatId, reply);
|
|
1824
|
+
|
|
1716
1825
|
const chunks = splitMessage(reply);
|
|
1717
|
-
for (
|
|
1826
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1827
|
+
if (i > 0) await simulateInterChunkDelay(bot, chatId, chunks[i]);
|
|
1718
1828
|
try {
|
|
1719
|
-
await bot.sendMessage(chatId,
|
|
1829
|
+
await bot.sendMessage(chatId, chunks[i], { parse_mode: 'Markdown' });
|
|
1720
1830
|
} catch {
|
|
1721
|
-
await bot.sendMessage(chatId,
|
|
1831
|
+
await bot.sendMessage(chatId, chunks[i]);
|
|
1722
1832
|
}
|
|
1723
1833
|
}
|
|
1724
1834
|
}
|
|
1725
1835
|
} catch (err) {
|
|
1836
|
+
clearInterval(typingInterval);
|
|
1726
1837
|
logger.error(`[Bot] Error processing reaction in chat ${chatId}: ${err.message}`);
|
|
1727
1838
|
}
|
|
1728
1839
|
});
|
|
@@ -1749,10 +1860,6 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1749
1860
|
}, 5000);
|
|
1750
1861
|
|
|
1751
1862
|
// ── 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
1863
|
const armShareDelivery = (delivered) => {
|
|
1757
1864
|
// If we just delivered something, wait longer (1–4h) before next check
|
|
1758
1865
|
// If nothing was delivered, check again sooner (10–45min) in case new shares appear
|
|
@@ -1763,9 +1870,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1763
1870
|
logger.debug(`[Bot] Next share check in ${Math.round(delayMs / 60_000)}m`);
|
|
1764
1871
|
|
|
1765
1872
|
setTimeout(async () => {
|
|
1766
|
-
// Respect quiet hours
|
|
1767
|
-
|
|
1768
|
-
if (hour >= quietStart && hour < quietEnd) {
|
|
1873
|
+
// Respect quiet hours (env vars → YAML config → defaults 02:00–06:00)
|
|
1874
|
+
if (isQuietHours(config.life)) {
|
|
1769
1875
|
armShareDelivery(false);
|
|
1770
1876
|
return;
|
|
1771
1877
|
}
|
package/src/life/engine.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { isQuietHours } from '../utils/timeUtils.js';
|
|
5
6
|
|
|
6
7
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
7
8
|
const STATE_FILE = join(LIFE_DIR, 'state.json');
|
|
@@ -195,12 +196,8 @@ export class LifeEngine {
|
|
|
195
196
|
const logger = getLogger();
|
|
196
197
|
this._timerId = null;
|
|
197
198
|
|
|
198
|
-
// Check quiet hours
|
|
199
|
-
|
|
200
|
-
const quietStart = lifeConfig.quiet_hours?.start ?? 2;
|
|
201
|
-
const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
|
|
202
|
-
const currentHour = new Date().getHours();
|
|
203
|
-
if (currentHour >= quietStart && currentHour < quietEnd) {
|
|
199
|
+
// Check quiet hours (env vars → YAML config → defaults 02:00–06:00)
|
|
200
|
+
if (isQuietHours(this.config.life)) {
|
|
204
201
|
logger.debug('[LifeEngine] Quiet hours — skipping tick');
|
|
205
202
|
this._armNext();
|
|
206
203
|
return;
|
|
@@ -407,31 +404,9 @@ This is your private thought space — be genuine, be curious, be alive.`;
|
|
|
407
404
|
const response = await this._innerChat(prompt);
|
|
408
405
|
|
|
409
406
|
if (response) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
for (const line of ideaLines) {
|
|
413
|
-
this._addIdea(line.replace(/^IDEA:\s*/, '').trim());
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Extract shares
|
|
417
|
-
const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
|
|
418
|
-
for (const line of shareLines) {
|
|
419
|
-
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'think', 'medium');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Extract questions to ask users
|
|
423
|
-
const askLines = response.split('\n').filter(l => l.trim().startsWith('ASK:'));
|
|
424
|
-
for (const line of askLines) {
|
|
425
|
-
this.shareQueue.add(line.replace(/^ASK:\s*/, '').trim(), 'think', 'medium', null, ['question']);
|
|
426
|
-
}
|
|
407
|
+
const extracted = this._extractTaggedLines(response, ['IDEA', 'SHARE', 'ASK', 'IMPROVE']);
|
|
408
|
+
this._processResponseTags(extracted, 'think');
|
|
427
409
|
|
|
428
|
-
// Extract self-improvement proposals for evolution pipeline
|
|
429
|
-
const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
|
|
430
|
-
for (const line of improveLines) {
|
|
431
|
-
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Store as episodic memory
|
|
435
410
|
this.memoryManager.addEpisodic({
|
|
436
411
|
type: 'thought',
|
|
437
412
|
source: 'think',
|
|
@@ -440,7 +415,7 @@ This is your private thought space — be genuine, be curious, be alive.`;
|
|
|
440
415
|
importance: 3,
|
|
441
416
|
});
|
|
442
417
|
|
|
443
|
-
logger.info(`[LifeEngine] Think complete (${response.length} chars, ${
|
|
418
|
+
logger.info(`[LifeEngine] Think complete (${response.length} chars, ${extracted.IDEA.length} ideas, ${extracted.SHARE.length} shares, ${extracted.ASK.length} questions, ${extracted.IMPROVE.length} improvements)`);
|
|
444
419
|
}
|
|
445
420
|
}
|
|
446
421
|
|
|
@@ -477,25 +452,9 @@ If you learn a key fact or concept, prefix it with "LEARNED:" followed by "topic
|
|
|
477
452
|
const response = await this._dispatchWorker('research', prompt);
|
|
478
453
|
|
|
479
454
|
if (response) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for (const line of shareLines) {
|
|
483
|
-
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'browse', 'medium');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Extract learned facts
|
|
487
|
-
const learnedLines = response.split('\n').filter(l => l.trim().startsWith('LEARNED:'));
|
|
488
|
-
for (const line of learnedLines) {
|
|
489
|
-
const content = line.replace(/^LEARNED:\s*/, '').trim();
|
|
490
|
-
const colonIdx = content.indexOf(':');
|
|
491
|
-
if (colonIdx > 0) {
|
|
492
|
-
const topicKey = content.slice(0, colonIdx).trim();
|
|
493
|
-
const summary = content.slice(colonIdx + 1).trim();
|
|
494
|
-
this.memoryManager.addSemantic(topicKey, { summary });
|
|
495
|
-
}
|
|
496
|
-
}
|
|
455
|
+
const extracted = this._extractTaggedLines(response, ['SHARE', 'LEARNED']);
|
|
456
|
+
this._processResponseTags(extracted, 'browse');
|
|
497
457
|
|
|
498
|
-
// Store as episodic memory
|
|
499
458
|
this.memoryManager.addEpisodic({
|
|
500
459
|
type: 'discovery',
|
|
501
460
|
source: 'browse',
|
|
@@ -594,10 +553,10 @@ Respond with just your creation — no tool calls needed.`;
|
|
|
594
553
|
const response = await this._innerChat(prompt);
|
|
595
554
|
|
|
596
555
|
if (response) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for (const
|
|
600
|
-
this.shareQueue.add(
|
|
556
|
+
const extracted = this._extractTaggedLines(response, ['SHARE']);
|
|
557
|
+
// Creative shares get a 'creation' tag for richer attribution
|
|
558
|
+
for (const text of extracted.SHARE) {
|
|
559
|
+
this.shareQueue.add(text, 'create', 'medium', null, ['creation']);
|
|
601
560
|
}
|
|
602
561
|
|
|
603
562
|
this.memoryManager.addEpisodic({
|
|
@@ -1203,23 +1162,11 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1203
1162
|
const response = await this._innerChat(prompt);
|
|
1204
1163
|
|
|
1205
1164
|
if (response) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
for (const line of improveLines) {
|
|
1209
|
-
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Extract patterns as semantic memories
|
|
1213
|
-
const patternLines = response.split('\n').filter(l => l.trim().startsWith('PATTERN:'));
|
|
1214
|
-
for (const line of patternLines) {
|
|
1215
|
-
const content = line.replace(/^PATTERN:\s*/, '').trim();
|
|
1216
|
-
this.memoryManager.addSemantic('interaction_patterns', { summary: content });
|
|
1217
|
-
}
|
|
1165
|
+
const extracted = this._extractTaggedLines(response, ['IMPROVE', 'PATTERN']);
|
|
1166
|
+
this._processResponseTags(extracted, 'reflect');
|
|
1218
1167
|
|
|
1219
|
-
// Write a journal entry with the reflection
|
|
1220
1168
|
this.journalManager.writeEntry('Interaction Reflection', response);
|
|
1221
1169
|
|
|
1222
|
-
// Store as episodic memory
|
|
1223
1170
|
this.memoryManager.addEpisodic({
|
|
1224
1171
|
type: 'thought',
|
|
1225
1172
|
source: 'reflect',
|
|
@@ -1228,7 +1175,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1228
1175
|
importance: 5,
|
|
1229
1176
|
});
|
|
1230
1177
|
|
|
1231
|
-
logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${
|
|
1178
|
+
logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${extracted.IMPROVE.length} improvements, ${extracted.PATTERN.length} patterns)`);
|
|
1232
1179
|
}
|
|
1233
1180
|
}
|
|
1234
1181
|
|
|
@@ -1307,6 +1254,78 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1307
1254
|
|
|
1308
1255
|
// ── Utilities ──────────────────────────────────────────────────
|
|
1309
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Extract tagged lines from an LLM response.
|
|
1259
|
+
* Tags are prefixes like "SHARE:", "IDEA:", "IMPROVE:", etc. that the LLM
|
|
1260
|
+
* uses to signal structured intents within free-form text.
|
|
1261
|
+
*
|
|
1262
|
+
* @param {string} response - Raw LLM response text.
|
|
1263
|
+
* @param {string[]} tags - List of tag prefixes to extract (e.g. ['SHARE', 'IDEA']).
|
|
1264
|
+
* @returns {Record<string, string[]>} Map of tag → array of extracted values (prefix stripped, trimmed).
|
|
1265
|
+
*/
|
|
1266
|
+
_extractTaggedLines(response, tags) {
|
|
1267
|
+
const lines = response.split('\n');
|
|
1268
|
+
const result = {};
|
|
1269
|
+
for (const tag of tags) {
|
|
1270
|
+
result[tag] = [];
|
|
1271
|
+
}
|
|
1272
|
+
for (const line of lines) {
|
|
1273
|
+
const trimmed = line.trim();
|
|
1274
|
+
for (const tag of tags) {
|
|
1275
|
+
if (trimmed.startsWith(`${tag}:`)) {
|
|
1276
|
+
result[tag].push(trimmed.slice(tag.length + 1).trim());
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Process common tagged lines from an activity response, routing each tag
|
|
1286
|
+
* to the appropriate handler (share queue, ideas backlog, semantic memory).
|
|
1287
|
+
*
|
|
1288
|
+
* @param {Record<string, string[]>} extracted - Output from _extractTaggedLines.
|
|
1289
|
+
* @param {string} source - Activity source for share queue attribution (e.g. 'think', 'browse').
|
|
1290
|
+
*/
|
|
1291
|
+
_processResponseTags(extracted, source) {
|
|
1292
|
+
if (extracted.SHARE) {
|
|
1293
|
+
for (const text of extracted.SHARE) {
|
|
1294
|
+
this.shareQueue.add(text, source, 'medium');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (extracted.IDEA) {
|
|
1298
|
+
for (const text of extracted.IDEA) {
|
|
1299
|
+
this._addIdea(text);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (extracted.IMPROVE) {
|
|
1303
|
+
for (const text of extracted.IMPROVE) {
|
|
1304
|
+
this._addIdea(`[IMPROVE] ${text}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (extracted.ASK) {
|
|
1308
|
+
for (const text of extracted.ASK) {
|
|
1309
|
+
this.shareQueue.add(text, source, 'medium', null, ['question']);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (extracted.LEARNED) {
|
|
1313
|
+
for (const text of extracted.LEARNED) {
|
|
1314
|
+
const colonIdx = text.indexOf(':');
|
|
1315
|
+
if (colonIdx > 0) {
|
|
1316
|
+
const topicKey = text.slice(0, colonIdx).trim();
|
|
1317
|
+
const summary = text.slice(colonIdx + 1).trim();
|
|
1318
|
+
this.memoryManager.addSemantic(topicKey, { summary });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (extracted.PATTERN) {
|
|
1323
|
+
for (const text of extracted.PATTERN) {
|
|
1324
|
+
this.memoryManager.addSemantic('interaction_patterns', { summary: text });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1310
1329
|
_formatDuration(ms) {
|
|
1311
1330
|
const hours = Math.floor(ms / 3600_000);
|
|
1312
1331
|
const minutes = Math.floor((ms % 3600_000) / 60_000);
|
package/src/life/evolution.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { getStartOfDayMs } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const EVOLUTION_FILE = join(LIFE_DIR, 'evolution.json');
|
|
9
10
|
|
|
10
|
-
function genId(prefix = 'evo') {
|
|
11
|
-
return `${prefix}_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
const VALID_STATUSES = ['research', 'planned', 'coding', 'pr_open', 'merged', 'rejected', 'failed'];
|
|
15
12
|
const TERMINAL_STATUSES = ['merged', 'rejected', 'failed'];
|
|
16
13
|
|
|
@@ -231,9 +228,8 @@ export class EvolutionTracker {
|
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
getProposalsToday() {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
return this._data.proposals.filter(p => p.createdAt >= startOfDay.getTime());
|
|
231
|
+
const cutoff = getStartOfDayMs();
|
|
232
|
+
return this._data.proposals.filter(p => p.createdAt >= cutoff);
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
// ── Internal ──────────────────────────────────────────────────
|