ai-control-center 1.15.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * AI Control Center Telegram Bot — remote pipeline control + notifications.
3
+ *
4
+ * v1.6.67 — Resilient polling architecture inspired by OpenClaw's monitorTelegramProvider.
5
+ *
6
+ * Key improvements over v1.6.66:
7
+ * 1. Fresh Bot instance per restart cycle — no stale HTTP/polling state
8
+ * 2. Exponential backoff with jitter (2s → 30s cap) — prevents API hammering
9
+ * 3. Error classification — recoverable network errors vs fatal bugs
10
+ * 4. Standalone API for notifications — survives bot restarts
11
+ *
12
+ * Setup:
13
+ * 1. Talk to @BotFather on Telegram → /newbot → get token
14
+ * 2. Set env vars (prefix with your project prefix from aicc.config.js):
15
+ * export {PREFIX}_TELEGRAM_TOKEN=your_bot_token
16
+ * export {PREFIX}_TELEGRAM_CHAT_ID=your_chat_id (for push notifications)
17
+ * export {PREFIX}_TELEGRAM_ALLOWED_IDS=id1,id2 (whitelist — comma-separated user/chat IDs)
18
+ * 3. Run: aicc telegram
19
+ *
20
+ * Security:
21
+ * - TELEGRAM_ALLOWED_IDS restricts who can use the bot.
22
+ * - If not set, only TELEGRAM_CHAT_ID is allowed.
23
+ * - If neither is set, the bot runs in "discovery mode" — it only responds
24
+ * to /start (to show the chat ID) and ignores everything else.
25
+ * - Unauthorized users get a single rejection message.
26
+ *
27
+ * To find your chat ID: send /start to the bot, check the logs.
28
+ */
29
+ import { Api, Bot } from 'grammy';
30
+ import { env, getConfig, loadConfig } from '../config.js';
31
+ import { autoResumePipeline } from '../shared/action-runner.js';
32
+ import { bus } from '../shared/event-bus.js';
33
+ import { registerCommands } from './commands.js';
34
+ import { setupNotifications } from './notifications.js';
35
+
36
+ // ─── Resilience utilities (ported from OpenClaw's monitorTelegramProvider) ─────
37
+
38
+ /** Backoff policy — starts at 2s, caps at 30s, with 25% random jitter */
39
+ const RESTART_POLICY = { initialMs: 2000, maxMs: 30000, factor: 1.8, jitter: 0.25 };
40
+
41
+ /** Exponential backoff with jitter — prevents thundering herd on API recovery */
42
+ function computeBackoff(attempt, policy = RESTART_POLICY) {
43
+ const base = Math.min(policy.initialMs * Math.pow(policy.factor, attempt), policy.maxMs);
44
+ const jitter = base * policy.jitter * (Math.random() * 2 - 1); // ±25%
45
+ return Math.max(500, Math.round(base + jitter));
46
+ }
47
+
48
+ /** Classify errors as recoverable (network/transient) vs fatal (code bugs, auth) */
49
+ function isRecoverableError(err) {
50
+ if (!err) return false;
51
+ const msg = String(err?.message || err).toLowerCase();
52
+ const code = err?.code || '';
53
+
54
+ // Recoverable system error codes (network layer)
55
+ const RECOVERABLE_CODES = [
56
+ 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN',
57
+ 'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH', 'ERR_SOCKET_CONNECTION_TIMEOUT',
58
+ ];
59
+ if (RECOVERABLE_CODES.includes(code)) return true;
60
+
61
+ // Telegram API 409 conflict is always recoverable
62
+ if (err?.error_code === 409) return true;
63
+
64
+ // Recoverable message patterns (network issues, rate limits, server errors)
65
+ const RECOVERABLE_PATTERNS = [
66
+ 'network', 'timeout', 'econnreset', 'econnrefused', 'etimedout',
67
+ 'socket', 'dns', 'fetch failed', 'abort', 'terminated', 'getaddrinfo',
68
+ 'connection', '502 ', '503 ', '429 ', 'too many requests',
69
+ 'internal server error', 'bad gateway', 'service unavailable',
70
+ ];
71
+ return RECOVERABLE_PATTERNS.some(p => msg.includes(p));
72
+ }
73
+
74
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
75
+
76
+ // ─── Exported for testing ──────────────────────────────────────────────────────
77
+ export { computeBackoff, isRecoverableError, RESTART_POLICY };
78
+
79
+ // ─── Main bot startup ──────────────────────────────────────────────────────────
80
+
81
+ export async function startBot() {
82
+ // Guard against double execution (ESM can re-evaluate modules)
83
+ const GUARD_KEY = '__aicc_telegram_bot_started__';
84
+ if (globalThis[GUARD_KEY]) return;
85
+ globalThis[GUARD_KEY] = true;
86
+
87
+ // Ensure config + .env are loaded before reading env vars
88
+ await loadConfig().catch(() => {});
89
+
90
+ const BOT_TOKEN = env('TELEGRAM_TOKEN');
91
+ const CHAT_ID = env('TELEGRAM_CHAT_ID');
92
+ const ALLOWED_RAW = env('TELEGRAM_ALLOWED_IDS');
93
+
94
+ if (!BOT_TOKEN) {
95
+ const prefix = (() => { try { return getConfig().envPrefix; } catch { return 'AICC'; } })();
96
+ console.error(`\n Missing ${prefix}_TELEGRAM_TOKEN environment variable.`);
97
+ console.error(' Get one from @BotFather on Telegram.\n');
98
+ process.exit(1);
99
+ }
100
+
101
+ // Deprecation notice: recommend OpenClaw bridge for Telegram
102
+ try {
103
+ const { existsSync: _exists } = await import('fs');
104
+ const { resolve: _resolve } = await import('path');
105
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
106
+ if (_exists(_resolve(homeDir, '.openclaw', 'openclaw.json'))) {
107
+ console.log(' ⚠️ OpenClaw detected. Consider using the OpenClaw bridge instead of the built-in Telegram bot.');
108
+ console.log(' Set roleplay.openclawBridge: true in aicc.config.js and run "aicc start".');
109
+ console.log(' The bridge provides multi-channel support (Telegram + WhatsApp + Slack + Discord).\n');
110
+ }
111
+ } catch { /* ok */ }
112
+
113
+ // ─── Build whitelist ──────────────────────────────────────────────────────────
114
+ // Accepts user IDs and/or chat IDs (both are numbers in Telegram).
115
+ // Sources: TELEGRAM_ALLOWED_IDS (comma-separated) + TELEGRAM_CHAT_ID
116
+ const allowedIds = new Set();
117
+
118
+ if (ALLOWED_RAW) {
119
+ ALLOWED_RAW.split(',').map(s => s.trim()).filter(Boolean).forEach(id => allowedIds.add(id));
120
+ }
121
+ if (CHAT_ID) {
122
+ allowedIds.add(CHAT_ID);
123
+ }
124
+
125
+ const isWhitelistConfigured = allowedIds.size > 0;
126
+
127
+ // ─── Standalone API for notifications — survives bot restarts ─────────────────
128
+ // This never gets stale because it's not tied to any polling lifecycle.
129
+ const notifyApi = new Api(BOT_TOKEN);
130
+
131
+ /** Send a notification to the chat — fire-and-forget, never throws */
132
+ function notifyChat(text, opts = {}) {
133
+ if (!CHAT_ID) return;
134
+ notifyApi.sendMessage(CHAT_ID, text, { parse_mode: 'HTML', ...opts }).catch(() => {});
135
+ }
136
+
137
+ // ─── Auth middleware (reusable across fresh bot instances) ─────────────────────
138
+ const authMiddleware = async (ctx, next) => {
139
+ const chatId = String(ctx.chat?.id ?? '');
140
+ const userId = String(ctx.from?.id ?? '');
141
+ const username = ctx.from?.username || 'unknown';
142
+
143
+ // Always allow /start so new users can discover their chat ID
144
+ const isStart = ctx.message?.text?.startsWith('/start') || false;
145
+
146
+ if (!isWhitelistConfigured) {
147
+ // Discovery mode — only /start works
148
+ if (isStart) {
149
+ console.log(` [AUTH] Discovery mode — /start from ${username} (user: ${userId}, chat: ${chatId})`);
150
+ console.log(` [AUTH] To whitelist, set: ${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS=${userId}`);
151
+ await ctx.reply(
152
+ `Your user ID: \`${userId}\`\nYour chat ID: \`${chatId}\`\n\n` +
153
+ `Add one of these to your env to enable the bot:\n` +
154
+ `\`${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS=${userId}\``,
155
+ { parse_mode: 'Markdown' }
156
+ );
157
+ }
158
+ return; // Block all commands in discovery mode
159
+ }
160
+
161
+ // Whitelist check — match against user ID or chat ID
162
+ if (allowedIds.has(userId) || allowedIds.has(chatId)) {
163
+ return next(); // Authorized — proceed to command handlers
164
+ }
165
+
166
+ // Unauthorized
167
+ console.log(` [AUTH] Blocked: ${username} (user: ${userId}, chat: ${chatId})`);
168
+
169
+ if (isStart) {
170
+ await ctx.reply(
171
+ `Access denied.\n\nYour user ID: \`${userId}\`\nContact the bot owner to request access.`,
172
+ { parse_mode: 'Markdown' }
173
+ );
174
+ }
175
+ // Silently ignore all other messages from unauthorized users
176
+ };
177
+
178
+ // ─── Notifications — set up ONCE using standalone API ─────────────────────────
179
+ // Pass { api: notifyApi } as a proxy — setupNotifications only uses bot.api.*
180
+ // This way notifications survive bot restarts without re-registering bus listeners.
181
+ setupNotifications({ api: notifyApi }, bus, CHAT_ID);
182
+
183
+ // Start watching status.json
184
+ bus.startWatching();
185
+
186
+ // Initialize roleplay room + PM agent (non-fatal)
187
+ try {
188
+ const { isRoleplayEnabled } = await import('../roleplay/roleplay-config.js');
189
+ if (isRoleplayEnabled()) {
190
+ const { initRoom } = await import('../roleplay/room.js');
191
+ const { startPMAgent } = await import('../roleplay/pm-agent.js');
192
+ const room = initRoom(bus);
193
+ startPMAgent(bus, room);
194
+ }
195
+ } catch { /* roleplay optional — non-fatal */ }
196
+
197
+ // ─── Resilient polling loop ──────────────────────────────────────────────────
198
+ let _crashCount = 0;
199
+ let _attempt = 0;
200
+ let _currentBot = null;
201
+ let _triggerRestart = null; // crash signal resolver — accessible by global handlers
202
+ let _running = true;
203
+ let _lastRecoveryNotifyTime = 0; // suppress repeated "back online" messages
204
+ const RECOVERY_NOTIFY_COOLDOWN = 5 * 60 * 1000; // 5 minutes between recovery notifications
205
+
206
+ /** Create a FRESH Bot instance with auth middleware + commands registered.
207
+ * This is the #1 fix — a fresh instance has no stale HTTP state, no broken
208
+ * keep-alive connections, no corrupted polling offset. OpenClaw does this
209
+ * every cycle via createPollingBot(). */
210
+ /** Telegram command menu — shown when user types "/" in the chat */
211
+ const BOT_COMMANDS = [
212
+ { command: 'feature', description: '✨ Submit a new feature' },
213
+ { command: 'bug', description: '🐛 Report and fix a bug' },
214
+ { command: 'review', description: '🔍 Trigger code review' },
215
+ { command: 'approve', description: '✅ Approve current feature' },
216
+ { command: 'reject', description: '❌ Reject with reason' },
217
+ { command: 'implement', description: '⚡ Trigger Coder implementation' },
218
+ { command: 'deploy', description: '🚀 Deploy to environment' },
219
+ { command: 'status', description: '📊 Pipeline status' },
220
+ { command: 'health', description: '🏥 System health check' },
221
+ { command: 'logs', description: '📋 View session logs' },
222
+ { command: 'docs', description: '📂 Browse pipeline documents' },
223
+ { command: 'costs', description: '💰 AI usage & cost summary' },
224
+ { command: 'leaderboard', description: '🏆 AI model performance' },
225
+ { command: 'audit', description: '📋 Recent audit log' },
226
+ { command: 'autopilot', description: '⚡ Toggle auto-pilot mode' },
227
+ { command: 'reset', description: '🔄 Reset / Abandon feature' },
228
+ { command: 'cleanup', description: '🧹 Clean up workspace' },
229
+ { command: 'retry', description: '🔁 Retry from checkpoint' },
230
+ { command: 'ask', description: '🤖 Ask AI a question' },
231
+ { command: 'aimode', description: '🧠 View/toggle AI mode' },
232
+ { command: 'dryrun', description: '🧪 Pipeline dry run' },
233
+ { command: 'assign', description: '🎯 Assign project URL + goal to AI IT dept' },
234
+ { command: 'qa', description: '🔬 Run browser QA tests on target website' },
235
+ { command: 'suggest', description: '💡 AI suggests features based on QA data' },
236
+ { command: 'threads', description: '💬 View open discussion threads' },
237
+ { command: 'menu', description: '📱 Show main menu buttons' },
238
+ ];
239
+
240
+ function createFreshBot() {
241
+ const bot = new Bot(BOT_TOKEN);
242
+ bot.use(authMiddleware);
243
+ registerCommands(bot);
244
+ // Register command menu with Telegram — shows suggestions when user types "/"
245
+ bot.api.setMyCommands(BOT_COMMANDS).catch(err =>
246
+ console.error(` [Bot] Failed to set command menu: ${err.message}`)
247
+ );
248
+ return bot;
249
+ }
250
+
251
+ async function resilientLoop() {
252
+ while (_running) {
253
+ // ── Create FRESH bot each cycle — no stale HTTP/polling state ──────────
254
+ const bot = createFreshBot();
255
+ _currentBot = bot;
256
+
257
+ // Crash signal — resolved when bot.catch() or global error handlers fire.
258
+ // The Promise.race below picks up either clean-stop or crash.
259
+ const crashSignal = new Promise(resolve => { _triggerRestart = resolve; });
260
+
261
+ bot.catch((err) => {
262
+ const e = err.error || err;
263
+ _crashCount++;
264
+ console.error(` [Bot] Polling/middleware error: ${e.message || e}`);
265
+ try { bot.stop(); } catch {}
266
+ if (_triggerRestart) _triggerRestart(e);
267
+ });
268
+
269
+ // Clear any stale webhook before starting — prevents 409 conflicts
270
+ try { await bot.api.deleteWebhook({ drop_pending_updates: false }); } catch {}
271
+
272
+ try {
273
+ const result = await Promise.race([
274
+ bot.start({
275
+ onStart: (info) => {
276
+ const wasRestarted = _crashCount > 0;
277
+ if (wasRestarted) _attempt = 0; // reset backoff on successful reconnect
278
+ _crashCount = 0;
279
+
280
+ console.log(`\n ${getConfig().name} Telegram Bot`);
281
+ console.log(` @${info.username}`);
282
+ if (isWhitelistConfigured) {
283
+ console.log(` Whitelist: ${[...allowedIds].join(', ')}`);
284
+ } else {
285
+ console.log(` WARNING: No whitelist configured — discovery mode only`);
286
+ console.log(` Set ${getConfig().envPrefix}_TELEGRAM_ALLOWED_IDS to enable commands`);
287
+ }
288
+ if (CHAT_ID) console.log(` Push notifications → chat ${CHAT_ID}`);
289
+ console.log('');
290
+
291
+ // Notify user when bot recovers from a crash (with cooldown to prevent spam)
292
+ if (wasRestarted && Date.now() - _lastRecoveryNotifyTime > RECOVERY_NOTIFY_COOLDOWN) {
293
+ _lastRecoveryNotifyTime = Date.now();
294
+ const s = bus.getStatus();
295
+ const pipelineInfo = s?.current_feature
296
+ ? `\nPipeline: <code>${s.current_feature}</code> at stage <b>${s.stage}</b>`
297
+ : '';
298
+ notifyChat(`🟢 Bot is back online after crash recovery.${pipelineInfo}`);
299
+ }
300
+
301
+ // Resume any auto pipeline that was interrupted by a restart
302
+ setTimeout(() => autoResumePipeline().catch(err =>
303
+ console.error('[Pipeline] Resume error:', err.message)), 3000);
304
+ },
305
+ }).then(() => ({ type: 'clean-stop' })),
306
+ crashSignal.then(e => ({ type: 'crash', error: e })),
307
+ ]);
308
+
309
+ if (result.type === 'clean-stop') {
310
+ // Graceful stop (SIGINT) — exit loop
311
+ console.log(' [Bot] Graceful stop.');
312
+ break;
313
+ }
314
+
315
+ // ── Handle crash — compute backoff and retry ────────────────────────
316
+ const err = result.error;
317
+ _attempt++;
318
+
319
+ // 409 = another instance polling — must wait for Telegram's 30s timeout
320
+ if (err?.error_code === 409) {
321
+ const delay = 35000;
322
+ _crashCount = 0; // 409 is not a real crash — reset so recovery notification doesn't fire
323
+ console.log(` [Bot] Conflict (409) — waiting 35s for Telegram to release... (attempt ${_attempt})`);
324
+ await sleep(delay);
325
+ continue;
326
+ }
327
+
328
+ // Classify error and compute appropriate delay
329
+ const recoverable = isRecoverableError(err);
330
+ const delay = recoverable ? computeBackoff(_attempt) : 30000;
331
+ const label = recoverable ? 'Recoverable' : 'Fatal';
332
+ console.log(` [Bot] ${label} error — restarting with fresh instance in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
333
+ // Only notify on first error or after cooldown — prevent spam during crash loops
334
+ if (_attempt <= 1 || Date.now() - _lastRecoveryNotifyTime > RECOVERY_NOTIFY_COOLDOWN) {
335
+ notifyChat(`${recoverable ? '⚠️' : '🚨'} Bot ${label.toLowerCase()} error: ${err?.message || 'unknown'}\nRestarting in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
336
+ }
337
+ await sleep(delay);
338
+
339
+ } catch (err) {
340
+ // Startup-phase error (before polling begins)
341
+ _crashCount++;
342
+ _attempt++;
343
+
344
+ if (err.error_code === 409) {
345
+ _crashCount = 0; // 409 is not a real crash — reset so recovery notification doesn't fire
346
+ console.log(` [Bot] Conflict (409) at startup — waiting 35s...`);
347
+ await sleep(35000);
348
+ continue;
349
+ }
350
+
351
+ const recoverable = isRecoverableError(err);
352
+ const delay = recoverable ? computeBackoff(_attempt) : 30000;
353
+ console.error(` [Bot] Startup error: ${err.message}`);
354
+ console.log(` [Bot] Restarting in ${(delay / 1000).toFixed(1)}s (attempt ${_attempt})...`);
355
+ notifyChat(`🚨 Bot startup error: ${err.message}\nRestarting in ${(delay / 1000).toFixed(1)}s...`);
356
+ await sleep(delay);
357
+ }
358
+ }
359
+ }
360
+
361
+ // ─── Global error handlers — force restart via crash signal ──────────────────
362
+ // These catch errors that escape grammy's error boundary (unhandled promises,
363
+ // uncaught exceptions from callbacks, etc.) and feed them into the resilient
364
+ // loop instead of letting the process die silently.
365
+
366
+ process.on('uncaughtException', (err) => {
367
+ console.error(' [Bot] Uncaught exception:', err.message);
368
+ _crashCount++;
369
+ notifyChat(`🚨 Uncaught exception: ${err.message}\nForcing restart with fresh bot instance...`);
370
+ if (_currentBot) try { _currentBot.stop(); } catch {}
371
+ if (_triggerRestart) _triggerRestart(err);
372
+ });
373
+
374
+ process.on('unhandledRejection', (err) => {
375
+ const msg = err?.message || String(err);
376
+ if (isRecoverableError(err)) {
377
+ // Network/transient error — restart with backoff
378
+ console.error(' [Bot] Unhandled rejection (recoverable):', msg);
379
+ _crashCount++;
380
+ if (_currentBot) try { _currentBot.stop(); } catch {}
381
+ if (_triggerRestart) _triggerRestart(err);
382
+ } else {
383
+ // Non-network rejection — log but don't restart (might be a code bug
384
+ // that would cause infinite restart loops)
385
+ console.error(' [Bot] Unhandled rejection (non-recoverable, not restarting):', msg);
386
+ }
387
+ });
388
+
389
+ // ─── Signal handling ──────────────────────────────────────────────────────────
390
+
391
+ // Ignore SIGHUP — prevents instant death when terminal disconnects
392
+ // (SSH timeout, terminal tab close, macOS sleep, etc.)
393
+ process.on('SIGHUP', () => {
394
+ console.log(' [Bot] SIGHUP received — ignoring (terminal disconnect).');
395
+ });
396
+
397
+ // Cleanup on explicit interrupt
398
+ process.on('SIGINT', () => {
399
+ _running = false;
400
+ bus.stopWatching();
401
+ if (_currentBot) _currentBot.stop();
402
+ process.exit(0);
403
+ });
404
+
405
+ // ─── Start the resilient loop ─────────────────────────────────────────────────
406
+ resilientLoop().catch(err => {
407
+ console.error(' [Bot] Resilience loop fatal crash:', err.message);
408
+ notifyChat(`💀 Bot resilience loop crashed: ${err.message}`);
409
+ process.exit(1);
410
+ });
411
+ }
412
+
413
+ // Auto-start when run directly (node bot.js), not when imported via aicc CLI
414
+ if (process.argv[1]?.endsWith('bot.js')) {
415
+ startBot();
416
+ }