appback-remoteagent 0.14.2 → 0.14.4

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/dist/bot.js CHANGED
@@ -1087,10 +1087,14 @@ async function runWithPendingAnimation(botToken, chatId, task) {
1087
1087
  const rendered = formatProviderTelegramChunks(progressChunks, parseMode);
1088
1088
  const extra = rendered.parseMode ? { parse_mode: rendered.parseMode } : undefined;
1089
1089
  for (const chunk of rendered.chunks) {
1090
- await sendTelegramMessage(botToken, chatId, chunk, extra);
1090
+ await sendTelegramMessage(botToken, chatId, chunk, extra).catch((error) => {
1091
+ console.warn(`[telegram-progress-delivery] chat=${chatId} dropped progress message: ${formatTelegramDeliveryError(error)}`);
1092
+ });
1091
1093
  }
1092
1094
  if (normalized.documents.length > 0) {
1093
- await sendTelegramDocuments(botToken, chatId, normalized.documents);
1095
+ await sendTelegramDocuments(botToken, chatId, normalized.documents).catch((error) => {
1096
+ console.warn(`[telegram-progress-delivery] chat=${chatId} dropped progress document(s): ${formatTelegramDeliveryError(error)}`);
1097
+ });
1094
1098
  }
1095
1099
  },
1096
1100
  };
@@ -1146,6 +1150,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1146
1150
  let untaggedIntentRetryCount = 0;
1147
1151
  let missingEvidenceRetryCount = 0;
1148
1152
  let deliveredProgressCount = 0;
1153
+ let providerCompleted = false;
1149
1154
  const ensureStillBound = async (phase) => {
1150
1155
  if (!sessionId) {
1151
1156
  return;
@@ -1238,6 +1243,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1238
1243
  if (currentSession) {
1239
1244
  await memoryService.completeTask(currentSession.session, parsed.chunks.join("\n"));
1240
1245
  }
1246
+ providerCompleted = true;
1241
1247
  autoContinue.clear(botId, chatId, sessionId);
1242
1248
  return parsed.chunks;
1243
1249
  }
@@ -1314,7 +1320,12 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1314
1320
  }
1315
1321
  finally {
1316
1322
  if (currentSession) {
1317
- await botManagement.markProviderIdle(botId, sessionId);
1323
+ if (providerCompleted) {
1324
+ await botManagement.markProviderCompleted(botId, sessionId);
1325
+ }
1326
+ else {
1327
+ await botManagement.markProviderIdle(botId, sessionId);
1328
+ }
1318
1329
  }
1319
1330
  }
1320
1331
  }
