appback-remoteagent 0.13.21 → 0.14.1

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 CHANGED
@@ -1,6 +1,5 @@
1
1
  TELEGRAM_BOT_TOKEN=your-telegram-bot-token
2
2
  TELEGRAM_BOT_TOKENS=
3
- TELEGRAM_MAIN_BOT_ID=
4
3
  TELEGRAM_OWNER_ID=
5
4
  TELEGRAM_MESSAGE_BATCH_MS=1500
6
5
  TELEGRAM_AUTO_PROGRESS_MAX_TURNS=6
@@ -9,12 +8,11 @@ TELEGRAM_RETRYABLE_ERROR_RETRIES=2
9
8
  TELEGRAM_RETRYABLE_ERROR_DELAY_MS=5000
10
9
  TELEGRAM_UNTAGGED_INTENT_RETRIES=2
11
10
  TELEGRAM_SCHEDULER_TICK_MS=1000
12
- TELEGRAM_TIERED_POLLING_MIN_BOTS=4
11
+ TELEGRAM_TIERED_POLLING_MIN_BOTS=5
13
12
  TELEGRAM_ACTIVE_POLL_INTERVAL_MS=3000
14
- TELEGRAM_IDLE_POLL_INTERVAL_MS=30000
15
- TELEGRAM_COLD_POLL_INTERVAL_MS=300000
16
- TELEGRAM_ACTIVE_IDLE_MS=1800000
17
- TELEGRAM_COLD_IDLE_MS=21600000
13
+ TELEGRAM_RUNNING_POLL_INTERVAL_MS=60000
14
+ TELEGRAM_SECONDARY_POLL_INTERVAL_MS=60000
15
+ TELEGRAM_TERTIARY_POLL_INTERVAL_MS=180000
18
16
  ARTIFACT_CLEANUP_ENABLED=true
19
17
  ARTIFACT_RETENTION_DAYS=30
20
18
  ARTIFACT_CLEANUP_INTERVAL_MS=86400000
package/README.md CHANGED
@@ -112,11 +112,8 @@ Current command surface implemented in `src/bot.ts`:
112
112
  | `/bots` | Lists the currently configured Telegram bots |
113
113
  | `/bot add <token>` | Adds a conversation bot, restarts the runtime, and confirms the result after restart |
114
114
  | `/bot doctor` | Checks configured Telegram bots and removes bots that Telegram reports as permanently dead |
115
- | `/bot main <number\|@username\|id>` | Selects the main bot. If no valid main is configured, the oldest configured bot is used |
116
115
  | `/bot remove <username\|id>` | Removes a configured Telegram bot, restarts the runtime, and confirms the result after restart |
117
116
  | `/bot reload` | Restarts the runtime and confirms the result after restart |
118
- | `/sleep [bot]` | Puts a sub bot into deep sleep without removing its configuration |
119
- | `/wake <bot>` | Wakes a sleeping sub bot |
120
117
  | `/install codex\|claude` | Runs the configured provider install or update command for the bot owner |
121
118
  | `/login codex` | Starts the Codex device-auth login flow and returns a browser URL when available |
122
119
  | `/login claude [token]` | Starts or finishes the configured Claude Code login flow for the bot owner |
@@ -127,7 +124,7 @@ Current command surface implemented in `src/bot.ts`:
127
124
  | `/batch cancel` | Discards the current batch |
128
125
  | `/batch status` | Shows current batch state |
129
126
 
130
- Bot sleep keeps long-idle sub bots configured while stopping their polling load. See [docs/BOT_SLEEP.md](./docs/BOT_SLEEP.md).
127
+ Multi-bot polling is tiered by recent activity and active provider work. See [docs/BOT_POLLING_POLICY.md](./docs/BOT_POLLING_POLICY.md).
131
128
 
132
129
  ### 2. Terminal control
133
130
 
@@ -31,7 +31,7 @@ export class CodexAdapter {
31
31
  };
32
32
  }
33
33
  if (code !== 0) {
34
- throw new Error(this.formatProcessError(stdout, stderr, timedOut));
34
+ throw new Error(this.formatProcessError(stdout, stderr, timedOut, code));
35
35
  }
