kernelbot 1.0.30 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.env.example +0 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +56 -2
  4. package/config.example.yaml +31 -0
  5. package/package.json +1 -1
  6. package/src/agent.js +200 -32
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +402 -6
  12. package/src/claude-auth.js +0 -0
  13. package/src/coder.js +0 -0
  14. package/src/conversation.js +51 -5
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/life/codebase.js +388 -0
  19. package/src/life/engine.js +1317 -0
  20. package/src/life/evolution.js +244 -0
  21. package/src/life/improvements.js +81 -0
  22. package/src/life/journal.js +109 -0
  23. package/src/life/memory.js +283 -0
  24. package/src/life/share-queue.js +136 -0
  25. package/src/persona.js +0 -0
  26. package/src/prompts/orchestrator.js +62 -2
  27. package/src/prompts/persona.md +7 -0
  28. package/src/prompts/system.js +0 -0
  29. package/src/prompts/workers.js +10 -9
  30. package/src/providers/anthropic.js +0 -0
  31. package/src/providers/base.js +0 -0
  32. package/src/providers/index.js +0 -0
  33. package/src/providers/models.js +8 -1
  34. package/src/providers/openai-compat.js +0 -0
  35. package/src/security/audit.js +0 -0
  36. package/src/security/auth.js +0 -0
  37. package/src/security/confirm.js +0 -0
  38. package/src/self.js +0 -0
  39. package/src/services/stt.js +0 -0
  40. package/src/services/tts.js +0 -0
  41. package/src/skills/catalog.js +0 -0
  42. package/src/skills/custom.js +0 -0
  43. package/src/swarm/job-manager.js +0 -0
  44. package/src/swarm/job.js +11 -0
  45. package/src/swarm/worker-registry.js +0 -0
  46. package/src/tools/browser.js +0 -0
  47. package/src/tools/categories.js +0 -0
  48. package/src/tools/coding.js +1 -1
  49. package/src/tools/docker.js +0 -0
  50. package/src/tools/git.js +0 -0
  51. package/src/tools/github.js +0 -0
  52. package/src/tools/index.js +0 -0
  53. package/src/tools/jira.js +0 -0
  54. package/src/tools/monitor.js +0 -0
  55. package/src/tools/network.js +0 -0
  56. package/src/tools/orchestrator-tools.js +60 -3
  57. package/src/tools/os.js +0 -0
  58. package/src/tools/persona.js +0 -0
  59. package/src/tools/process.js +0 -0
  60. package/src/utils/config.js +0 -0
  61. package/src/utils/display.js +0 -0
  62. package/src/utils/logger.js +0 -0
  63. package/src/worker.js +27 -8
package/src/bot.js CHANGED
@@ -53,9 +53,16 @@ class ChatQueue {
53
53
  }
54
54
  }
55
55
 
56
- export function startBot(config, agent, conversationManager, jobManager, automationManager) {
56
+ export function startBot(config, agent, conversationManager, jobManager, automationManager, lifeDeps = {}) {
57
+ const { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge } = lifeDeps;
57
58
  const logger = getLogger();
58
- const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
59
+ const bot = new TelegramBot(config.telegram.bot_token, {
60
+ polling: {
61
+ params: {
62
+ allowed_updates: ['message', 'callback_query', 'message_reaction'],
63
+ },
64
+ },
65
+ });
59
66
  const chatQueue = new ChatQueue();
60
67
  const batchWindowMs = config.telegram.batch_window_ms || 3000;
61
68
 
@@ -87,6 +94,10 @@ export function startBot(config, agent, conversationManager, jobManager, automat
87
94
  { command: 'jobs', description: 'List running and recent jobs' },
88
95
  { command: 'cancel', description: 'Cancel running job(s)' },
89
96
  { command: 'auto', description: 'Manage recurring automations' },
97
+ { command: 'life', description: 'Inner life engine status and control' },
98
+ { command: 'journal', description: 'View today\'s journal or a past date' },
99
+ { command: 'memories', description: 'View recent memories or search' },
100
+ { command: 'evolution', description: 'Self-evolution status, history, and lessons' },
90
101
  { command: 'context', description: 'Show all models, auth, and context info' },
91
102
  { command: 'clean', description: 'Clear conversation and start fresh' },
92
103
  { command: 'history', description: 'Show message count in memory' },
@@ -125,7 +136,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
125
136
  });
126
137
  return edited.message_id;
127
138
  } catch {
128
- return opts.editMessageId;
139
+ // Edit failed — fall through to send new message
129
140
  }
