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.
- package/.env.example +11 -0
- package/README.md +76 -341
- package/bin/kernel.js +134 -15
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +2 -1
- package/src/agent.js +166 -19
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +295 -163
- package/src/conversation.js +70 -3
- 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/prompts/persona.md +27 -0
- package/src/providers/base.js +51 -8
- package/src/providers/google-genai.js +198 -0
- package/src/providers/index.js +6 -1
- package/src/providers/models.js +6 -2
- package/src/providers/openai-compat.js +25 -11
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +37 -15
- 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 +13 -15
- package/src/tools/network.js +22 -18
- package/src/tools/os.js +37 -2
- package/src/tools/process.js +21 -14
- package/src/utils/config.js +66 -0
- package/src/utils/date.js +19 -0
- package/src/utils/display.js +1 -1
- package/src/utils/ids.js +12 -0
- package/src/utils/shell.js +31 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
- package/src/utils/truncate.js +42 -0
- 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.')}
|
|
57
|
-
console.log(` ${chalk.cyan('7.')} Manage
|
|
58
|
-
console.log(` ${chalk.cyan('8.')}
|
|
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
|
|
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(
|
|
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
|
|
548
|
+
await changeOrchestratorModel(config, rl);
|
|
433
549
|
break;
|
|
434
550
|
case '7':
|
|
435
|
-
await
|
|
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:
|
package/config.example.yaml
CHANGED
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|