kernelbot 1.0.32 → 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/README.md +281 -276
- package/bin/kernel.js +51 -12
- package/package.json +2 -1
- package/src/agent.js +200 -33
- package/src/bot.js +228 -105
- package/src/conversation.js +121 -8
- package/src/prompts/orchestrator.js +44 -2
- package/src/prompts/persona.md +34 -0
- package/src/providers/base.js +16 -5
- 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/swarm/job.js +11 -0
- package/src/tools/docker.js +6 -13
- package/src/tools/monitor.js +5 -14
- package/src/tools/network.js +10 -17
- package/src/tools/orchestrator-tools.js +42 -0
- package/src/tools/os.js +37 -2
- package/src/tools/process.js +7 -14
- package/src/utils/config.js +59 -0
- package/src/utils/shell.js +31 -0
- package/src/utils/truncate.js +42 -0
- package/src/worker.js +18 -17
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
|
|
|
@@ -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
|
|
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(
|
|
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
|
|
468
|
+
await changeOrchestratorModel(config, rl);
|
|
433
469
|
break;
|
|
434
470
|
case '7':
|
|
435
|
-
await
|
|
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.
|
|
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 }) {
|
|
@@ -49,7 +47,7 @@ export class OrchestratorAgent {
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
/** Build the orchestrator system prompt. */
|
|
52
|
-
_getSystemPrompt(chatId, user) {
|
|
50
|
+
_getSystemPrompt(chatId, user, temporalContext = null) {
|
|
53
51
|
const logger = getLogger();
|
|
54
52
|
const skillId = this.conversationManager.getSkill(chatId);
|
|
55
53
|
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
@@ -76,8 +74,8 @@ export class OrchestratorAgent {
|
|
|
76
74
|
sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'}`);
|
|
80
|
-
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock);
|
|
77
|
+
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'}`);
|
|
78
|
+
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
setSkill(chatId, skillId) {
|
|
@@ -282,32 +280,18 @@ 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
|
-
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
|
|
288
|
+
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
|
|
305
289
|
const logger = getLogger();
|
|
306
290
|
|
|
307
291
|
logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
|
|
308
292
|
|
|
309
293
|
// Store callbacks so workers can use them later
|
|
310
|
-
this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
|
|
294
|
+
this._chatCallbacks.set(chatId, { onUpdate, sendPhoto, sendReaction: opts.sendReaction, lastUserMessageId: opts.messageId });
|
|
311
295
|
|
|
312
296
|
// Handle pending responses (confirmation or credential)
|
|
313
297
|
const pending = this._pending.get(chatId);
|
|
@@ -322,6 +306,22 @@ export class OrchestratorAgent {
|
|
|
322
306
|
|
|
323
307
|
const { max_tool_depth } = this.config.orchestrator;
|
|
324
308
|
|
|
309
|
+
// Detect time gap before adding the new message
|
|
310
|
+
let temporalContext = null;
|
|
311
|
+
const lastTs = this.conversationManager.getLastMessageTimestamp(chatId);
|
|
312
|
+
if (lastTs) {
|
|
313
|
+
const gapMs = Date.now() - lastTs;
|
|
314
|
+
const gapMinutes = Math.floor(gapMs / 60_000);
|
|
315
|
+
if (gapMinutes >= 30) {
|
|
316
|
+
const gapHours = Math.floor(gapMinutes / 60);
|
|
317
|
+
const gapText = gapHours >= 1
|
|
318
|
+
? `${gapHours} hour(s)`
|
|
319
|
+
: `${gapMinutes} minute(s)`;
|
|
320
|
+
temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic.]`;
|
|
321
|
+
logger.info(`Time gap detected for chat ${chatId}: ${gapText}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
325
|
// Add user message to persistent history
|
|
326
326
|
this.conversationManager.addMessage(chatId, 'user', userMessage);
|
|
327
327
|
|
|
@@ -329,7 +329,7 @@ export class OrchestratorAgent {
|
|
|
329
329
|
const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
|
|
330
330
|
logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
|
|
331
331
|
|
|
332
|
-
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
332
|
+
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
|
|
333
333
|
|
|
334
334
|
logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
|
|
335
335
|
|
|
@@ -738,6 +738,7 @@ export class OrchestratorAgent {
|
|
|
738
738
|
callbacks: {
|
|
739
739
|
onProgress: (text) => addActivity(text),
|
|
740
740
|
onHeartbeat: (text) => job.addProgress(text),
|
|
741
|
+
onStats: (stats) => job.updateStats(stats),
|
|
741
742
|
onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
|
|
742
743
|
onComplete: (result, parsedResult) => {
|
|
743
744
|
logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
|
|
@@ -840,8 +841,16 @@ export class OrchestratorAgent {
|
|
|
840
841
|
for (const job of running) {
|
|
841
842
|
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
842
843
|
const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
|
|
843
|
-
const
|
|
844
|
-
|
|
844
|
+
const stats = `${job.llmCalls} LLM calls, ${job.toolCalls} tools`;
|
|
845
|
+
const recentActivity = job.progress.slice(-5).join(' → ');
|
|
846
|
+
let line = `- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s [${stats}]`;
|
|
847
|
+
if (job.lastThinking) {
|
|
848
|
+
line += `\n Thinking: "${job.lastThinking.slice(0, 150)}"`;
|
|
849
|
+
}
|
|
850
|
+
if (recentActivity) {
|
|
851
|
+
line += `\n Recent: ${recentActivity}`;
|
|
852
|
+
}
|
|
853
|
+
lines.push(line);
|
|
845
854
|
}
|
|
846
855
|
|
|
847
856
|
// Queued/waiting jobs
|
|
@@ -881,20 +890,28 @@ export class OrchestratorAgent {
|
|
|
881
890
|
return `[Active Workers]\n${lines.join('\n')}`;
|
|
882
891
|
}
|
|
883
892
|
|
|
884
|
-
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
893
|
+
async _runLoop(chatId, messages, user, startDepth, maxDepth, temporalContext = null) {
|
|
885
894
|
const logger = getLogger();
|
|
886
895
|
|
|
887
896
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
888
897
|
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
889
898
|
|
|
890
|
-
// Inject
|
|
899
|
+
// Inject transient context messages (not stored in conversation history)
|
|
900
|
+
let workingMessages = [...messages];
|
|
901
|
+
|
|
902
|
+
// On first iteration, inject temporal context if present
|
|
903
|
+
if (depth === 0 && temporalContext) {
|
|
904
|
+
workingMessages = [{ role: 'user', content: `[Temporal Context]\n${temporalContext}` }, ...workingMessages];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Inject worker activity digest
|
|
891
908
|
const digest = this._buildWorkerDigest(chatId);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
909
|
+
if (digest) {
|
|
910
|
+
workingMessages = [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...workingMessages];
|
|
911
|
+
}
|
|
895
912
|
|
|
896
913
|
const response = await this.orchestratorProvider.chat({
|
|
897
|
-
system: this._getSystemPrompt(chatId, user),
|
|
914
|
+
system: this._getSystemPrompt(chatId, user, temporalContext),
|
|
898
915
|
messages: workingMessages,
|
|
899
916
|
tools: orchestratorToolDefinitions,
|
|
900
917
|
});
|
|
@@ -923,6 +940,7 @@ export class OrchestratorAgent {
|
|
|
923
940
|
logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
924
941
|
await this._sendUpdate(chatId, `⚡ ${summary}`);
|
|
925
942
|
|
|
943
|
+
const chatCallbacks = this._chatCallbacks.get(chatId) || {};
|
|
926
944
|
const result = await executeOrchestratorTool(block.name, block.input, {
|
|
927
945
|
chatId,
|
|
928
946
|
jobManager: this.jobManager,
|
|
@@ -930,6 +948,8 @@ export class OrchestratorAgent {
|
|
|
930
948
|
spawnWorker: (job) => this._spawnWorker(job),
|
|
931
949
|
automationManager: this.automationManager,
|
|
932
950
|
user,
|
|
951
|
+
sendReaction: chatCallbacks.sendReaction || null,
|
|
952
|
+
lastUserMessageId: chatCallbacks.lastUserMessageId || null,
|
|
933
953
|
});
|
|
934
954
|
|
|
935
955
|
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
@@ -978,11 +998,158 @@ export class OrchestratorAgent {
|
|
|
978
998
|
return `Updating automation ${input.automation_id}`;
|
|
979
999
|
case 'delete_automation':
|
|
980
1000
|
return `Deleting automation ${input.automation_id}`;
|
|
1001
|
+
case 'send_reaction':
|
|
1002
|
+
return `Reacting with ${input.emoji}`;
|
|
981
1003
|
default:
|
|
982
1004
|
return name;
|
|
983
1005
|
}
|
|
984
1006
|
}
|
|
985
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
|
+
|
|
986
1153
|
/** Background persona extraction. */
|
|
987
1154
|
async _extractPersonaBackground(userMessage, reply, user) {
|
|
988
1155
|
const logger = getLogger();
|