36
36
  if (timedOut) {
37
37
  throw new Error(this.formatTimeoutError());
@@ -147,7 +147,7 @@ export class CodexAdapter {
147
147
  async readOutput(outputPath) {
148
148
  return (await fs.readFile(outputPath, "utf8").catch(() => "")).trim();
149
149
  }
150
- formatProcessError(stdout, stderr, timedOut = false) {
150
+ formatProcessError(stdout, stderr, timedOut = false, code) {
151
151
  const structured = this.extractStructuredError(stdout, stderr);
152
152
  if (structured) {
153
153
  return structured;
@@ -158,7 +158,7 @@ export class CodexAdapter {
158
158
  }
159
159
  return timedOut
160
160
  ? this.formatTimeoutError()
161
- : "Codex execution failed without any output.";
161
+ : `Codex process exited with code ${code ?? "unknown"} without stdout/stderr.`;
162
162
  }
163
163
  extractStructuredError(stdout, stderr) {
164
164
  const messages = [];
package/dist/bot.js CHANGED
@@ -33,11 +33,8 @@ const HELP_TEXT = [
33
33
  "/bots",
34
34
  "/bot add <token>",
35
35
  "/bot doctor",
36
- "/bot main <number|@username|id>",
37
36
  "/bot remove <username|id>",
38
37
  "/bot reload",
39
- "/sleep [bot]",
40
- "/wake <bot>",
41
38
  "/install codex|claude",
42
39
  "/login codex",
43
40
  "/login claude [token]",
@@ -86,8 +83,6 @@ const RECOGNIZED_COMMANDS = new Set([
86
83
  "docs",
87
84
  "bots",
88
85
  "bot",
89
- "sleep",
90
- "wake",
91
86
  "install",
92
87
  "login",
93
88
  "reset",
@@ -232,7 +227,7 @@ export function createBot(token, bridge, botManagement, botInfo) {
232
227
  await bridge.logSystem(botId, chatId, `Telegram text dispatch (${text.length} chars).`);
233
228
  await runWithPendingAnimation(target.botToken, target.telegramChatId, async (helpers) => {
234
229
  return {
235
- chunks: await routeTelegramWorkLoop(bridge, botId, chatId, text, "Telegram text request", helpers, autoContinue, memoryService),
230
+ chunks: await routeTelegramWorkLoop(bridge, botId, chatId, text, "Telegram text request", botManagement, helpers, autoContinue, memoryService),
236
231
  };
237
232
  });
238
233
  });
@@ -701,8 +696,8 @@ ${bridge.formatStatus(mapping)}`);
701
696
  const sourceBotId = getBotId();
702
697
  const { args, rest } = parseCommand(ctx.message?.text, 2);
703
698
  const action = args[0]?.toLowerCase();
704
- if (!action || !["add", "doctor", "main", "remove", "reload"].includes(action)) {
705
- await reply(ctx, "Usage: `/bot add <token>`, `/bot doctor`, `/bot main <number|@username|id>`, `/bot remove <username|id>`, or `/bot reload`", {
699
+ if (!action || !["add", "doctor", "remove", "reload"].includes(action)) {
700
+ await reply(ctx, "Usage: `/bot add <token>`, `/bot doctor`, `/bot remove <username|id>`, or `/bot reload`", {
706
701
  parse_mode: "Markdown",
707
702
  });
708
703
  return;
@@ -725,11 +720,6 @@ ${bridge.formatStatus(mapping)}`);
725
720
  await reply(ctx, result.message);
726
721
  return;
727
722
  }
728
- if (action === "main") {
729
- const result = await botManagement.setMainBot(commandTarget(args, rest));
730
- await reply(ctx, result.message);
731
- return;
732
- }
733
723
  if (action === "remove") {
734
724
  const result = await botManagement.removeBot(commandTarget(args, rest), sourceBotId, sourceBotToken, ctx.chat.id);
735
725
  await reply(ctx, result.message);
@@ -738,19 +728,6 @@ ${bridge.formatStatus(mapping)}`);
738
728
  const result = await botManagement.reloadBots(sourceBotId, sourceBotToken, ctx.chat.id);
739
729
  await reply(ctx, result.message);
740
730
  });
741
- bot.command("sleep", async (ctx) => {
742
- await ensureOwnerControlAccess(ctx);
743
- const sourceBotId = getBotId();
744
- const { args, rest } = parseCommand(ctx.message?.text, 1);
745
- const result = await botManagement.sleepBot(commandTarget(args, rest), sourceBotId);
746
- await reply(ctx, result.message);
747
- });
748
- bot.command("wake", async (ctx) => {
749
- await ensureOwnerControlAccess(ctx);
750
- const { args, rest } = parseCommand(ctx.message?.text, 1);
751
- const result = await botManagement.wakeBot(commandTarget(args, rest));
752
- await reply(ctx, result.message);
753
- });
754
731
  bot.command("install", async (ctx) => {
755
732
  await ensureOwnerControlAccess(ctx);
756
733
  const { args } = parseCommand(ctx.message?.text, 1);
@@ -849,7 +826,7 @@ ${bridge.formatStatus(mapping)}`);
849
826
  await bridge.logSystem(botId, chatId, `Telegram image received: ${downloaded.path}`);
850
827
  await runWithPendingAnimation(token, ctx.chat.id, async (helpers) => {
851
828
  return {
852
- chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, "Telegram image request", helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
829
+ chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, "Telegram image request", botManagement, helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
853
830
  };
854
831
  });
855
832
  return;
@@ -874,7 +851,7 @@ ${bridge.formatStatus(mapping)}`);
874
851
  await bridge.logSystem(botId, chatId, `Telegram ${attachment.kind} received: ${downloaded.path}`);
875
852
  await runWithPendingAnimation(token, ctx.chat.id, async (helpers) => {
876
853
  return {
877
- chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, `Telegram ${attachment.kind} request`, helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
854
+ chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, `Telegram ${attachment.kind} request`, botManagement, helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
878
855
  };
879
856
  });
880
857
  return;
@@ -895,7 +872,7 @@ ${bridge.formatStatus(mapping)}`);
895
872
  await bridge.logSystem(botId, chatId, `Telegram ${attachmentKind} received: ${downloaded.path}`);
896
873
  await runWithPendingAnimation(token, ctx.chat.id, async (helpers) => {
897
874
  return {
898
- chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, `Telegram ${attachmentKind} request`, helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
875
+ chunks: await routeTelegramWorkLoop(bridge, botId, chatId, message, `Telegram ${attachmentKind} request`, botManagement, helpers, autoContinue, memoryService, sanitizeAttachmentResponseBlocks),
899
876
  };
900
877
  });
901
878
  return;
@@ -1148,7 +1125,7 @@ async function runWithPendingAnimation(botToken, chatId, task) {
1148
1125
  }
1149
1126
  }
1150
1127
  }
1151
- async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, helpers, autoContinue, memoryService, transform = (blocks) => blocks) {
1128
+ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botManagement, helpers, autoContinue, memoryService, transform = (blocks) => blocks) {
1152
1129
  const currentSession = await bridge.status(botId, chatId);
1153
1130
  const sessionId = currentSession?.session.sessionId;
1154
1131
  if (currentSession) {
@@ -1180,154 +1157,164 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, help
1180
1157
  await bridge.stopSessionRun(sessionId, botId, chatId, `Telegram work loop stopped during ${phase} because the chat is now bound to another session.`);
1181
1158
  throw new SilentTelegramAbort(`Session ${currentSession?.session.publicId ?? sessionId} is no longer bound to this chat.`);
1182
1159
  };
1183
- for (let turn = 1;; turn += 1) {
1184
- if (typeof maxTurns === "number" && maxTurns > 0 && turn > maxTurns) {
1185
- const limitMessage = `Automatic continue limit (${maxTurns}) reached before a final result.`;
1186
- await bridge.logSystem(botId, chatId, limitMessage);
1187
- autoContinue.clear(botId, chatId, sessionId);
1188
- return [limitMessage];
1189
- }
1190
- if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1191
- const stopMessage = "Automatic continuation stopped.";
1192
- await bridge.logSystem(botId, chatId, stopMessage);
1193
- autoContinue.clear(botId, chatId, sessionId);
1194
- return [stopMessage];
1195
- }
1196
- await ensureStillBound(`turn ${turn} start`);
1197
- const turnLabel = `${label} turn ${turn}`;
1198
- await bridge.logSystem(botId, chatId, `${turnLabel} started.`);
1199
- try {
1200
- const responses = sessionId
1201
- ? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt)
1202
- : await bridge.routeMessage(botId, chatId, prompt);
1203
- await ensureStillBound(`${turnLabel} response`);
1204
- const parsed = parseReportResponses(bridge.formatResponses(responses), transform);
1205
- await bridge.logSystem(botId, chatId, `${turnLabel} returned ${parsed.kind}.`);
1206
- emptyResponseRetryCount = 0;
1207
- retryableErrorCount = 0;
1208
- if (parsed.kind === "progress") {
1209
- untaggedIntentRetryCount = 0;
1210
- missingEvidenceRetryCount = 0;
1211
- deliveredProgressCount += 1;
1212
- if (currentSession) {
1213
- const progress = await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
1214
- if (progress.repeated) {
1215
- const repeatedMessage = [
1216
- "Repeated progress detected. The same work pattern has appeared 3 or more times.",
1217
- "Automatic continuation stopped so the task can be inspected instead of looping.",
1160
+ if (currentSession) {
1161
+ await botManagement.markProviderRunning(botId, sessionId);
1162
+ }
1163
+ try {
1164
+ for (let turn = 1;; turn += 1) {
1165
+ if (typeof maxTurns === "number" && maxTurns > 0 && turn > maxTurns) {
1166
+ const limitMessage = `Automatic continue limit (${maxTurns}) reached before a final result.`;
1167
+ await bridge.logSystem(botId, chatId, limitMessage);
1168
+ autoContinue.clear(botId, chatId, sessionId);
1169
+ return [limitMessage];
1170
+ }
1171
+ if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1172
+ const stopMessage = "Automatic continuation stopped.";
1173
+ await bridge.logSystem(botId, chatId, stopMessage);
1174
+ autoContinue.clear(botId, chatId, sessionId);
1175
+ return [stopMessage];
1176
+ }
1177
+ await ensureStillBound(`turn ${turn} start`);
1178
+ const turnLabel = `${label} turn ${turn}`;
1179
+ await bridge.logSystem(botId, chatId, `${turnLabel} started.`);
1180
+ try {
1181
+ const responses = sessionId
1182
+ ? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt)
1183
+ : await bridge.routeMessage(botId, chatId, prompt);
1184
+ await ensureStillBound(`${turnLabel} response`);
1185
+ const parsed = parseReportResponses(bridge.formatResponses(responses), transform);
1186
+ await bridge.logSystem(botId, chatId, `${turnLabel} returned ${parsed.kind}.`);
1187
+ emptyResponseRetryCount = 0;
1188
+ retryableErrorCount = 0;
1189
+ if (parsed.kind === "progress") {
1190
+ untaggedIntentRetryCount = 0;
1191
+ missingEvidenceRetryCount = 0;
1192
+ deliveredProgressCount += 1;
1193
+ if (currentSession) {
1194
+ const progress = await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
1195
+ if (progress.repeated) {
1196
+ const repeatedMessage = [
1197
+ "Repeated progress detected. The same work pattern has appeared 3 or more times.",
1198
+ "Automatic continuation stopped so the task can be inspected instead of looping.",
1199
+ ].join("\n");
1200
+ await bridge.logSystem(botId, chatId, repeatedMessage);
1201
+ autoContinue.clear(botId, chatId, sessionId);
1202
+ return [repeatedMessage];
1203
+ }
1204
+ }
1205
+ await ensureStillBound(`${turnLabel} progress delivery`);
1206
+ await helpers.reportProgress(parsed.chunks);
1207
+ if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1208
+ const stopMessage = "Automatic continuation stopped after the latest progress report.";
1209
+ await bridge.logSystem(botId, chatId, stopMessage);
1210
+ autoContinue.clear(botId, chatId, sessionId);
1211
+ return [stopMessage];
1212
+ }
1213
+ prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1214
+ continue;
1215
+ }
1216
+ if (parsed.kind === "result") {
1217
+ untaggedIntentRetryCount = 0;
1218
+ const resultText = parsed.chunks.join("\n");
1219
+ const evidenceIssue = classifyMissingResultEvidence(resultText);
1220
+ if (evidenceIssue && missingEvidenceRetryCount < 1) {
1221
+ missingEvidenceRetryCount += 1;
1222
+ const retryMessage = `${turnLabel} returned a result without required evidence: ${evidenceIssue}`;
1223
+ await bridge.logSystem(botId, chatId, retryMessage);
1224
+ prompt = appendManagedContext(formatMissingEvidenceRetryPrompt(resultText, evidenceIssue), managedContext);
1225
+ continue;
1226
+ }
1227
+ if (evidenceIssue) {
1228
+ const blockedMessage = [
1229
+ "Provider reported a completed result without concrete evidence after a retry.",
1230
+ `Reason: ${evidenceIssue}`,
1231
+ "Automatic continuation stopped so the work is not accepted on an unsupported claim.",
1218
1232
  ].join("\n");
1219
- await bridge.logSystem(botId, chatId, repeatedMessage);
1233
+ await bridge.logSystem(botId, chatId, blockedMessage);
1220
1234
  autoContinue.clear(botId, chatId, sessionId);
1221
- return [repeatedMessage];
1235
+ return [blockedMessage];
1236
+ }
1237
+ await ensureStillBound(`${turnLabel} final delivery`);
1238
+ if (currentSession) {
1239
+ await memoryService.completeTask(currentSession.session, parsed.chunks.join("\n"));
1222
1240
  }
1241
+ autoContinue.clear(botId, chatId, sessionId);
1242
+ return parsed.chunks;
1223
1243
  }
1224
- await ensureStillBound(`${turnLabel} progress delivery`);
1225
- await helpers.reportProgress(parsed.chunks);
1226
- if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1227
- const stopMessage = "Automatic continuation stopped after the latest progress report.";
1228
- await bridge.logSystem(botId, chatId, stopMessage);
1244
+ if (parsed.kind === "blocked") {
1245
+ untaggedIntentRetryCount = 0;
1246
+ missingEvidenceRetryCount = 0;
1247
+ await ensureStillBound(`${turnLabel} final delivery`);
1229
1248
  autoContinue.clear(botId, chatId, sessionId);
1230
- return [stopMessage];
1249
+ return parsed.chunks;
1231
1250
  }
1232
- prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1233
- continue;
1234
- }
1235
- if (parsed.kind === "result") {
1236
- untaggedIntentRetryCount = 0;
1237
- const resultText = parsed.chunks.join("\n");
1238
- const evidenceIssue = classifyMissingResultEvidence(resultText);
1239
- if (evidenceIssue && missingEvidenceRetryCount < 1) {
1240
- missingEvidenceRetryCount += 1;
1241
- const retryMessage = `${turnLabel} returned a result without required evidence: ${evidenceIssue}`;
1251
+ if (looksLikeUntaggedIntentOnlyResponse(parsed.chunks.join("\n")) && untaggedIntentRetryCount < untaggedIntentRetries) {
1252
+ untaggedIntentRetryCount += 1;
1253
+ const retryMessage = `${turnLabel} returned an untagged intent-only response; asking provider to do concrete work before replying.`;
1242
1254
  await bridge.logSystem(botId, chatId, retryMessage);
1243
- prompt = appendManagedContext(formatMissingEvidenceRetryPrompt(resultText, evidenceIssue), managedContext);
1255
+ prompt = appendManagedContext(formatUntaggedIntentRetryPrompt(parsed.chunks.join("\n")), managedContext);
1244
1256
  continue;
1245
1257
  }
1246
- if (evidenceIssue) {
1247
- const blockedMessage = [
1248
- "Provider reported a completed result without concrete evidence after a retry.",
1249
- `Reason: ${evidenceIssue}`,
1250
- "Automatic continuation stopped so the work is not accepted on an unsupported claim.",
1251
- ].join("\n");
1252
- await bridge.logSystem(botId, chatId, blockedMessage);
1253
- autoContinue.clear(botId, chatId, sessionId);
1254
- return [blockedMessage];
1255
- }
1256
- await ensureStillBound(`${turnLabel} final delivery`);
1257
- if (currentSession) {
1258
- await memoryService.completeTask(currentSession.session, parsed.chunks.join("\n"));
1259
- }
1258
+ await bridge.logSystem(botId, chatId, `${turnLabel} returned an untagged response; treating it as final output.`);
1260
1259
  autoContinue.clear(botId, chatId, sessionId);
1261
1260
  return parsed.chunks;
1262
1261
  }
1263
- if (parsed.kind === "blocked") {
1264
- untaggedIntentRetryCount = 0;
1265
- missingEvidenceRetryCount = 0;
1266
- await ensureStillBound(`${turnLabel} final delivery`);
1267
- autoContinue.clear(botId, chatId, sessionId);
1268
- return parsed.chunks;
1269
- }
1270
- if (looksLikeUntaggedIntentOnlyResponse(parsed.chunks.join("\n")) && untaggedIntentRetryCount < untaggedIntentRetries) {
1271
- untaggedIntentRetryCount += 1;
1272
- const retryMessage = `${turnLabel} returned an untagged intent-only response; asking provider to do concrete work before replying.`;
1273
- await bridge.logSystem(botId, chatId, retryMessage);
1274
- prompt = appendManagedContext(formatUntaggedIntentRetryPrompt(parsed.chunks.join("\n")), managedContext);
1275
- continue;
1276
- }
1277
- await bridge.logSystem(botId, chatId, `${turnLabel} returned an untagged response; treating it as final output.`);
1278
- autoContinue.clear(botId, chatId, sessionId);
1279
- return parsed.chunks;
1280
- }
1281
- catch (error) {
1282
- if (error instanceof SilentTelegramAbort) {
1283
- throw error;
1284
- }
1285
- const messageText = error instanceof Error ? error.message : "An unexpected error occurred.";
1286
- const retryable = classifyRetryableProviderIssue(messageText, retryableErrorDelayMs);
1287
- if (isProviderTimeoutError(messageText)) {
1288
- const timeoutMessage = formatProviderTimeoutFinalMessage(messageText);
1289
- console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} timed out: ${messageText}`);
1290
- await bridge.logSystem(botId, chatId, `${turnLabel} timed out: ${messageText}`);
1291
- autoContinue.clear(botId, chatId, sessionId);
1292
- return [timeoutMessage];
1293
- }
1294
- if (isEmptyResponseError(messageText) && emptyResponseRetryCount < emptyResponseRetries) {
1295
- emptyResponseRetryCount += 1;
1296
- const retryMessage = `${turnLabel} returned an empty response; retrying automatic continuation (${emptyResponseRetryCount}/${emptyResponseRetries}).`;
1297
- console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${retryMessage}`);
1298
- await bridge.logSystem(botId, chatId, retryMessage);
1299
- prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1300
- continue;
1301
- }
1302
- if (retryable && retryableErrorCount < retryableErrorRetries) {
1303
- retryableErrorCount += 1;
1304
- const retryMessage = formatRetryableProviderRetryMessage(retryable, retryableErrorCount, retryableErrorRetries);
1305
- console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} retrying: ${messageText}`);
1306
- await bridge.logSystem(botId, chatId, `${turnLabel} retrying after temporary provider issue: ${messageText}`);
1307
- await helpers.reportProgress([retryMessage]);
1308
- if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1309
- const stopMessage = "Automatic continuation stopped after the latest retry notice.";
1310
- await bridge.logSystem(botId, chatId, stopMessage);
1262
+ catch (error) {
1263
+ if (error instanceof SilentTelegramAbort) {
1264
+ throw error;
1265
+ }
1266
+ const messageText = error instanceof Error ? error.message : "An unexpected error occurred.";
1267
+ const retryable = classifyRetryableProviderIssue(messageText, retryableErrorDelayMs);
1268
+ if (isProviderTimeoutError(messageText)) {
1269
+ const timeoutMessage = formatProviderTimeoutFinalMessage(messageText);
1270
+ console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} timed out: ${messageText}`);
1271
+ await bridge.logSystem(botId, chatId, `${turnLabel} timed out: ${messageText}`);
1311
1272
  autoContinue.clear(botId, chatId, sessionId);
1312
- return [stopMessage];
1273
+ return [timeoutMessage];
1313
1274
  }
1314
- await sleep(retryable.retryAfterMs);
1315
- prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1316
- continue;
1317
- }
1318
- console.error(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} failed: ${messageText}`, error);
1319
- await bridge.logSystem(botId, chatId, `${turnLabel} failed: ${messageText}`);
1320
- autoContinue.clear(botId, chatId, sessionId);
1321
- if (retryable) {
1322
- return [formatRetryableProviderFinalMessage(retryable)];
1323
- }
1324
- if (isEmptyResponseError(messageText) && deliveredProgressCount > 0) {
1325
- return [
1326
- "The last progress report was delivered, but the follow-up provider response came back empty. Automatic continuation stopped here.",
1327
- "Send a new message such as `continue` to resume the same session from the latest state.",
1328
- ];
1275
+ if (isEmptyResponseError(messageText) && emptyResponseRetryCount < emptyResponseRetries) {
1276
+ emptyResponseRetryCount += 1;
1277
+ const retryMessage = `${turnLabel} returned an empty response; retrying automatic continuation (${emptyResponseRetryCount}/${emptyResponseRetries}).`;
1278
+ console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${retryMessage}`);
1279
+ await bridge.logSystem(botId, chatId, retryMessage);
1280
+ prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1281
+ continue;
1282
+ }
1283
+ if (retryable && retryableErrorCount < retryableErrorRetries) {
1284
+ retryableErrorCount += 1;
1285
+ const retryMessage = formatRetryableProviderRetryMessage(retryable, retryableErrorCount, retryableErrorRetries);
1286
+ console.warn(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} retrying: ${messageText}`);
1287
+ await bridge.logSystem(botId, chatId, `${turnLabel} retrying after temporary provider issue: ${messageText}`);
1288
+ await helpers.reportProgress([retryMessage]);
1289
+ if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
1290
+ const stopMessage = "Automatic continuation stopped after the latest retry notice.";
1291
+ await bridge.logSystem(botId, chatId, stopMessage);
1292
+ autoContinue.clear(botId, chatId, sessionId);
1293
+ return [stopMessage];
1294
+ }
1295
+ await sleep(retryable.retryAfterMs);
1296
+ prompt = appendManagedContext(REPORT_CONTINUE_PROMPT, managedContext);
1297
+ continue;
1298
+ }
1299
+ console.error(`[telegram-route] bot=${botId} chat=${chatId} ${turnLabel} failed: ${messageText}`, error);
1300
+ await bridge.logSystem(botId, chatId, `${turnLabel} failed: ${messageText}`);
1301
+ autoContinue.clear(botId, chatId, sessionId);
1302
+ if (retryable) {
1303
+ return [formatRetryableProviderFinalMessage(retryable)];
1304
+ }
1305
+ if (isEmptyResponseError(messageText) && deliveredProgressCount > 0) {
1306
+ return [
1307
+ "The last progress report was delivered, but the follow-up provider response came back empty. Automatic continuation stopped here.",
1308
+ "Send a new message such as `continue` to resume the same session from the latest state.",
1309
+ ];
1310
+ }
1311
+ throw error;
1329
1312
  }
1330
- throw error;
1313
+ }
1314
+ }
1315
+ finally {
1316
+ if (currentSession) {
1317
+ await botManagement.markProviderIdle(botId, sessionId);
1331
1318
  }
1332
1319
  }
1333
1320
  }
@@ -1542,7 +1529,7 @@ function formatMissingEvidenceRetryPrompt(lastResponse, issue) {
1542
1529
  ].join("\n");
1543
1530
  }
1544
1531
  function isEmptyResponseError(message) {
1545
- return /empty response/i.test(message);
1532
+ return /empty response|failed without any output|without stdout\/stderr/i.test(message);
1546
1533
  }
1547
1534
  function looksLikeBlockedBody(text) {
1548
1535
  if (!text.trim()) {
package/dist/config.js CHANGED
@@ -135,12 +135,11 @@ export const config = {
135
135
  telegramPollingBackoffMaxMs: readTimeout("TELEGRAM_POLLING_BACKOFF_MAX_MS", 900_000),
136
136
  telegramPollingMaxConcurrency: readTimeout("TELEGRAM_POLLING_MAX_CONCURRENCY", 3),
137
137
  telegramSchedulerTickMs: readTimeout("TELEGRAM_SCHEDULER_TICK_MS", 1000),
138
- telegramTieredPollingMinBots: readTimeout("TELEGRAM_TIERED_POLLING_MIN_BOTS", 4),
138
+ telegramTieredPollingMinBots: readTimeout("TELEGRAM_TIERED_POLLING_MIN_BOTS", 5),
139
139
  telegramActivePollIntervalMs: readTimeout("TELEGRAM_ACTIVE_POLL_INTERVAL_MS", 3000),
140
- telegramIdlePollIntervalMs: readTimeout("TELEGRAM_IDLE_POLL_INTERVAL_MS", 30_000),
141
- telegramColdPollIntervalMs: readTimeout("TELEGRAM_COLD_POLL_INTERVAL_MS", 300_000),
142
- telegramActiveIdleMs: readTimeout("TELEGRAM_ACTIVE_IDLE_MS", 1_800_000),
143
- telegramColdIdleMs: readTimeout("TELEGRAM_COLD_IDLE_MS", 21_600_000),
140
+ telegramRunningPollIntervalMs: readTimeout("TELEGRAM_RUNNING_POLL_INTERVAL_MS", 60_000),
141
+ telegramSecondaryPollIntervalMs: readTimeout("TELEGRAM_SECONDARY_POLL_INTERVAL_MS", 60_000),
142
+ telegramTertiaryPollIntervalMs: readTimeout("TELEGRAM_TERTIARY_POLL_INTERVAL_MS", 180_000),
144
143
  telegramOwnerId: readOptional("TELEGRAM_OWNER_ID"),
145
144
  telegramMessageBatchMs: readNonNegativeTimeout("TELEGRAM_MESSAGE_BATCH_MS", 1500),
146
145
  telegramTypingIntervalMs: readTimeout("TELEGRAM_TYPING_INTERVAL_MS", 10_000),
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { BotManagementService } from "./services/bot-management-service.js";
15
15
  import { LocalUiService } from "./services/local-ui-service.js";
16
16
  import { AgentMemoryService } from "./services/agent-memory-service.js";
17
17
  import { BotPollingStateService } from "./services/bot-polling-state-service.js";
18
+ import { computePolicyPollIntervalMs, computeRecentMessageRanks } from "./services/polling-policy.js";
18
19
  import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
19
20
  import { setTelegramCommandMenu } from "./telegram-command-menu.js";
20
21
  const execFileAsync = promisify(execFile);
@@ -140,10 +141,6 @@ async function startManualPollingScheduler(bots) {
140
141
  const runtimeStates = new Map();
141
142
  const botIds = pollingBots.map((bot) => String(bot.botInfo.id));
142
143
  await botPollingState.prune(botIds);
143
- const explicitMainBotId = process.env.TELEGRAM_MAIN_BOT_ID?.trim();
144
- const mainBotId = explicitMainBotId && botIds.includes(explicitMainBotId)
145
- ? explicitMainBotId
146
- : botIds[0];
147
144
  for (const [index, bot] of pollingBots.entries()) {
148
145
  const botId = String(bot.botInfo.id);
149
146
  runtimeStates.set(botId, {
@@ -162,32 +159,37 @@ async function startManualPollingScheduler(bots) {
162
159
  while (true) {
163
160
  const now = Date.now();
164
161
  let activePolls = [...runtimeStates.values()].filter((state) => state.inFlight).length;
165
- for (const bot of pollingBots) {
166
- if (activePolls >= config.telegramPollingMaxConcurrency) {
167
- break;
168
- }
162
+ const pollingStates = await botPollingState.list();
163
+ const rankByBotId = computeRecentMessageRanks(botIds, pollingStates);
164
+ const dueBots = pollingBots
165
+ .map((bot) => {
169
166
  const botId = String(bot.botInfo.id);
170
167
  const runtime = runtimeStates.get(botId);
171
- if (!runtime || runtime.inFlight) {
172
- continue;
173
- }
174
- const state = await botPollingState.get(botId, bot.botInfo.username);
175
- const isMain = botId === mainBotId;
176
- if (!isMain && state.sleepMode === "deep") {
177
- continue;
178
- }
179
- const nextPollAt = state.nextPollAt ? Date.parse(state.nextPollAt) : 0;
180
- if (Number.isFinite(nextPollAt) && nextPollAt > now) {
181
- continue;
168
+ const state = pollingStates[botId];
169
+ const nextPollAt = state?.nextPollAt ? Date.parse(state.nextPollAt) : 0;
170
+ return {
171
+ bot,
172
+ botId,
173
+ runtime,
174
+ state,
175
+ rank: rankByBotId.get(botId) ?? pollingBots.length,
176
+ nextPollAt: Number.isFinite(nextPollAt) ? nextPollAt : 0,
177
+ };
178
+ })
179
+ .filter((entry) => entry.runtime && !entry.runtime.inFlight && entry.nextPollAt <= now)
180
+ .sort((left, right) => left.nextPollAt - right.nextPollAt || left.rank - right.rank);
181
+ for (const entry of dueBots) {
182
+ if (activePolls >= config.telegramPollingMaxConcurrency) {
183
+ break;
182
184
  }
183
- runtime.inFlight = true;
185
+ entry.runtime.inFlight = true;
184
186
  activePolls += 1;
185
- void pollTelegramBot(bot, runtime, {
186
- isMain,
187
+ void pollTelegramBot(entry.bot, entry.runtime, {
187
188
  totalBots: pollingBots.length,
188
- lastMessageAt: state.lastMessageAt,
189
+ botRank: entry.rank,
190
+ state: entry.state,
189
191
  }).finally(() => {
190
- runtime.inFlight = false;
192
+ entry.runtime.inFlight = false;
191
193
  });
192
194
  }
193
195
  await sleep(config.telegramSchedulerTickMs);
@@ -233,8 +235,20 @@ async function pollTelegramBot(pollingBot, runtime, options) {
233
235
  }
234
236
  runtime.offset = orderedUpdates[orderedUpdates.length - 1].update_id + 1;
235
237
  }
236
- const lastMessageAt = orderedUpdates.some(hasMessagePayload) ? new Date(now).toISOString() : options.lastMessageAt;
237
- const nextPollAt = now + computePolicyPollIntervalMs(options.isMain, options.totalBots, lastMessageAt, now);
238
+ const receivedMessage = orderedUpdates.some(hasMessagePayload);
239
+ const lastMessageAt = receivedMessage ? new Date(now).toISOString() : options.state?.lastMessageAt;
240
+ const nextPollAt = now + computePolicyPollIntervalMs(options.totalBots, receivedMessage ? 1 : options.botRank, {
241
+ ...options.state,
242
+ botId,
243
+ consecutiveFailures: options.state?.consecutiveFailures ?? runtime.consecutiveFailures,
244
+ lastMessageAt,
245
+ }, {
246
+ tieredPollingMinBots: config.telegramTieredPollingMinBots,
247
+ activePollIntervalMs: config.telegramActivePollIntervalMs,
248
+ runningPollIntervalMs: config.telegramRunningPollIntervalMs,
249
+ secondaryPollIntervalMs: config.telegramSecondaryPollIntervalMs,
250
+ tertiaryPollIntervalMs: config.telegramTertiaryPollIntervalMs,
251
+ });
238
252
  await botPollingState.recordPoll(botId, {
239
253
  username: pollingBot.botInfo.username,
240
254
  lastPollAt: new Date(now).toISOString(),
@@ -271,22 +285,6 @@ async function pollTelegramBot(pollingBot, runtime, options) {
271
285
  });
272
286
  }
273
287
  }
274
- function computePolicyPollIntervalMs(isMain, totalBots, lastMessageAt, now) {
275
- if (isMain || totalBots < config.telegramTieredPollingMinBots) {
276
- return config.telegramActivePollIntervalMs;
277
- }
278
- const lastMessageTime = lastMessageAt ? Date.parse(lastMessageAt) : undefined;
279
- const idleMs = lastMessageTime && Number.isFinite(lastMessageTime)
280
- ? now - lastMessageTime
281
- : Number.POSITIVE_INFINITY;
282
- if (idleMs <= config.telegramActiveIdleMs) {
283
- return config.telegramActivePollIntervalMs;
284
- }
285
- if (idleMs <= config.telegramColdIdleMs) {
286
- return config.telegramIdlePollIntervalMs;
287
- }
288
- return config.telegramColdPollIntervalMs;
289
- }
290
288
  function hasMessagePayload(update) {
291
289
  return Boolean(update.message || update.edited_message || update.channel_post);
292
290
  }