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
|
@@ -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,31 @@
|
|
|
1
|
+
export function computeRecentMessageRanks(botIds, states) {
|
|
2
|
+
const ranked = botIds
|
|
3
|
+
.map((botId) => ({
|
|
4
|
+
botId,
|
|
5
|
+
timestamp: parseStateTime(states[botId]?.lastMessageAt) ?? 0,
|
|
6
|
+
}))
|
|
7
|
+
.sort((left, right) => right.timestamp - left.timestamp);
|
|
8
|
+
return new Map(ranked.map((entry, index) => [entry.botId, index + 1]));
|
|
9
|
+
}
|
|
10
|
+
export function computePolicyPollIntervalMs(totalBots, botRank, state, policy) {
|
|
11
|
+
if (state?.runningSessionIds && state.runningSessionIds.length > 0) {
|
|
12
|
+
return policy.runningPollIntervalMs;
|
|
13
|
+
}
|
|
14
|
+
if (totalBots < policy.tieredPollingMinBots) {
|
|
15
|
+
return policy.activePollIntervalMs;
|
|
16
|
+
}
|
|
17
|
+
if (botRank <= 4) {
|
|
18
|
+
return policy.activePollIntervalMs;
|
|
19
|
+
}
|
|
20
|
+
if (botRank <= 8) {
|
|
21
|
+
return policy.secondaryPollIntervalMs;
|
|
22
|
+
}
|
|
23
|
+
return policy.tertiaryPollIntervalMs;
|
|
24
|
+
}
|
|
25
|
+
function parseStateTime(value) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const timestamp = Date.parse(value);
|
|
30
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
31
|
+
}
|
|
@@ -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/docs/RELEASING.md
CHANGED
|
@@ -28,7 +28,7 @@ A release is only considered complete when all of the following are done in orde
|
|
|
28
28
|
4. Run `npm run build`
|
|
29
29
|
5. Commit the completed work
|
|
30
30
|
6. Push `main` to `origin/main`
|
|
31
|
-
7. Publish `appback-remoteagent@<version>` to npm
|
|
31
|
+
7. Publish `appback-remoteagent@<version>` to npm from server 30's npm credentials
|
|
32
32
|
8. Install that exact npm version on server 30 and server 26
|
|
33
33
|
9. Restart `remoteagent.service` on each target if runtime code changed
|
|
34
34
|
10. Verify the relevant runtime path, logs, or Telegram behavior on each target
|
|
@@ -64,18 +64,65 @@ git status
|
|
|
64
64
|
git add -A
|
|
65
65
|
git commit -m "Release 0.12.3"
|
|
66
66
|
git push origin main
|
|
67
|
-
npm
|
|
67
|
+
npm pack
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
## npm publish procedure
|
|
71
|
+
|
|
72
|
+
Use this package-specific path for `appback-remoteagent`.
|
|
73
|
+
|
|
74
|
+
Do not publish this package from machine 21 with the `21token`. That token is scoped for `@appbackhub` packages and cannot publish the unscoped `appback-remoteagent` package. It can authenticate but `npm publish` fails with:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
403 Forbidden - You may not perform that action with these credentials.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The successful publish path is:
|
|
71
81
|
|
|
72
82
|
```bash
|
|
73
|
-
VERSION=0.
|
|
83
|
+
VERSION=0.14.0
|
|
84
|
+
npm pack
|
|
85
|
+
scp appback-remoteagent-$VERSION.tgz au2223@192.168.0.30:/tmp/appback-remoteagent-$VERSION.tgz
|
|
86
|
+
ssh au2223@192.168.0.30 "bash -lc 'npm publish /tmp/appback-remoteagent-$VERSION.tgz'"
|
|
87
|
+
npm view appback-remoteagent version
|
|
88
|
+
ssh au2223@192.168.0.30 'bash -lc "npm view appback-remoteagent version"'
|
|
89
|
+
rm -f appback-remoteagent-$VERSION.tgz
|
|
90
|
+
ssh au2223@192.168.0.30 "rm -f /tmp/appback-remoteagent-$VERSION.tgz"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If registry caching briefly returns the previous version after publish, wait a few seconds and rerun `npm view appback-remoteagent version` before installing to servers.
|
|
94
|
+
|
|
95
|
+
## Target deployment sequence
|
|
96
|
+
|
|
97
|
+
Server 30 uses a systemd service:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
VERSION=0.14.0
|
|
101
|
+
ssh au2223@192.168.0.30 "VERSION=$VERSION bash -s" <<'REMOTE'
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
sudo -n env "PATH=$PATH" npm install -g appback-remoteagent@$VERSION
|
|
104
|
+
/home/au2223/.nvm/versions/node/v22.22.0/bin/remoteagent-install
|
|
105
|
+
sudo -n systemctl restart remoteagent
|
|
106
|
+
systemctl is-active remoteagent
|
|
107
|
+
journalctl -u remoteagent --since '2 minutes ago' --no-pager
|
|
108
|
+
REMOTE
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Server 26 currently uses user-level start/stop scripts, not a systemd `remoteagent.service`:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
VERSION=0.14.0
|
|
115
|
+
ssh ospadmin@192.168.0.26 "VERSION=$VERSION bash -s" <<'REMOTE'
|
|
116
|
+
set -euo pipefail
|
|
74
117
|
npm install -g appback-remoteagent@$VERSION
|
|
75
118
|
remoteagent-install
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
~/.remoteagent/stop-remoteagent.sh || true
|
|
120
|
+
sleep 2
|
|
121
|
+
~/.remoteagent/start-remoteagent.sh
|
|
122
|
+
sleep 3
|
|
123
|
+
pgrep -af "appback-remoteagent/dist/index.js"
|
|
124
|
+
tail -80 ~/.remoteagent/logs/agent.log
|
|
125
|
+
REMOTE
|
|
79
126
|
```
|
|
80
127
|
|
|
81
128
|
## Other Runtime Follow-up
|
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.
|