@@ -2226,23 +2237,38 @@ async function sendTelegramDocument(botToken, chatId, document) {
2226
2237
  async function sendTelegramMessage(botToken, chatId, text, extra) {
2227
2238
  const startedAt = Date.now();
2228
2239
  try {
2229
- try {
2230
- return await callTelegramApi(botToken, "sendMessage", {
2231
- chat_id: String(chatId),
2232
- text,
2233
- parse_mode: extra?.parse_mode,
2234
- });
2235
- }
2236
- catch (error) {
2237
- if (!extra?.parse_mode || !isTelegramEntityParseError(error)) {
2238
- throw error;
2240
+ let lastError;
2241
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
2242
+ try {
2243
+ try {
2244
+ return await callTelegramApi(botToken, "sendMessage", {
2245
+ chat_id: String(chatId),
2246
+ text,
2247
+ parse_mode: extra?.parse_mode,
2248
+ });
2249
+ }
2250
+ catch (error) {
2251
+ if (!extra?.parse_mode || !isTelegramEntityParseError(error)) {
2252
+ throw error;
2253
+ }
2254
+ console.warn(`[telegram-sendMessage-fallback] chat=${chatId} parseMode=${extra.parse_mode}: ${formatTelegramDeliveryError(error)}`);
2255
+ return await callTelegramApi(botToken, "sendMessage", {
2256
+ chat_id: String(chatId),
2257
+ text: stripTelegramHtml(text),
2258
+ });
2259
+ }
2260
+ }
2261
+ catch (error) {
2262
+ lastError = error;
2263
+ if (attempt >= 3 || !isRetryableTelegramDeliveryError(error)) {
2264
+ throw error;
2265
+ }
2266
+ const delayMs = attempt * 1500;
2267
+ console.warn(`[telegram-sendMessage-retry] chat=${chatId} attempt=${attempt}/3 delayMs=${delayMs}: ${formatTelegramDeliveryError(error)}`);
2268
+ await sleep(delayMs);
2239
2269
  }
2240
- console.warn(`[telegram-sendMessage-fallback] chat=${chatId} parseMode=${extra.parse_mode}: ${error instanceof Error ? error.message : String(error)}`);
2241
- return await callTelegramApi(botToken, "sendMessage", {
2242
- chat_id: String(chatId),
2243
- text: stripTelegramHtml(text),
2244
- });
2245
2270
  }
2271
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
2246
2272
  }
2247
2273
  finally {
2248
2274
  const elapsedMs = Date.now() - startedAt;
@@ -2282,7 +2308,16 @@ async function callTelegramApi(botToken, method, params) {
2282
2308
  args.push("--data-urlencode", `${key}=${value}`);
2283
2309
  }
2284
2310
  }
2285
- const { stdout, stderr } = await execFileAsync("curl", args);
2311
+ let stdout;
2312
+ let stderr;
2313
+ try {
2314
+ const result = await execFileAsync("curl", args);
2315
+ stdout = result.stdout;
2316
+ stderr = result.stderr;
2317
+ }
2318
+ catch (error) {
2319
+ throw new TelegramApiError(method, summarizeCurlTelegramError(error, method));
2320
+ }
2286
2321
  if (stderr?.trim()) {
2287
2322
  console.error(`curl stderr for ${method}: ${stderr.trim()}`);
2288
2323
  }
@@ -2302,6 +2337,26 @@ class TelegramApiError extends Error {
2302
2337
  this.name = "TelegramApiError";
2303
2338
  }
2304
2339
  }
2340
+ function summarizeCurlTelegramError(error, method) {
2341
+ if (!(error instanceof Error)) {
2342
+ return `Telegram API ${method} failed: ${String(error)}`;
2343
+ }
2344
+ const stderr = typeof error === "object" && error !== null && "stderr" in error
2345
+ ? String(error.stderr ?? "").trim()
2346
+ : "";
2347
+ const code = typeof error === "object" && error !== null && "code" in error
2348
+ ? String(error.code ?? "")
2349
+ : "";
2350
+ const message = stderr || error.message;
2351
+ return [`Telegram API ${method} curl failed`, code ? `code=${code}` : undefined, message].filter(Boolean).join(": ");
2352
+ }
2353
+ function formatTelegramDeliveryError(error) {
2354
+ return error instanceof Error ? error.message : String(error);
2355
+ }
2356
+ function isRetryableTelegramDeliveryError(error) {
2357
+ const message = formatTelegramDeliveryError(error);
2358
+ return /timed out|timeout|Bad Gateway|502|503|504|ECONNRESET|connection reset|EAI_AGAIN|ENOTFOUND/i.test(message);
2359
+ }
2305
2360
  function isTelegramForbiddenError(error) {
2306
2361
  if (error instanceof GrammyError) {
2307
2362
  return error.error_code === 403 || /^Forbidden:/i.test(error.description);
package/dist/index.js CHANGED
@@ -236,11 +236,13 @@ async function pollTelegramBot(pollingBot, runtime, options) {
236
236
  runtime.offset = orderedUpdates[orderedUpdates.length - 1].update_id + 1;
237
237
  }
238
238
  const receivedMessage = orderedUpdates.some(hasMessagePayload);
239
- const lastMessageAt = receivedMessage ? new Date(now).toISOString() : options.state?.lastMessageAt;
239
+ const lastUserMessageAt = receivedMessage ? new Date(now).toISOString() : options.state?.lastUserMessageAt;
240
+ const lastMessageAt = receivedMessage ? lastUserMessageAt : options.state?.lastMessageAt;
240
241
  const nextPollAt = now + computePolicyPollIntervalMs(options.totalBots, receivedMessage ? 1 : options.botRank, {
241
242
  ...options.state,
242
243
  botId,
243
244
  consecutiveFailures: options.state?.consecutiveFailures ?? runtime.consecutiveFailures,
245
+ lastUserMessageAt,
244
246
  lastMessageAt,
245
247
  }, {
246
248
  tieredPollingMinBots: config.telegramTieredPollingMinBots,
@@ -253,6 +255,7 @@ async function pollTelegramBot(pollingBot, runtime, options) {
253
255
  username: pollingBot.botInfo.username,
254
256
  lastPollAt: new Date(now).toISOString(),
255
257
  lastUpdateAt: orderedUpdates.length > 0 ? new Date(now).toISOString() : undefined,
258
+ lastUserMessageAt,
256
259
  lastMessageAt,
257
260
  nextPollAt: new Date(nextPollAt).toISOString(),
258
261
  consecutiveFailures: 0,
@@ -54,6 +54,11 @@ export class BotManagementService {
54
54
  const pollingBotId = bot ? String(bot.id) : botId;
55
55
  await this.pollingState.markIdle(pollingBotId, sessionId, bot?.username);
56
56
  }
57
+ async markProviderCompleted(botId, sessionId) {
58
+ const bot = await this.findConfiguredBot(botId);
59
+ const pollingBotId = bot ? String(bot.id) : botId;
60
+ await this.pollingState.markCompleted(pollingBotId, sessionId, bot?.username);
61
+ }
57
62
  async getPendingOperationNotice() {
58
63
  const pending = await this.readPendingOperation();
59
64
  if (!pending) {
@@ -70,6 +70,21 @@ export class BotPollingStateService {
70
70
  state.bots[botId] = current;
71
71
  await this.writeNow();
72
72
  }
73
+ async markCompleted(botId, sessionId, username) {
74
+ await this.markIdle(botId, sessionId, username);
75
+ const state = await this.read();
76
+ const current = state.bots[botId] ?? {
77
+ botId,
78
+ username,
79
+ consecutiveFailures: 0,
80
+ };
81
+ if (username) {
82
+ current.username = username;
83
+ }
84
+ current.lastProviderCompletedAt = new Date().toISOString();
85
+ state.bots[botId] = current;
86
+ await this.writeNow();
87
+ }
73
88
  async recordPoll(botId, patch) {
74
89
  const state = await this.read();
75
90
  const current = state.bots[botId] ?? {
@@ -2,7 +2,7 @@ export function computeRecentMessageRanks(botIds, states) {
2
2
  const ranked = botIds
3
3
  .map((botId) => ({
4
4
  botId,
5
- timestamp: parseStateTime(states[botId]?.lastMessageAt) ?? 0,
5
+ timestamp: latestTimestamp(states[botId]?.lastUserMessageAt ?? states[botId]?.lastMessageAt, states[botId]?.lastProviderCompletedAt),
6
6
  }))
7
7
  .sort((left, right) => right.timestamp - left.timestamp);
8
8
  return new Map(ranked.map((entry, index) => [entry.botId, index + 1]));
@@ -29,3 +29,6 @@ function parseStateTime(value) {
29
29
  const timestamp = Date.parse(value);
30
30
  return Number.isFinite(timestamp) ? timestamp : undefined;
31
31
  }
32
+ function latestTimestamp(...values) {
33
+ return Math.max(0, ...values.map((value) => parseStateTime(value) ?? 0));
34
+ }
@@ -14,6 +14,13 @@ When 5 or more bots are configured:
14
14
  - the next 4 idle bots poll every 60 seconds
15
15
  - all remaining idle bots poll every 180 seconds
16
16
 
17
+ Recent activity is updated only when:
18
+
19
+ - the operator sends a message to that bot
20
+ - the provider returns a completed result to that bot
21
+
22
+ Polling itself must not update recent activity. A bot should not become "recent" merely because RemoteAgent checked Telegram for updates.
23
+
17
24
  When a bot has active provider work:
18
25
 
19
26
  - that bot polls every 60 seconds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",