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 +4 -6
- package/README.md +1 -4
- package/dist/adapters/codex-adapter.js +3 -3
- package/dist/bot.js +151 -164
- package/dist/config.js +4 -5
- package/dist/index.js +40 -42
- package/dist/services/bot-management-service.js +24 -110
- package/dist/services/bot-polling-state-service.js +28 -13
- package/dist/services/polling-policy.js +31 -0
- package/dist/telegram-command-menu.js +0 -2
- package/docs/BOT_POLLING_POLICY.md +32 -0
- package/docs/OPERATIONS.md +3 -3
- package/docs/POLLING_POLICY_TODO.md +43 -0
- package/docs/RELEASING.md +54 -7
- package/package.json +1 -1
- package/docs/BOT_SLEEP.md +0 -78
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=
|
|
11
|
+
TELEGRAM_TIERED_POLLING_MIN_BOTS=5
|
|
13
12
|
TELEGRAM_ACTIVE_POLL_INTERVAL_MS=3000
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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", "
|
|
705
|
-
await reply(ctx, "Usage: `/bot add <token>`, `/bot doctor`, `/bot
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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,
|
|
1233
|
+
await bridge.logSystem(botId, chatId, blockedMessage);
|
|
1220
1234
|
autoContinue.clear(botId, chatId, sessionId);
|
|
1221
|
-
return [
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
|
1249
|
+
return parsed.chunks;
|
|
1231
1250
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
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(
|
|
1255
|
+
prompt = appendManagedContext(formatUntaggedIntentRetryPrompt(parsed.chunks.join("\n")), managedContext);
|
|
1244
1256
|
continue;
|
|
1245
1257
|
}
|
|
1246
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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 [
|
|
1273
|
+
return [timeoutMessage];
|
|
1313
1274
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
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",
|
|
138
|
+
telegramTieredPollingMinBots: readTimeout("TELEGRAM_TIERED_POLLING_MIN_BOTS", 5),
|
|
139
139
|
telegramActivePollIntervalMs: readTimeout("TELEGRAM_ACTIVE_POLL_INTERVAL_MS", 3000),
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
const
|
|
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
|
}
|