130
141
  }
131
142
  }
@@ -1100,6 +1111,296 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1100
1111
  return;
1101
1112
  }
1102
1113
 
1114
+ // ── /life command ──────────────────────────────────────────────
1115
+ if (text === '/life' || text.startsWith('/life ')) {
1116
+ logger.info(`[Bot] /life command from ${username} (${userId}) in chat ${chatId}`);
1117
+ const args = text.slice('/life'.length).trim();
1118
+
1119
+ if (!lifeEngine) {
1120
+ await bot.sendMessage(chatId, 'Life engine is not available.');
1121
+ return;
1122
+ }
1123
+
1124
+ if (args === 'pause') {
1125
+ lifeEngine.pause();
1126
+ await bot.sendMessage(chatId, '⏸️ Inner life paused. Use `/life resume` to restart.', { parse_mode: 'Markdown' });
1127
+ return;
1128
+ }
1129
+ if (args === 'resume') {
1130
+ lifeEngine.resume();
1131
+ await bot.sendMessage(chatId, '▶️ Inner life resumed!');
1132
+ return;
1133
+ }
1134
+ if (args.startsWith('trigger')) {
1135
+ const activityType = args.split(/\s+/)[1] || null;
1136
+ const validTypes = ['think', 'browse', 'journal', 'create', 'self_code', 'code_review', 'reflect'];
1137
+ if (activityType && !validTypes.includes(activityType)) {
1138
+ await bot.sendMessage(chatId, `Unknown activity type. Available: ${validTypes.join(', ')}`);
1139
+ return;
1140
+ }
1141
+ await bot.sendMessage(chatId, `⚡ Triggering ${activityType || 'random'} activity...`);
1142
+ lifeEngine.triggerNow(activityType).catch(err => {
1143
+ logger.error(`[Bot] Life trigger failed: ${err.message}`);
1144
+ });
1145
+ return;
1146
+ }
1147
+ if (args === 'review') {
1148
+ if (evolutionTracker) {
1149
+ const active = evolutionTracker.getActiveProposal();
1150
+ const openPRs = evolutionTracker.getPRsToCheck();
1151
+ const lines = ['*Evolution Status*', ''];
1152
+ if (active) {
1153
+ lines.push(`Active: \`${active.id}\` — ${active.status}`);
1154
+ lines.push(` ${(active.triggerContext || '').slice(0, 150)}`);
1155
+ } else {
1156
+ lines.push('_No active proposals._');
1157
+ }
1158
+ if (openPRs.length > 0) {
1159
+ lines.push('', '*Open PRs:*');
1160
+ for (const p of openPRs) {
1161
+ lines.push(` • PR #${p.prNumber}: ${p.prUrl || 'no URL'}`);
1162
+ }
1163
+ }
1164
+ lines.push('', '_Use `/evolution` for full evolution status._');
1165
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1166
+ } else {
1167
+ await bot.sendMessage(chatId, 'Evolution system not available. Use `/evolution` for details.', { parse_mode: 'Markdown' });
1168
+ }
1169
+ return;
1170
+ }
1171
+
1172
+ // Default: show status
1173
+ const status = lifeEngine.getStatus();
1174
+ const lines = [
1175
+ '🌱 *Inner Life*',
1176
+ '',
1177
+ `*Status:* ${status.paused ? '⏸️ Paused' : status.status === 'active' ? '🟢 Active' : '⚪ Idle'}`,
1178
+ `*Total activities:* ${status.totalActivities}`,
1179
+ `*Last activity:* ${status.lastActivity || 'none'} (${status.lastActivityAgo})`,
1180
+ `*Last wake-up:* ${status.lastWakeUpAgo}`,
1181
+ '',
1182
+ '*Activity counts:*',
1183
+ ` 💭 Think: ${status.activityCounts.think || 0}`,
1184
+ ` 🌐 Browse: ${status.activityCounts.browse || 0}`,
1185
+ ` 📓 Journal: ${status.activityCounts.journal || 0}`,
1186
+ ` 🎨 Create: ${status.activityCounts.create || 0}`,
1187
+ ` 🔧 Self-code: ${status.activityCounts.self_code || 0}`,
1188
+ ` 🔍 Code review: ${status.activityCounts.code_review || 0}`,
1189
+ ` 🪞 Reflect: ${status.activityCounts.reflect || 0}`,
1190
+ '',
1191
+ '_Commands:_',
1192
+ '`/life pause` — Pause activities',
1193
+ '`/life resume` — Resume activities',
1194
+ '`/life trigger [think|browse|journal|create|self_code|code_review|reflect]` — Trigger now',
1195
+ ];
1196
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1197
+ return;
1198
+ }
1199
+
1200
+ // ── /journal command ─────────────────────────────────────────
1201
+ if (text === '/journal' || text.startsWith('/journal ')) {
1202
+ logger.info(`[Bot] /journal command from ${username} (${userId}) in chat ${chatId}`);
1203
+
1204
+ if (!journalManager) {
1205
+ await bot.sendMessage(chatId, 'Journal system is not available.');
1206
+ return;
1207
+ }
1208
+
1209
+ const args = text.slice('/journal'.length).trim();
1210
+
1211
+ if (args && /^\d{4}-\d{2}-\d{2}$/.test(args)) {
1212
+ const entry = journalManager.getEntry(args);
1213
+ if (!entry) {
1214
+ await bot.sendMessage(chatId, `No journal entry for ${args}.`);
1215
+ return;
1216
+ }
1217
+ const chunks = splitMessage(entry);
1218
+ for (const chunk of chunks) {
1219
+ try { await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' }); }
1220
+ catch { await bot.sendMessage(chatId, chunk); }
1221
+ }
1222
+ return;
1223
+ }
1224
+
1225
+ if (args === 'list') {
1226
+ const dates = journalManager.list(15);
1227
+ if (dates.length === 0) {
1228
+ await bot.sendMessage(chatId, 'No journal entries yet.');
1229
+ return;
1230
+ }
1231
+ const lines = ['📓 *Journal Entries*', '', ...dates.map(d => ` • \`${d}\``)];
1232
+ lines.push('', '_Use `/journal YYYY-MM-DD` to read an entry._');
1233
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1234
+ return;
1235
+ }
1236
+
1237
+ // Default: show today's journal
1238
+ const today = journalManager.getToday();
1239
+ if (!today) {
1240
+ await bot.sendMessage(chatId, '📓 No journal entries today yet.\n\n_Use `/journal list` to see past entries._', { parse_mode: 'Markdown' });
1241
+ return;
1242
+ }
1243
+ const chunks = splitMessage(today);
1244
+ for (const chunk of chunks) {
1245
+ try { await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' }); }
1246
+ catch { await bot.sendMessage(chatId, chunk); }
1247
+ }
1248
+ return;
1249
+ }
1250
+
1251
+ // ── /memories command ────────────────────────────────────────
1252
+ if (text === '/memories' || text.startsWith('/memories ')) {
1253
+ logger.info(`[Bot] /memories command from ${username} (${userId}) in chat ${chatId}`);
1254
+
1255
+ if (!memoryManager) {
1256
+ await bot.sendMessage(chatId, 'Memory system is not available.');
1257
+ return;
1258
+ }
1259
+
1260
+ const args = text.slice('/memories'.length).trim();
1261
+
1262
+ if (args.startsWith('about ')) {
1263
+ const query = args.slice('about '.length).trim();
1264
+ const results = memoryManager.searchEpisodic(query, 10);
1265
+ if (results.length === 0) {
1266
+ await bot.sendMessage(chatId, `No memories matching "${query}".`);
1267
+ return;
1268
+ }
1269
+ const lines = [`🧠 *Memories about "${query}"*`, ''];
1270
+ for (const m of results) {
1271
+ const date = new Date(m.timestamp).toLocaleDateString();
1272
+ lines.push(`• ${m.summary} _(${date}, importance: ${m.importance})_`);
1273
+ }
1274
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1275
+ return;
1276
+ }
1277
+
1278
+ // Default: show last 10 memories
1279
+ const recent = memoryManager.getRecentEpisodic(168, 10); // last 7 days
1280
+ if (recent.length === 0) {
1281
+ await bot.sendMessage(chatId, '🧠 No memories yet.');
1282
+ return;
1283
+ }
1284
+ const lines = ['🧠 *Recent Memories*', ''];
1285
+ for (const m of recent) {
1286
+ const ago = Math.round((Date.now() - m.timestamp) / 60000);
1287
+ const timeLabel = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
1288
+ const icon = { interaction: '💬', discovery: '🔍', thought: '💭', creation: '🎨' }[m.type] || '•';
1289
+ lines.push(`${icon} ${m.summary} _(${timeLabel})_`);
1290
+ }
1291
+ lines.push('', '_Use `/memories about <topic>` to search._');
1292
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1293
+ return;
1294
+ }
1295
+
1296
+ // ── /evolution command ──────────────────────────────────────────
1297
+ if (text === '/evolution' || text.startsWith('/evolution ')) {
1298
+ logger.info(`[Bot] /evolution command from ${username} (${userId}) in chat ${chatId}`);
1299
+ const args = text.slice('/evolution'.length).trim();
1300
+
1301
+ if (!evolutionTracker) {
1302
+ await bot.sendMessage(chatId, 'Evolution system is not available.');
1303
+ return;
1304
+ }
1305
+
1306
+ if (args === 'history') {
1307
+ const proposals = evolutionTracker.getRecentProposals(10);
1308
+ if (proposals.length === 0) {
1309
+ await bot.sendMessage(chatId, 'No evolution proposals yet.');
1310
+ return;
1311
+ }
1312
+ const lines = ['*Evolution History*', ''];
1313
+ for (const p of proposals.reverse()) {
1314
+ const statusIcon = { research: '🔬', planned: '📋', coding: '💻', pr_open: '🔄', merged: '✅', rejected: '❌', failed: '💥' }[p.status] || '•';
1315
+ const age = Math.round((Date.now() - p.createdAt) / 3600_000);
1316
+ const ageLabel = age < 24 ? `${age}h ago` : `${Math.round(age / 24)}d ago`;
1317
+ lines.push(`${statusIcon} \`${p.id}\` — ${p.status} (${ageLabel})`);
1318
+ lines.push(` ${(p.triggerContext || '').slice(0, 100)}`);
1319
+ if (p.prUrl) lines.push(` PR: ${p.prUrl}`);
1320
+ lines.push('');
1321
+ }
1322
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1323
+ return;
1324
+ }
1325
+
1326
+ if (args === 'lessons') {
1327
+ const lessons = evolutionTracker.getRecentLessons(15);
1328
+ if (lessons.length === 0) {
1329
+ await bot.sendMessage(chatId, 'No evolution lessons learned yet.');
1330
+ return;
1331
+ }
1332
+ const lines = ['*Evolution Lessons*', ''];
1333
+ for (const l of lessons.reverse()) {
1334
+ lines.push(`• [${l.category}] ${l.lesson}`);
1335
+ if (l.fromProposal) lines.push(` _from ${l.fromProposal}_`);
1336
+ lines.push('');
1337
+ }
1338
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1339
+ return;
1340
+ }
1341
+
1342
+ if (args === 'trigger') {
1343
+ if (!lifeEngine) {
1344
+ await bot.sendMessage(chatId, 'Life engine is not available.');
1345
+ return;
1346
+ }
1347
+ await bot.sendMessage(chatId, '⚡ Triggering evolution cycle...');
1348
+ lifeEngine.triggerNow('self_code').catch(err => {
1349
+ logger.error(`[Bot] Evolution trigger failed: ${err.message}`);
1350
+ });
1351
+ return;
1352
+ }
1353
+
1354
+ if (args === 'scan') {
1355
+ if (!codebaseKnowledge) {
1356
+ await bot.sendMessage(chatId, 'Codebase knowledge is not available.');
1357
+ return;
1358
+ }
1359
+ await bot.sendMessage(chatId, '🔍 Scanning codebase...');
1360
+ codebaseKnowledge.scanChanged().then(count => {
1361
+ bot.sendMessage(chatId, `✅ Scanned ${count} changed files.`).catch(() => {});
1362
+ }).catch(err => {
1363
+ bot.sendMessage(chatId, `Failed: ${err.message}`).catch(() => {});
1364
+ });
1365
+ return;
1366
+ }
1367
+
1368
+ // Default: show status
1369
+ const active = evolutionTracker.getActiveProposal();
1370
+ const stats = evolutionTracker.getStats();
1371
+ const openPRs = evolutionTracker.getPRsToCheck();
1372
+
1373
+ const lines = [
1374
+ '🧬 *Self-Evolution*',
1375
+ '',
1376
+ `*Stats:* ${stats.totalProposals} total | ${stats.merged} merged | ${stats.rejected} rejected | ${stats.failed} failed`,
1377
+ `*Success rate:* ${stats.successRate}%`,
1378
+ `*Open PRs:* ${openPRs.length}`,
1379
+ ];
1380
+
1381
+ if (active) {
1382
+ const statusIcon = { research: '🔬', planned: '📋', coding: '💻', pr_open: '🔄' }[active.status] || '•';
1383
+ lines.push('');
1384
+ lines.push(`*Active proposal:* ${statusIcon} \`${active.id}\` — ${active.status}`);
1385
+ lines.push(` ${(active.triggerContext || '').slice(0, 120)}`);
1386
+ if (active.prUrl) lines.push(` PR: ${active.prUrl}`);
1387
+ } else {
1388
+ lines.push('', '_No active proposal_');
1389
+ }
1390
+
1391
+ lines.push(
1392
+ '',
1393
+ '_Commands:_',
1394
+ '`/evolution history` — Recent proposals',
1395
+ '`/evolution lessons` — Learned lessons',
1396
+ '`/evolution trigger` — Trigger evolution now',
1397
+ '`/evolution scan` — Scan codebase',
1398
+ );
1399
+
1400
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1401
+ return;
1402
+ }
1403
+
1103
1404
  if (text === '/help') {
1104
1405
  const activeSkill = agent.getActiveSkill(chatId);
1105
1406
  const skillLine = activeSkill
@@ -1117,6 +1418,10 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1117
1418
  '/jobs — List running and recent jobs',
1118
1419
  '/cancel — Cancel running job(s)',
1119
1420
  '/auto — Manage recurring automations',
1421
+ '/life — Inner life engine status & control',
1422
+ '/journal — View today\'s journal or a past date',
1423
+ '/memories — View recent memories or search',
1424
+ '/evolution — Self-evolution status, history, lessons',
1120
1425
  '/context — Show all models, auth, and context info',
1121
1426
  '/clean — Clear conversation and start fresh',
1122
1427
  '/history — Show message count in memory',
@@ -1293,12 +1598,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1293
1598
  });
1294
1599
  return edited.message_id;
1295
1600
  } catch {
1296
- return opts.editMessageId;
1601
+ // Edit failed — fall through to send new message
1297
1602
  }
