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.
@@ -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, env.mainBotId, await this.pollingState.list());
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}${current ? ` [${role}]` : ""}`,
46
- `mainBot: ${mainLabel}`,
42
+ `bot: ${currentLabel}`,
47
43
  `botCount: ${bots.length}`,
48
- `sleep: ${this.pollingState.formatMode(String(current?.id ?? currentBotId), role === "main", currentState)}`,
44
+ `pollingState: ${this.pollingState.formatMode(currentState)}`,
49
45
  ].join("\n");
50
46
  }
51
- async sleepBot(selector, currentBotId) {
52
- const env = await this.readEnvConfig();
53
- const bots = this.zipBots(env.tokens, env.usernames);
54
- if (bots.length === 0) {
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 wakeBot(selector) {
73
- const env = await this.readEnvConfig();
74
- const bots = this.zipBots(env.tokens, env.usernames);
75
- const target = this.resolveBotSelector(bots, selector);
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, mainBotId);
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, nextMainBotId);
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, mainBotId);
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, mainBotId).map((line) => `- ${line.replace(/^\d+\.\s*/, "")}`)
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, mainBotId) {
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, mainBotId, states = {}) {
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, mainBotId, states),
526
+ ...this.formatBotListLines(bots, states),
599
527
  ].join("\n");
600
528
  }
601
- formatBotListLines(bots, explicitMainBotId, states = {}) {
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
- `[${isMain ? "main" : "sub"}]`,
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 && state.sleepMode !== "deep" ? `nextPoll=${this.formatAge(state.nextPollAt)}` : undefined,
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 setSleepMode(botId, sleepMode, username) {
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
- if (sleepMode === "awake") {
48
- current.nextPollAt = new Date().toISOString();
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(botId, isMain, state) {
83
- if (isMain) {
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.
@@ -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 more than three configured Telegram bots, polling pressure can become operationally visible.
39
- Bot sleep is the planned mitigation: keep one main bot reachable and let long-idle sub bots stop polling without removing their configuration.
40
- See [BOT_SLEEP.md](./BOT_SLEEP.md).
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 publish
67
+ npm pack
68
68
  ```
69
69
 
70
- Typical target deployment sequence for server 30 and server 26:
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.13.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
- sudo systemctl restart remoteagent
77
- systemctl is-active remoteagent
78
- journalctl -u remoteagent --since "2 minutes ago" --no-pager
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.13.21",
3
+ "version": "0.14.1",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",
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.