appback-remoteagent 0.13.21 → 0.14.0
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 +19 -28
- 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 +34 -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/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,6 +159,8 @@ 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;
|
|
162
|
+
const pollingStates = await botPollingState.list();
|
|
163
|
+
const rankByBotId = computeRecentMessageRanks(botIds, pollingStates);
|
|
165
164
|
for (const bot of pollingBots) {
|
|
166
165
|
if (activePolls >= config.telegramPollingMaxConcurrency) {
|
|
167
166
|
break;
|
|
@@ -172,10 +171,6 @@ async function startManualPollingScheduler(bots) {
|
|
|
172
171
|
continue;
|
|
173
172
|
}
|
|
174
173
|
const state = await botPollingState.get(botId, bot.botInfo.username);
|
|
175
|
-
const isMain = botId === mainBotId;
|
|
176
|
-
if (!isMain && state.sleepMode === "deep") {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
174
|
const nextPollAt = state.nextPollAt ? Date.parse(state.nextPollAt) : 0;
|
|
180
175
|
if (Number.isFinite(nextPollAt) && nextPollAt > now) {
|
|
181
176
|
continue;
|
|
@@ -183,9 +178,9 @@ async function startManualPollingScheduler(bots) {
|
|
|
183
178
|
runtime.inFlight = true;
|
|
184
179
|
activePolls += 1;
|
|
185
180
|
void pollTelegramBot(bot, runtime, {
|
|
186
|
-
isMain,
|
|
187
181
|
totalBots: pollingBots.length,
|
|
188
|
-
|
|
182
|
+
botRank: rankByBotId.get(botId) ?? pollingBots.length,
|
|
183
|
+
state,
|
|
189
184
|
}).finally(() => {
|
|
190
185
|
runtime.inFlight = false;
|
|
191
186
|
});
|
|
@@ -233,8 +228,20 @@ async function pollTelegramBot(pollingBot, runtime, options) {
|
|
|
233
228
|
}
|
|
234
229
|
runtime.offset = orderedUpdates[orderedUpdates.length - 1].update_id + 1;
|
|
235
230
|
}
|
|
236
|
-
const
|
|
237
|
-
const
|
|
231
|
+
const receivedMessage = orderedUpdates.some(hasMessagePayload);
|
|
232
|
+
const lastMessageAt = receivedMessage ? new Date(now).toISOString() : options.state?.lastMessageAt;
|
|
233
|
+
const nextPollAt = now + computePolicyPollIntervalMs(options.totalBots, receivedMessage ? 1 : options.botRank, {
|
|
234
|
+
...options.state,
|
|
235
|
+
botId,
|
|
236
|
+
consecutiveFailures: options.state?.consecutiveFailures ?? runtime.consecutiveFailures,
|
|
237
|
+
lastMessageAt,
|
|
238
|
+
}, {
|
|
239
|
+
tieredPollingMinBots: config.telegramTieredPollingMinBots,
|
|
240
|
+
activePollIntervalMs: config.telegramActivePollIntervalMs,
|
|
241
|
+
runningPollIntervalMs: config.telegramRunningPollIntervalMs,
|
|
242
|
+
secondaryPollIntervalMs: config.telegramSecondaryPollIntervalMs,
|
|
243
|
+
tertiaryPollIntervalMs: config.telegramTertiaryPollIntervalMs,
|
|
244
|
+
});
|
|
238
245
|
await botPollingState.recordPoll(botId, {
|
|
239
246
|
username: pollingBot.botInfo.username,
|
|
240
247
|
lastPollAt: new Date(now).toISOString(),
|
|
@@ -271,22 +278,6 @@ async function pollTelegramBot(pollingBot, runtime, options) {
|
|
|
271
278
|
});
|
|
272
279
|
}
|
|
273
280
|
}
|
|
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
281
|
function hasMessagePayload(update) {
|
|
291
282
|
return Boolean(update.message || update.edited_message || update.channel_post);
|
|
292
283
|
}
|
|
@@ -29,57 +29,30 @@ export class BotManagementService {
|
|
|
29
29
|
if (bots.length === 0) {
|
|
30
30
|
return "No Telegram bots are configured.";
|
|
31
31
|
}
|
|
32
|
-
return this.formatBots(bots,
|
|
32
|
+
return this.formatBots(bots, await this.pollingState.list());
|
|
33
33
|
}
|
|
34
34
|
async formatCurrentBotSummary(currentBotId) {
|
|
35
35
|
const env = await this.readEnvConfig();
|
|
36
36
|
const bots = this.zipBots(env.tokens, env.usernames);
|
|
37
37
|
const current = this.resolveBotSelector(bots, currentBotId);
|
|
38
|
-
const main = this.resolveMainBot(bots, env.mainBotId);
|
|
39
38
|
const currentLabel = current ? `@${current.username} (${current.id})` : currentBotId;
|
|
40
|
-
const mainLabel = main ? `@${main.username} (${main.id})` : "not configured";
|
|
41
|
-
const role = current && main && current.id === main.id ? "main" : "sub";
|
|
42
39
|
const states = await this.pollingState.list();
|
|
43
40
|
const currentState = current ? states[String(current.id)] : undefined;
|
|
44
41
|
return [
|
|
45
|
-
`bot: ${currentLabel}
|
|
46
|
-
`mainBot: ${mainLabel}`,
|
|
42
|
+
`bot: ${currentLabel}`,
|
|
47
43
|
`botCount: ${bots.length}`,
|
|
48
|
-
`
|
|
44
|
+
`pollingState: ${this.pollingState.formatMode(currentState)}`,
|
|
49
45
|
].join("\n");
|
|
50
46
|
}
|
|
51
|
-
async
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
throw new Error("No Telegram bots are configured.");
|
|
56
|
-
}
|
|
57
|
-
const mainBotId = this.resolveMainBotId(bots, env.mainBotId);
|
|
58
|
-
const target = selector.trim()
|
|
59
|
-
? this.resolveBotSelector(bots, selector)
|
|
60
|
-
: this.resolveBotSelector(bots, currentBotId);
|
|
61
|
-
if (!target) {
|
|
62
|
-
throw new Error("Usage: /sleep <bot>");
|
|
63
|
-
}
|
|
64
|
-
if (String(target.id) === mainBotId) {
|
|
65
|
-
throw new Error(`@${target.username} is the main bot and cannot enter deep sleep.`);
|
|
66
|
-
}
|
|
67
|
-
await this.pollingState.setSleepMode(String(target.id), "deep", target.username);
|
|
68
|
-
return {
|
|
69
|
-
message: `Set @${target.username} to deep sleep.\n\nIt remains configured. RemoteAgent will stop polling it until /wake is used from an awake bot.`,
|
|
70
|
-
};
|
|
47
|
+
async markProviderRunning(botId, sessionId) {
|
|
48
|
+
const bot = await this.findConfiguredBot(botId);
|
|
49
|
+
const pollingBotId = bot ? String(bot.id) : botId;
|
|
50
|
+
await this.pollingState.markRunning(pollingBotId, sessionId, bot?.username);
|
|
71
51
|
}
|
|
72
|
-
async
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (!target) {
|
|
77
|
-
throw new Error("Usage: /wake <bot>");
|
|
78
|
-
}
|
|
79
|
-
await this.pollingState.setSleepMode(String(target.id), "awake", target.username);
|
|
80
|
-
return {
|
|
81
|
-
message: `Woke @${target.username}. It will be polled again.`,
|
|
82
|
-
};
|
|
52
|
+
async markProviderIdle(botId, sessionId) {
|
|
53
|
+
const bot = await this.findConfiguredBot(botId);
|
|
54
|
+
const pollingBotId = bot ? String(bot.id) : botId;
|
|
55
|
+
await this.pollingState.markIdle(pollingBotId, sessionId, bot?.username);
|
|
83
56
|
}
|
|
84
57
|
async getPendingOperationNotice() {
|
|
85
58
|
const pending = await this.readPendingOperation();
|
|
@@ -109,7 +82,6 @@ export class BotManagementService {
|
|
|
109
82
|
}
|
|
110
83
|
const tokens = [...env.tokens, trimmed];
|
|
111
84
|
const usernames = [...env.usernames, target.username];
|
|
112
|
-
const mainBotId = this.resolveMainBotId(this.zipBots(tokens, usernames), env.mainBotId);
|
|
113
85
|
const backupEnvPath = await this.backupEnv();
|
|
114
86
|
const pending = {
|
|
115
87
|
version: 1,
|
|
@@ -128,7 +100,7 @@ export class BotManagementService {
|
|
|
128
100
|
};
|
|
129
101
|
try {
|
|
130
102
|
await this.writePendingOperation(pending);
|
|
131
|
-
await this.writeEnvConfig(env.lines, tokens, usernames
|
|
103
|
+
await this.writeEnvConfig(env.lines, tokens, usernames);
|
|
132
104
|
await this.launchRestartJob();
|
|
133
105
|
}
|
|
134
106
|
catch (error) {
|
|
@@ -162,10 +134,6 @@ export class BotManagementService {
|
|
|
162
134
|
const remainingBots = bots.filter((value) => value.token !== target.token);
|
|
163
135
|
const tokens = remainingBots.map((value) => value.token);
|
|
164
136
|
const usernames = remainingBots.map((value) => value.username);
|
|
165
|
-
const currentMain = this.resolveMainBot(bots, env.mainBotId);
|
|
166
|
-
const nextMainBotId = currentMain?.token === target.token
|
|
167
|
-
? this.promoteMainBotAfterRemoval(bots, target)?.id.toString()
|
|
168
|
-
: this.resolveMainBotId(remainingBots, env.mainBotId);
|
|
169
137
|
const replyToken = tokens.includes(sourceBotToken) ? sourceBotToken : tokens[0];
|
|
170
138
|
const notifyViaUsername = remainingBots.find((bot) => bot.token === replyToken)?.username;
|
|
171
139
|
const backupEnvPath = await this.backupEnv();
|
|
@@ -186,7 +154,7 @@ export class BotManagementService {
|
|
|
186
154
|
};
|
|
187
155
|
try {
|
|
188
156
|
await this.writePendingOperation(pending);
|
|
189
|
-
await this.writeEnvConfig(env.lines, tokens, usernames
|
|
157
|
+
await this.writeEnvConfig(env.lines, tokens, usernames);
|
|
190
158
|
await this.launchRestartJob();
|
|
191
159
|
}
|
|
192
160
|
catch (error) {
|
|
@@ -194,43 +162,16 @@ export class BotManagementService {
|
|
|
194
162
|
await this.clearPendingOperation().catch(() => undefined);
|
|
195
163
|
throw error;
|
|
196
164
|
}
|
|
197
|
-
const promoted = nextMainBotId ? remainingBots.find((bot) => String(bot.id) === nextMainBotId) : undefined;
|
|
198
|
-
const mainLine = promoted && currentMain?.token === target.token
|
|
199
|
-
? `Main bot will be promoted to @${promoted.username} (${promoted.id}).`
|
|
200
|
-
: undefined;
|
|
201
165
|
const notifyLine = replyToken === sourceBotToken || !notifyViaUsername
|
|
202
166
|
? "The runtime will restart once and then report the result here."
|
|
203
167
|
: `The runtime will restart once and then report the result through @${notifyViaUsername}.`;
|
|
204
168
|
return {
|
|
205
169
|
message: [
|
|
206
170
|
`Applying bot removal for @${target.username} (${target.id}).`,
|
|
207
|
-
mainLine,
|
|
208
171
|
notifyLine,
|
|
209
172
|
].filter(Boolean).join("\n\n"),
|
|
210
173
|
};
|
|
211
174
|
}
|
|
212
|
-
async setMainBot(selector) {
|
|
213
|
-
await this.ensureSupported();
|
|
214
|
-
await this.assertNoPendingOperation();
|
|
215
|
-
const trimmed = selector.trim();
|
|
216
|
-
if (!trimmed) {
|
|
217
|
-
throw new Error("Usage: /bot main <number|@username|id>");
|
|
218
|
-
}
|
|
219
|
-
const env = await this.readEnvConfig();
|
|
220
|
-
const bots = this.zipBots(env.tokens, env.usernames);
|
|
221
|
-
const target = this.resolveBotSelector(bots, trimmed);
|
|
222
|
-
if (!target) {
|
|
223
|
-
throw new Error(`Bot was not found: ${trimmed}`);
|
|
224
|
-
}
|
|
225
|
-
await this.writeEnvConfig(env.lines, env.tokens, env.usernames, String(target.id));
|
|
226
|
-
return {
|
|
227
|
-
message: [
|
|
228
|
-
`Set main bot to @${target.username} (${target.id}).`,
|
|
229
|
-
"",
|
|
230
|
-
this.formatBots(bots, String(target.id)),
|
|
231
|
-
].join("\n"),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
175
|
async doctorBots(sourceBotId, sourceBotToken, chatId) {
|
|
235
176
|
await this.ensureSupported();
|
|
236
177
|
await this.assertNoPendingOperation();
|
|
@@ -273,7 +214,6 @@ export class BotManagementService {
|
|
|
273
214
|
}
|
|
274
215
|
const tokens = alive.map((bot) => bot.token);
|
|
275
216
|
const usernames = alive.map((bot) => bot.username);
|
|
276
|
-
const mainBotId = this.resolveMainBotId(alive, env.mainBotId);
|
|
277
217
|
const replyToken = tokens.includes(sourceBotToken) ? sourceBotToken : tokens[0];
|
|
278
218
|
const notifyViaUsername = alive.find((bot) => bot.token === replyToken)?.username;
|
|
279
219
|
const backupEnvPath = await this.backupEnv();
|
|
@@ -294,7 +234,7 @@ export class BotManagementService {
|
|
|
294
234
|
};
|
|
295
235
|
try {
|
|
296
236
|
await this.writePendingOperation(pending);
|
|
297
|
-
await this.writeEnvConfig(env.lines, tokens, usernames
|
|
237
|
+
await this.writeEnvConfig(env.lines, tokens, usernames);
|
|
298
238
|
await this.launchRestartJob();
|
|
299
239
|
}
|
|
300
240
|
catch (error) {
|
|
@@ -345,9 +285,8 @@ export class BotManagementService {
|
|
|
345
285
|
}
|
|
346
286
|
const env = await this.readEnvConfig().catch(() => undefined);
|
|
347
287
|
const bots = env ? this.zipBots(env.tokens, env.usernames) : [];
|
|
348
|
-
const mainBotId = this.resolveMainBotId(bots, env?.mainBotId);
|
|
349
288
|
const listLines = bots.length > 0
|
|
350
|
-
? this.formatBotListLines(bots
|
|
289
|
+
? this.formatBotListLines(bots).map((line) => `- ${line.replace(/^\d+\.\s*/, "")}`)
|
|
351
290
|
: ["- none"];
|
|
352
291
|
const lines = pending.status === "rolled_back"
|
|
353
292
|
? [
|
|
@@ -485,7 +424,6 @@ export class BotManagementService {
|
|
|
485
424
|
const tokenLine = lines.find((line) => line.startsWith("TELEGRAM_BOT_TOKENS="));
|
|
486
425
|
const singleTokenLine = lines.find((line) => line.startsWith("TELEGRAM_BOT_TOKEN="));
|
|
487
426
|
const usernameLine = lines.find((line) => line.startsWith("TELEGRAM_BOT_USERNAMES="));
|
|
488
|
-
const mainBotLine = lines.find((line) => line.startsWith("TELEGRAM_MAIN_BOT_ID="));
|
|
489
427
|
const tokens = tokenLine
|
|
490
428
|
? this.parseCsv(tokenLine.slice("TELEGRAM_BOT_TOKENS=".length))
|
|
491
429
|
: singleTokenLine
|
|
@@ -497,17 +435,14 @@ export class BotManagementService {
|
|
|
497
435
|
lines,
|
|
498
436
|
tokens,
|
|
499
437
|
usernames,
|
|
500
|
-
mainBotId: mainBotLine?.slice("TELEGRAM_MAIN_BOT_ID=".length).trim() || undefined,
|
|
501
438
|
};
|
|
502
439
|
}
|
|
503
|
-
async writeEnvConfig(originalLines, tokens, usernames
|
|
440
|
+
async writeEnvConfig(originalLines, tokens, usernames) {
|
|
504
441
|
const normalizedUsernames = await this.normalizeUsernamesFromTelegram(tokens, usernames);
|
|
505
|
-
const normalizedMainBotId = this.resolveMainBotId(this.zipBots(tokens, normalizedUsernames), mainBotId);
|
|
506
442
|
const nextLines = [];
|
|
507
443
|
let hasMulti = false;
|
|
508
444
|
let hasSingle = false;
|
|
509
445
|
let hasUsernames = false;
|
|
510
|
-
let hasMainBot = false;
|
|
511
446
|
for (const line of originalLines) {
|
|
512
447
|
if (line.startsWith("TELEGRAM_BOT_TOKENS=")) {
|
|
513
448
|
nextLines.push(`TELEGRAM_BOT_TOKENS=${tokens.join(",")}`);
|
|
@@ -525,10 +460,6 @@ export class BotManagementService {
|
|
|
525
460
|
continue;
|
|
526
461
|
}
|
|
527
462
|
if (line.startsWith("TELEGRAM_MAIN_BOT_ID=")) {
|
|
528
|
-
if (normalizedMainBotId) {
|
|
529
|
-
nextLines.push(`TELEGRAM_MAIN_BOT_ID=${normalizedMainBotId}`);
|
|
530
|
-
}
|
|
531
|
-
hasMainBot = true;
|
|
532
463
|
continue;
|
|
533
464
|
}
|
|
534
465
|
nextLines.push(line);
|
|
@@ -542,9 +473,6 @@ export class BotManagementService {
|
|
|
542
473
|
if (!hasUsernames) {
|
|
543
474
|
nextLines.unshift(`TELEGRAM_BOT_USERNAMES=${normalizedUsernames.join(",")}`);
|
|
544
475
|
}
|
|
545
|
-
if (!hasMainBot && normalizedMainBotId) {
|
|
546
|
-
nextLines.unshift(`TELEGRAM_MAIN_BOT_ID=${normalizedMainBotId}`);
|
|
547
|
-
}
|
|
548
476
|
const output = `${nextLines.filter((line, index, all) => !(index === all.length - 1 && line === "")).join("\n")}\n`;
|
|
549
477
|
await fs.writeFile(this.envPath, output, "utf8");
|
|
550
478
|
}
|
|
@@ -589,27 +517,24 @@ export class BotManagementService {
|
|
|
589
517
|
|| String(bot.id) === withoutAt
|
|
590
518
|
|| bot.token === normalized);
|
|
591
519
|
}
|
|
592
|
-
formatBots(bots,
|
|
520
|
+
formatBots(bots, states = {}) {
|
|
593
521
|
if (bots.length === 0) {
|
|
594
522
|
return "No Telegram bots are configured.";
|
|
595
523
|
}
|
|
596
524
|
return [
|
|
597
525
|
`Configured bots (${bots.length})`,
|
|
598
|
-
...this.formatBotListLines(bots,
|
|
526
|
+
...this.formatBotListLines(bots, states),
|
|
599
527
|
].join("\n");
|
|
600
528
|
}
|
|
601
|
-
formatBotListLines(bots,
|
|
602
|
-
const mainBotId = this.resolveMainBotId(bots, explicitMainBotId);
|
|
529
|
+
formatBotListLines(bots, states = {}) {
|
|
603
530
|
return bots.map((bot, index) => {
|
|
604
531
|
const botId = String(bot.id);
|
|
605
|
-
const isMain = botId === mainBotId;
|
|
606
532
|
const state = states[botId];
|
|
607
533
|
const details = [
|
|
608
|
-
|
|
609
|
-
this.pollingState.formatMode(botId, isMain, state),
|
|
534
|
+
this.pollingState.formatMode(state),
|
|
610
535
|
state?.lastMessageAt ? `lastMessage=${this.formatAge(state.lastMessageAt)}` : undefined,
|
|
611
536
|
state?.lastPollAt ? `lastPoll=${this.formatAge(state.lastPollAt)}` : undefined,
|
|
612
|
-
state?.nextPollAt
|
|
537
|
+
state?.nextPollAt ? `nextPoll=${this.formatAge(state.nextPollAt)}` : undefined,
|
|
613
538
|
state?.consecutiveFailures ? `failures=${state.consecutiveFailures}` : undefined,
|
|
614
539
|
].filter(Boolean).join(" ");
|
|
615
540
|
return `${index + 1}. @${bot.username} (${bot.id}) ${details}`;
|
|
@@ -634,24 +559,13 @@ export class BotManagementService {
|
|
|
634
559
|
}
|
|
635
560
|
return `${Math.round(absMs / 86_400_000)}d ${suffix}`;
|
|
636
561
|
}
|
|
637
|
-
resolveMainBot(bots, mainBotId) {
|
|
638
|
-
if (bots.length === 0) {
|
|
639
|
-
return undefined;
|
|
640
|
-
}
|
|
641
|
-
const explicit = mainBotId ? bots.find((bot) => String(bot.id) === mainBotId) : undefined;
|
|
642
|
-
return explicit ?? bots[0];
|
|
643
|
-
}
|
|
644
|
-
resolveMainBotId(bots, mainBotId) {
|
|
645
|
-
return this.resolveMainBot(bots, mainBotId)?.id.toString();
|
|
646
|
-
}
|
|
647
|
-
promoteMainBotAfterRemoval(bots, removed) {
|
|
648
|
-
const remaining = bots.filter((bot) => bot.token !== removed.token);
|
|
649
|
-
return remaining[removed.index] ?? remaining[0];
|
|
650
|
-
}
|
|
651
562
|
async listConfiguredBots() {
|
|
652
563
|
const env = await this.readEnvConfig();
|
|
653
564
|
return this.zipBots(env.tokens, env.usernames);
|
|
654
565
|
}
|
|
566
|
+
async findConfiguredBot(botId) {
|
|
567
|
+
return this.resolveBotSelector(await this.listConfiguredBots(), botId);
|
|
568
|
+
}
|
|
655
569
|
tokenId(token) {
|
|
656
570
|
return Number.parseInt(token.split(":", 1)[0] ?? "0", 10);
|
|
657
571
|
}
|
|
@@ -19,7 +19,6 @@ export class BotPollingStateService {
|
|
|
19
19
|
const current = state.bots[botId] ?? {
|
|
20
20
|
botId,
|
|
21
21
|
username,
|
|
22
|
-
sleepMode: "awake",
|
|
23
22
|
consecutiveFailures: 0,
|
|
24
23
|
};
|
|
25
24
|
if (username && current.username !== username) {
|
|
@@ -32,30 +31,49 @@ export class BotPollingStateService {
|
|
|
32
31
|
async list() {
|
|
33
32
|
return { ...(await this.read()).bots };
|
|
34
33
|
}
|
|
35
|
-
async
|
|
34
|
+
async markRunning(botId, sessionId, username) {
|
|
36
35
|
const state = await this.read();
|
|
37
36
|
const current = state.bots[botId] ?? {
|
|
38
37
|
botId,
|
|
39
38
|
username,
|
|
40
|
-
sleepMode,
|
|
41
39
|
consecutiveFailures: 0,
|
|
42
40
|
};
|
|
43
|
-
current.sleepMode = sleepMode;
|
|
44
41
|
if (username) {
|
|
45
42
|
current.username = username;
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
const sessions = new Set(current.runningSessionIds ?? []);
|
|
45
|
+
sessions.add(sessionId || "__unknown__");
|
|
46
|
+
current.runningSessionIds = [...sessions];
|
|
47
|
+
current.lastProviderStartedAt = new Date().toISOString();
|
|
48
|
+
state.bots[botId] = current;
|
|
49
|
+
await this.writeNow();
|
|
50
|
+
}
|
|
51
|
+
async markIdle(botId, sessionId, username) {
|
|
52
|
+
const state = await this.read();
|
|
53
|
+
const current = state.bots[botId] ?? {
|
|
54
|
+
botId,
|
|
55
|
+
username,
|
|
56
|
+
consecutiveFailures: 0,
|
|
57
|
+
};
|
|
58
|
+
if (username) {
|
|
59
|
+
current.username = username;
|
|
60
|
+
}
|
|
61
|
+
const runningSessionIds = current.runningSessionIds ?? [];
|
|
62
|
+
if (runningSessionIds.length > 0) {
|
|
63
|
+
const key = sessionId || "__unknown__";
|
|
64
|
+
current.runningSessionIds = runningSessionIds.filter((value) => value !== key);
|
|
65
|
+
}
|
|
66
|
+
if (!current.runningSessionIds || current.runningSessionIds.length === 0) {
|
|
67
|
+
delete current.runningSessionIds;
|
|
68
|
+
current.lastProviderFinishedAt = new Date().toISOString();
|
|
49
69
|
}
|
|
50
70
|
state.bots[botId] = current;
|
|
51
71
|
await this.writeNow();
|
|
52
|
-
return current;
|
|
53
72
|
}
|
|
54
73
|
async recordPoll(botId, patch) {
|
|
55
74
|
const state = await this.read();
|
|
56
75
|
const current = state.bots[botId] ?? {
|
|
57
76
|
botId,
|
|
58
|
-
sleepMode: "awake",
|
|
59
77
|
consecutiveFailures: 0,
|
|
60
78
|
};
|
|
61
79
|
state.bots[botId] = {
|
|
@@ -79,11 +97,8 @@ export class BotPollingStateService {
|
|
|
79
97
|
await this.writeNow();
|
|
80
98
|
}
|
|
81
99
|
}
|
|
82
|
-
formatMode(
|
|
83
|
-
|
|
84
|
-
return "awake";
|
|
85
|
-
}
|
|
86
|
-
return state?.sleepMode === "deep" ? "deep sleep" : "awake";
|
|
100
|
+
formatMode(state) {
|
|
101
|
+
return state?.runningSessionIds && state.runningSessionIds.length > 0 ? "running" : "idle";
|
|
87
102
|
}
|
|
88
103
|
async flush() {
|
|
89
104
|
if (this.pendingWrite) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function computeRecentMessageRanks(botIds, states) {
|
|
2
|
+
const ranked = botIds
|
|
3
|
+
.map((botId) => ({
|
|
4
|
+
botId,
|
|
5
|
+
timestamp: parseStateTime(states[botId]?.lastMessageAt)
|
|
6
|
+
?? parseStateTime(states[botId]?.lastUpdateAt)
|
|
7
|
+
?? parseStateTime(states[botId]?.lastPollAt)
|
|
8
|
+
?? 0,
|
|
9
|
+
}))
|
|
10
|
+
.sort((left, right) => right.timestamp - left.timestamp);
|
|
11
|
+
return new Map(ranked.map((entry, index) => [entry.botId, index + 1]));
|
|
12
|
+
}
|
|
13
|
+
export function computePolicyPollIntervalMs(totalBots, botRank, state, policy) {
|
|
14
|
+
if (state?.runningSessionIds && state.runningSessionIds.length > 0) {
|
|
15
|
+
return policy.runningPollIntervalMs;
|
|
16
|
+
}
|
|
17
|
+
if (totalBots < policy.tieredPollingMinBots) {
|
|
18
|
+
return policy.activePollIntervalMs;
|
|
19
|
+
}
|
|
20
|
+
if (botRank <= 4) {
|
|
21
|
+
return policy.activePollIntervalMs;
|
|
22
|
+
}
|
|
23
|
+
if (botRank <= 8) {
|
|
24
|
+
return policy.secondaryPollIntervalMs;
|
|
25
|
+
}
|
|
26
|
+
return policy.tertiaryPollIntervalMs;
|
|
27
|
+
}
|
|
28
|
+
function parseStateTime(value) {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const timestamp = Date.parse(value);
|
|
33
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
34
|
+
}
|
|
@@ -13,8 +13,6 @@ export const TELEGRAM_COMMAND_MENU = [
|
|
|
13
13
|
{ command: "batch", description: "Collect and send a multi-message batch" },
|
|
14
14
|
{ command: "bots", description: "List configured Telegram bots" },
|
|
15
15
|
{ command: "bot", description: "Manage Telegram bots" },
|
|
16
|
-
{ command: "sleep", description: "Deep sleep a sub bot" },
|
|
17
|
-
{ command: "wake", description: "Wake a sleeping sub bot" },
|
|
18
16
|
{ command: "install", description: "Install or update Codex or Claude" },
|
|
19
17
|
{ command: "login", description: "Run provider login flow" },
|
|
20
18
|
{ command: "reset", description: "Clear this chat binding" },
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Telegram bot polling policy
|
|
2
|
+
|
|
3
|
+
RemoteAgent can run multiple Telegram bots in one runtime. Polling is the loop that asks Telegram whether each bot has new updates.
|
|
4
|
+
|
|
5
|
+
## Policy
|
|
6
|
+
|
|
7
|
+
When 4 or fewer bots are configured:
|
|
8
|
+
|
|
9
|
+
- every bot polls every 3 seconds
|
|
10
|
+
|
|
11
|
+
When 5 or more bots are configured:
|
|
12
|
+
|
|
13
|
+
- the 4 most recently messaged idle bots poll every 3 seconds
|
|
14
|
+
- the next 4 idle bots poll every 60 seconds
|
|
15
|
+
- all remaining idle bots poll every 180 seconds
|
|
16
|
+
|
|
17
|
+
When a bot has active provider work:
|
|
18
|
+
|
|
19
|
+
- that bot polls every 60 seconds
|
|
20
|
+
- `REPORT:progress` keeps it running
|
|
21
|
+
- `REPORT:result`, `REPORT:blocked`, timeout, fatal error, or `/stop` completion returns it to idle
|
|
22
|
+
|
|
23
|
+
## Removed concepts
|
|
24
|
+
|
|
25
|
+
RemoteAgent no longer uses deep sleep, wake, or a special main bot for polling control. All configured bots remain reachable through polling, just at different intervals.
|
|
26
|
+
|
|
27
|
+
## Operational notes
|
|
28
|
+
|
|
29
|
+
- `/bots` shows each configured bot and its polling state.
|
|
30
|
+
- `/bot doctor` removes bots that Telegram reports as permanently dead.
|
|
31
|
+
- `/bot remove <username|id>` removes a configured bot from the runtime.
|
|
32
|
+
- Polling state is stored by Telegram bot id, not by display order.
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -35,9 +35,9 @@ Current production bot ownership is intentionally split:
|
|
|
35
35
|
Do not run the same Telegram bot token from multiple runtimes at the same time.
|
|
36
36
|
Bot polling conflicts are treated as incidents, not harmless warnings.
|
|
37
37
|
|
|
38
|
-
When a runtime has
|
|
39
|
-
|
|
40
|
-
See [
|
|
38
|
+
When a runtime has several configured Telegram bots, polling pressure can become operationally visible.
|
|
39
|
+
RemoteAgent reduces that pressure with rank-based polling intervals instead of deep sleep or a special main bot.
|
|
40
|
+
See [BOT_POLLING_POLICY.md](./BOT_POLLING_POLICY.md).
|
|
41
41
|
|
|
42
42
|
## Workspace policy
|
|
43
43
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Telegram polling policy TODO
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Simplify multi-bot polling so RemoteAgent can keep many Telegram bots configured without deep sleep, wake commands, or a special main bot.
|
|
6
|
+
|
|
7
|
+
## Required policy
|
|
8
|
+
|
|
9
|
+
- If configured bot count is 4 or less, every bot polls every 3 seconds.
|
|
10
|
+
- If configured bot count is 5 or more:
|
|
11
|
+
- the 4 most recently messaged idle bots poll every 3 seconds
|
|
12
|
+
- the next 4 idle bots poll every 60 seconds
|
|
13
|
+
- all remaining idle bots poll every 180 seconds
|
|
14
|
+
- A bot with active provider work polls every 60 seconds.
|
|
15
|
+
- `REPORT:progress` keeps the bot in running state.
|
|
16
|
+
- Running state ends only after final result, blocked result, fatal error, timeout, or `/stop` completion.
|
|
17
|
+
|
|
18
|
+
## Removed concepts
|
|
19
|
+
|
|
20
|
+
- No deep sleep state.
|
|
21
|
+
- No wake command.
|
|
22
|
+
- No main bot requirement.
|
|
23
|
+
- No separate sleep registry.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. Remove user-facing `/sleep`, `/wake`, and `/bot main` commands from help and command menu.
|
|
28
|
+
2. Remove main/deep-sleep logic from bot management output and environment writing.
|
|
29
|
+
3. Track bot provider activity as idle/running in the polling state file.
|
|
30
|
+
4. Replace idle-threshold polling with rank-based polling.
|
|
31
|
+
5. Update README, operations docs, and `.env.example`.
|
|
32
|
+
6. Run local checks only. Do not deploy to server 30 until explicitly requested.
|
|
33
|
+
|
|
34
|
+
## Local validation
|
|
35
|
+
|
|
36
|
+
- `npm run check`
|
|
37
|
+
- `npm run build`
|
|
38
|
+
- Confirm `/bots` output no longer shows main/sub or deep sleep.
|
|
39
|
+
- Confirm rank-based interval calculation is covered by a local test or deterministic helper check.
|
|
40
|
+
|
|
41
|
+
## Deployment rule
|
|
42
|
+
|
|
43
|
+
Server 30 deployment is intentionally out of scope for this change until the operator explicitly requests it.
|
package/package.json
CHANGED
package/docs/BOT_SLEEP.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Telegram Bot Sleep
|
|
2
|
-
|
|
3
|
-
## Why this exists
|
|
4
|
-
|
|
5
|
-
RemoteAgent can run multiple Telegram bots in one runtime.
|
|
6
|
-
Each active bot needs Telegram polling.
|
|
7
|
-
|
|
8
|
-
Operationally, up to three active polling bots has been stable enough.
|
|
9
|
-
When more than three bots are active, polling pressure and Telegram transport errors become more likely.
|
|
10
|
-
|
|
11
|
-
Sleep exists to reduce polling load without deleting bot configuration.
|
|
12
|
-
|
|
13
|
-
## Definitions
|
|
14
|
-
|
|
15
|
-
- main bot: the control bot that stays reachable
|
|
16
|
-
- sub bot: any configured bot that is not the main bot
|
|
17
|
-
- awake: the bot is polled normally
|
|
18
|
-
- deep sleep: the bot is configured but not polled
|
|
19
|
-
|
|
20
|
-
Sleep is not removal.
|
|
21
|
-
Removing a bot deletes it from runtime configuration.
|
|
22
|
-
Sleeping a bot keeps the bot registered and preserves its sessions.
|
|
23
|
-
|
|
24
|
-
## Policy
|
|
25
|
-
|
|
26
|
-
1. The main bot must not enter deep sleep.
|
|
27
|
-
2. Sub bots may enter deep sleep.
|
|
28
|
-
3. The main bot can wake a sleeping sub bot.
|
|
29
|
-
4. Bot doctor and sleep are separate features.
|
|
30
|
-
5. Bot doctor may remove permanently invalid Telegram bots.
|
|
31
|
-
6. Sleep must never remove a bot token or session state.
|
|
32
|
-
7. User-facing list output should show bot role and sleep state.
|
|
33
|
-
|
|
34
|
-
## Polling Strategy
|
|
35
|
-
|
|
36
|
-
The intended strategy is tiered:
|
|
37
|
-
|
|
38
|
-
1. recently used bots poll normally
|
|
39
|
-
2. idle bots poll less often
|
|
40
|
-
3. manually sleeping bots do not poll
|
|
41
|
-
|
|
42
|
-
This lets frequently used agents stay responsive while long-idle agents stop consuming polling capacity.
|
|
43
|
-
|
|
44
|
-
## Commands
|
|
45
|
-
|
|
46
|
-
Command surface:
|
|
47
|
-
|
|
48
|
-
```text
|
|
49
|
-
/sleep <bot>
|
|
50
|
-
/wake <bot>
|
|
51
|
-
/bot main <bot>
|
|
52
|
-
/bots
|
|
53
|
-
/list
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Expected behavior:
|
|
57
|
-
|
|
58
|
-
- `/sleep <bot>` puts a sub bot into deep sleep
|
|
59
|
-
- `/sleep` without a target may sleep the current bot only when it is not the main bot
|
|
60
|
-
- `/wake <bot>` wakes a sleeping sub bot
|
|
61
|
-
- `/bots` shows main/sub and awake/sleeping state
|
|
62
|
-
- `/list` shows session state and bot state where relevant
|
|
63
|
-
|
|
64
|
-
## Non-goals
|
|
65
|
-
|
|
66
|
-
- Do not infer sleep from natural-language provider replies.
|
|
67
|
-
- Do not treat sleep as `/bot remove`.
|
|
68
|
-
- Do not let Codex or Claude decide which bot should be removed.
|
|
69
|
-
- Do not create a separate sleep-only bot registry that can drift from configured bots.
|
|
70
|
-
|
|
71
|
-
## Implementation Notes
|
|
72
|
-
|
|
73
|
-
Sleep state should be keyed by stable Telegram bot id, not by display index.
|
|
74
|
-
|
|
75
|
-
Display indexes are only for user convenience.
|
|
76
|
-
Internal actions must resolve to bot id.
|
|
77
|
-
|
|
78
|
-
The runtime should still keep one always-awake control path through the main bot.
|