1298
1603
  }
1299
1604
  }
1300
1605
 
1301
- // Send new message(s)
1606
+ // Send new message(s) — also reached when edit fails
1302
1607
  const parts = splitMessage(update);
1303
1608
  let lastMsgId = null;
1304
1609
  for (const part of parts) {
@@ -1331,11 +1636,18 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1331
1636
  }
1332
1637
  };
1333
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
+ };
1645
+
1334
1646
  logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
1335
1647
  const reply = await agent.processMessage(chatId, mergedText, {
1336
1648
  id: userId,
1337
1649
  username,
1338
- }, onUpdate, sendPhoto);
1650
+ }, onUpdate, sendPhoto, { sendReaction, messageId: msg.message_id });
1339
1651
 
1340
1652
  clearInterval(typingInterval);
1341
1653
 
@@ -1369,6 +1681,90 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1369
1681
  });
1370
1682
  });
1371
1683
 
1684
+ // Handle message reactions (love, like, etc.)
1685
+ bot.on('message_reaction', async (reaction) => {
1686
+ const chatId = reaction.chat.id;
1687
+ const userId = reaction.user?.id;
1688
+ const username = reaction.user?.username || reaction.user?.first_name || 'unknown';
1689
+
1690
+ if (!userId || !isAllowedUser(userId, config)) return;
1691
+
1692
+ const newReactions = reaction.new_reaction || [];
1693
+ const emojis = newReactions
1694
+ .filter(r => r.type === 'emoji')
1695
+ .map(r => r.emoji);
1696
+
1697
+ if (emojis.length === 0) return;
1698
+
1699
+ logger.info(`[Bot] Reaction from ${username} (${userId}) in chat ${chatId}: ${emojis.join(' ')}`);
1700
+
1701
+ const reactionText = `[User reacted with ${emojis.join(' ')} to your message]`;
1702
+
1703
+ chatQueue.enqueue(chatId, async () => {
1704
+ 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
+ };
1746
+
1747
+ const reply = await agent.processMessage(chatId, reactionText, {
1748
+ id: userId,
1749
+ username,
1750
+ }, onUpdate, null, { sendReaction, messageId: reaction.message_id });
1751
+
1752
+ if (reply && reply.trim()) {
1753
+ const chunks = splitMessage(reply);
1754
+ for (const chunk of chunks) {
1755
+ try {
1756
+ await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
1757
+ } catch {
1758
+ await bot.sendMessage(chatId, chunk);
1759
+ }
1760
+ }
1761
+ }
1762
+ } catch (err) {
1763
+ logger.error(`[Bot] Error processing reaction in chat ${chatId}: ${err.message}`);
1764
+ }
1765
+ });
1766
+ });
1767
+
1372
1768
  bot.on('polling_error', (err) => {
1373
1769
  logger.error(`Telegram polling error: ${err.message}`);
1374
1770
  });
File without changes
package/src/coder.js CHANGED
File without changes
@@ -68,6 +68,48 @@ export class ConversationManager {
68
68
  return this.conversations.get(key);
69
69
  }
70
70
 
71
+ /**
72
+ * Get the timestamp of the most recent message in a chat.
73
+ * Used by agent.js for time-gap detection before the current message is added.
74
+ */
75
+ getLastMessageTimestamp(chatId) {
76
+ const history = this.getHistory(chatId);
77
+ if (history.length === 0) return null;
78
+ return history[history.length - 1].timestamp || null;
79
+ }
80
+
81
+ /**
82
+ * Format a timestamp as a relative time marker.
83
+ * Returns null for missing timestamps (backward compat with old messages).
84
+ */
85
+ _formatRelativeTime(ts) {
86
+ if (!ts) return null;
87
+ const diff = Date.now() - ts;
88
+ const seconds = Math.floor(diff / 1000);
89
+ if (seconds < 60) return '[just now]';
90
+ const minutes = Math.floor(seconds / 60);
91
+ if (minutes < 60) return `[${minutes}m ago]`;
92
+ const hours = Math.floor(minutes / 60);
93
+ if (hours < 24) return `[${hours}h ago]`;
94
+ const days = Math.floor(hours / 24);
95
+ return `[${days}d ago]`;
96
+ }
97
+
98
+ /**
99
+ * Return a shallow copy of a message with a time marker prepended to string content.
100
+ * Skips tool_result arrays and messages without timestamps.
101
+ */
102
+ _annotateWithTime(msg) {
103
+ const marker = this._formatRelativeTime(msg.timestamp);
104
+ if (!marker || typeof msg.content !== 'string') return msg;
105
+ return { ...msg, content: `${marker} ${msg.content}` };
106
+ }
107
+
108
+ /** Strip internal metadata fields, returning only API-safe {role, content}. */
109
+ _sanitize(msg) {
110
+ return { role: msg.role, content: msg.content };
111
+ }
112
+
71
113
  /**
72
114
  * Get history with older messages compressed into a summary.
73
115
  * Keeps the last `recentWindow` messages verbatim and summarizes older ones.
@@ -76,18 +118,19 @@ export class ConversationManager {
76
118
  const history = this.getHistory(chatId);
77
119
 
78
120
  if (history.length <= this.recentWindow) {
79
- return [...history];
121
+ return history.map(m => this._sanitize(this._annotateWithTime(m)));
80
122
  }
81
123
 
82
124
  const olderMessages = history.slice(0, history.length - this.recentWindow);
83
125
  const recentMessages = history.slice(history.length - this.recentWindow);
84
126
 
85
- // Compress older messages into a single summary
127
+ // Compress older messages into a single summary (include time markers when available)
86
128
  const summaryLines = olderMessages.map((msg) => {
129
+ const timeTag = this._formatRelativeTime(msg.timestamp);
87
130
  const content = typeof msg.content === 'string'
88
131
  ? msg.content.slice(0, 200)
89
132
  : JSON.stringify(msg.content).slice(0, 200);
90
- return `[${msg.role}]: ${content}`;
133
+ return `[${msg.role}]${timeTag ? ` ${timeTag}` : ''}: ${content}`;
91
134
  });
92
135
 
93
136
  const summaryMessage = {
@@ -95,8 +138,11 @@ export class ConversationManager {
95
138
  content: `[CONVERSATION SUMMARY - ${olderMessages.length} earlier messages]\n${summaryLines.join('\n')}`,
96
139
  };
97
140
 
141
+ // Annotate recent messages with time markers and strip metadata
142
+ const annotatedRecent = recentMessages.map(m => this._sanitize(this._annotateWithTime(m)));
143
+
98
144
  // Ensure result starts with user role
99
- const result = [summaryMessage, ...recentMessages];
145
+ const result = [summaryMessage, ...annotatedRecent];
100
146
 
101
147
  // If the first real message after summary is assistant, that's fine since
102
148
  // our summary is role:user. But ensure recent starts correctly.
@@ -105,7 +151,7 @@ export class ConversationManager {
105
151
 
106
152
  addMessage(chatId, role, content) {
107
153
  const history = this.getHistory(chatId);
108
- history.push({ role, content });
154
+ history.push({ role, content, timestamp: Date.now() });
109
155
 
110
156
  // Trim to max history
111
157
  while (history.length > this.maxHistory) {
File without changes
File without changes
File without changes