@songsid/agend 2.3.4-beta.1 → 2.3.4-beta.10

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.
Files changed (284) hide show
  1. package/THIRD_PARTY_NOTICES.md +37 -0
  2. package/dist/agend-home.d.ts +13 -0
  3. package/dist/agend-home.js +42 -0
  4. package/dist/agend-home.js.map +1 -1
  5. package/dist/agent-cli-instructions.md +1 -1
  6. package/dist/agent-credentials.d.ts +24 -0
  7. package/dist/agent-credentials.js +66 -0
  8. package/dist/agent-credentials.js.map +1 -0
  9. package/dist/agent-endpoint.d.ts +3 -1
  10. package/dist/agent-endpoint.js +13 -0
  11. package/dist/agent-endpoint.js.map +1 -1
  12. package/dist/auth-api.js +10 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/session-authorizations.d.ts +28 -4
  15. package/dist/authority/session-authorizations.js +63 -17
  16. package/dist/authority/session-authorizations.js.map +1 -1
  17. package/dist/backend/antigravity.js +3 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +3 -0
  20. package/dist/backend/claude-code.js +89 -4
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-footer-hints.d.ts +4 -0
  23. package/dist/backend/codex-footer-hints.js +27 -0
  24. package/dist/backend/codex-footer-hints.js.map +1 -0
  25. package/dist/backend/codex-session-lookup.d.ts +35 -0
  26. package/dist/backend/codex-session-lookup.js +93 -0
  27. package/dist/backend/codex-session-lookup.js.map +1 -1
  28. package/dist/backend/codex.d.ts +27 -0
  29. package/dist/backend/codex.js +124 -23
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/kiro-identity.d.ts +2 -0
  32. package/dist/backend/kiro-identity.js +13 -6
  33. package/dist/backend/kiro-identity.js.map +1 -1
  34. package/dist/backend/kiro.d.ts +3 -0
  35. package/dist/backend/kiro.js +68 -5
  36. package/dist/backend/kiro.js.map +1 -1
  37. package/dist/backend/muse.js +5 -3
  38. package/dist/backend/muse.js.map +1 -1
  39. package/dist/backend/types.d.ts +6 -1
  40. package/dist/backend/types.js +3 -0
  41. package/dist/backend/types.js.map +1 -1
  42. package/dist/backend-availability.d.ts +6 -0
  43. package/dist/backend-availability.js +13 -5
  44. package/dist/backend-availability.js.map +1 -1
  45. package/dist/cache-ledger.d.ts +85 -0
  46. package/dist/cache-ledger.js +236 -1
  47. package/dist/cache-ledger.js.map +1 -1
  48. package/dist/cache-service.d.ts +5 -0
  49. package/dist/cache-service.js +16 -4
  50. package/dist/cache-service.js.map +1 -1
  51. package/dist/channel/adapters/discord.js +27 -4
  52. package/dist/channel/adapters/discord.js.map +1 -1
  53. package/dist/channel/adapters/telegram.d.ts +51 -0
  54. package/dist/channel/adapters/telegram.js +238 -18
  55. package/dist/channel/adapters/telegram.js.map +1 -1
  56. package/dist/channel/inbox-temp-name.d.ts +9 -0
  57. package/dist/channel/inbox-temp-name.js +13 -0
  58. package/dist/channel/inbox-temp-name.js.map +1 -0
  59. package/dist/channel/mcp-tools.js +3 -3
  60. package/dist/channel/mcp-tools.js.map +1 -1
  61. package/dist/channel/types.d.ts +2 -0
  62. package/dist/chat-log-ids.d.ts +48 -0
  63. package/dist/chat-log-ids.js +182 -0
  64. package/dist/chat-log-ids.js.map +1 -0
  65. package/dist/classic-channel-manager.d.ts +8 -1
  66. package/dist/classic-channel-manager.js +24 -2
  67. package/dist/classic-channel-manager.js.map +1 -1
  68. package/dist/cli.js +4 -6
  69. package/dist/cli.js.map +1 -1
  70. package/dist/cross-instance-notice.d.ts +10 -0
  71. package/dist/cross-instance-notice.js +67 -5
  72. package/dist/cross-instance-notice.js.map +1 -1
  73. package/dist/daemon.d.ts +52 -0
  74. package/dist/daemon.js +530 -34
  75. package/dist/daemon.js.map +1 -1
  76. package/dist/delivery-consumed-watch.d.ts +23 -1
  77. package/dist/delivery-consumed-watch.js +30 -3
  78. package/dist/delivery-consumed-watch.js.map +1 -1
  79. package/dist/delivery-reconciliation.d.ts +7 -0
  80. package/dist/delivery-reconciliation.js +9 -1
  81. package/dist/delivery-reconciliation.js.map +1 -1
  82. package/dist/dialog-screen-text.d.ts +16 -0
  83. package/dist/dialog-screen-text.js +51 -0
  84. package/dist/dialog-screen-text.js.map +1 -0
  85. package/dist/export-import.d.ts +7 -0
  86. package/dist/export-import.js +44 -10
  87. package/dist/export-import.js.map +1 -1
  88. package/dist/fleet-context.d.ts +2 -0
  89. package/dist/fleet-manager.d.ts +147 -11
  90. package/dist/fleet-manager.js +747 -118
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-yaml-patch.d.ts +3 -0
  93. package/dist/fleet-yaml-patch.js +103 -0
  94. package/dist/fleet-yaml-patch.js.map +1 -0
  95. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  96. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  97. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  98. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  99. package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
  101. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -4
  102. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  103. package/dist/install-stream.d.ts +53 -0
  104. package/dist/install-stream.js +134 -0
  105. package/dist/install-stream.js.map +1 -0
  106. package/dist/instance-lifecycle.d.ts +20 -2
  107. package/dist/instance-lifecycle.js +80 -12
  108. package/dist/instance-lifecycle.js.map +1 -1
  109. package/dist/instance-topic-delete.d.ts +77 -0
  110. package/dist/instance-topic-delete.js +123 -0
  111. package/dist/instance-topic-delete.js.map +1 -0
  112. package/dist/instructions.d.ts +1 -1
  113. package/dist/instructions.js +1 -1
  114. package/dist/instructions.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +11 -0
  116. package/dist/kiro-engine-status.js +6 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-resume-summary.d.ts +37 -0
  119. package/dist/kiro-resume-summary.js +85 -0
  120. package/dist/kiro-resume-summary.js.map +1 -0
  121. package/dist/locale.js +82 -6
  122. package/dist/locale.js.map +1 -1
  123. package/dist/log-service.d.ts +30 -0
  124. package/dist/log-service.js +36 -3
  125. package/dist/log-service.js.map +1 -1
  126. package/dist/log-steps.d.ts +4 -3
  127. package/dist/log-steps.js +32 -7
  128. package/dist/log-steps.js.map +1 -1
  129. package/dist/logger.d.ts +12 -0
  130. package/dist/logger.js +43 -1
  131. package/dist/logger.js.map +1 -1
  132. package/dist/login-controller.js +9 -1
  133. package/dist/login-controller.js.map +1 -1
  134. package/dist/login-flows.d.ts +2 -0
  135. package/dist/login-flows.js +5 -2
  136. package/dist/login-flows.js.map +1 -1
  137. package/dist/login-manager.d.ts +6 -0
  138. package/dist/login-manager.js +10 -1
  139. package/dist/login-manager.js.map +1 -1
  140. package/dist/missed-offline.d.ts +25 -0
  141. package/dist/missed-offline.js +79 -0
  142. package/dist/missed-offline.js.map +1 -0
  143. package/dist/needs-you-hub.d.ts +6 -1
  144. package/dist/needs-you-hub.js +31 -8
  145. package/dist/needs-you-hub.js.map +1 -1
  146. package/dist/needs-you.d.ts +22 -2
  147. package/dist/needs-you.js +17 -3
  148. package/dist/needs-you.js.map +1 -1
  149. package/dist/office-share-page.d.ts +1 -1
  150. package/dist/office-share-page.js +6 -2
  151. package/dist/office-share-page.js.map +1 -1
  152. package/dist/outbound-handlers.js +59 -3
  153. package/dist/outbound-handlers.js.map +1 -1
  154. package/dist/outbound-schemas.d.ts +1 -0
  155. package/dist/outbound-schemas.js +9 -3
  156. package/dist/outbound-schemas.js.map +1 -1
  157. package/dist/package-preimage.js +27 -2
  158. package/dist/package-preimage.js.map +1 -1
  159. package/dist/queued-handoffs.d.ts +33 -0
  160. package/dist/queued-handoffs.js +75 -0
  161. package/dist/queued-handoffs.js.map +1 -0
  162. package/dist/reply-buttons-controller.js +10 -0
  163. package/dist/reply-buttons-controller.js.map +1 -1
  164. package/dist/scheduler/db.d.ts +2 -1
  165. package/dist/scheduler/db.js +23 -9
  166. package/dist/scheduler/db.js.map +1 -1
  167. package/dist/selection-card.d.ts +2 -3
  168. package/dist/selection-card.js +99 -26
  169. package/dist/selection-card.js.map +1 -1
  170. package/dist/settings-api.d.ts +13 -0
  171. package/dist/settings-api.js +12 -1
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/stall-reporter.d.ts +21 -0
  174. package/dist/stall-reporter.js +82 -0
  175. package/dist/stall-reporter.js.map +1 -0
  176. package/dist/tmux-control.d.ts +2 -2
  177. package/dist/tmux-control.js +4 -2
  178. package/dist/tmux-control.js.map +1 -1
  179. package/dist/tmux-exec-helper.d.ts +6 -3
  180. package/dist/tmux-exec-helper.js +115 -53
  181. package/dist/tmux-exec-helper.js.map +1 -1
  182. package/dist/tmux-manager.d.ts +6 -0
  183. package/dist/tmux-manager.js +17 -0
  184. package/dist/tmux-manager.js.map +1 -1
  185. package/dist/tool-permissions.d.ts +5 -2
  186. package/dist/tool-permissions.js +6 -5
  187. package/dist/tool-permissions.js.map +1 -1
  188. package/dist/tool-progress.d.ts +2 -0
  189. package/dist/tool-progress.js +19 -3
  190. package/dist/tool-progress.js.map +1 -1
  191. package/dist/topic-commands.js +4 -0
  192. package/dist/topic-commands.js.map +1 -1
  193. package/dist/tunnel/cloudflared-install.d.ts +4 -0
  194. package/dist/tunnel/cloudflared-install.js +20 -1
  195. package/dist/tunnel/cloudflared-install.js.map +1 -1
  196. package/dist/tunnel/cloudflared.d.ts +8 -1
  197. package/dist/tunnel/cloudflared.js +34 -6
  198. package/dist/tunnel/cloudflared.js.map +1 -1
  199. package/dist/tunnel/lease.d.ts +3 -3
  200. package/dist/tunnel/lease.js +23 -19
  201. package/dist/tunnel/lease.js.map +1 -1
  202. package/dist/tunnel/manager.js +6 -7
  203. package/dist/tunnel/manager.js.map +1 -1
  204. package/dist/ui/chat-store.js +20 -3
  205. package/dist/ui/chat-strings.js +4 -4
  206. package/dist/ui/chat-thread.js +52 -10
  207. package/dist/ui/fleet-canvas.js +4 -3
  208. package/dist/ui/fleet-office-model.js +55 -22
  209. package/dist/ui/fleet-office-overview.js +6 -38
  210. package/dist/ui/fleet-office-pack.js +4 -3
  211. package/dist/ui/fleet-office-skin.js +114 -21
  212. package/dist/ui/fleet-office-store.js +5 -0
  213. package/dist/ui/fleet-office-zones.js +2 -2
  214. package/dist/ui/fleet-office.js +41 -9
  215. package/dist/ui/fleet-usage.js +90 -27
  216. package/dist/ui/panel-chat.js +2 -0
  217. package/dist/ui/panel-log.js +7 -1
  218. package/dist/ui/panel-needs.js +17 -8
  219. package/dist/ui/settings-strings.js +2 -2
  220. package/dist/ui/shared/app-i18n.js +4 -4
  221. package/dist/ui/shared/app-needs.js +1 -1
  222. package/dist/ui/shared/app-shell.js +4 -2
  223. package/dist/ui/shared/app-stream.js +26 -0
  224. package/dist/ui/shared/app.css +51 -3
  225. package/dist/ui/shared/office/guild.png +0 -0
  226. package/dist/ui/shared/office/school.png +0 -0
  227. package/dist/ui/shared/panel-view.js +339 -17
  228. package/dist/ui/shared/usage-dialog.js +18 -1
  229. package/dist/ui/shared/view-history.css +17 -0
  230. package/dist/ui/shared/view-history.js +438 -0
  231. package/dist/ui/shared/view-strings.js +42 -0
  232. package/dist/unattended-install.d.ts +6 -0
  233. package/dist/unattended-install.js +79 -0
  234. package/dist/unattended-install.js.map +1 -0
  235. package/dist/usage/capacity.d.ts +69 -0
  236. package/dist/usage/capacity.js +119 -0
  237. package/dist/usage/capacity.js.map +1 -0
  238. package/dist/usage/i18n-keys.d.ts +1 -1
  239. package/dist/usage/i18n-keys.js +3 -0
  240. package/dist/usage/i18n-keys.js.map +1 -1
  241. package/dist/usage/profile-timeline.d.ts +16 -0
  242. package/dist/usage/profile-timeline.js +58 -0
  243. package/dist/usage/profile-timeline.js.map +1 -0
  244. package/dist/usage/quota-history.d.ts +43 -0
  245. package/dist/usage/quota-history.js +168 -0
  246. package/dist/usage/quota-history.js.map +1 -0
  247. package/dist/usage/usage-api.d.ts +31 -0
  248. package/dist/usage/usage-api.js +80 -1
  249. package/dist/usage/usage-api.js.map +1 -1
  250. package/dist/usage-report.d.ts +25 -1
  251. package/dist/usage-report.js +55 -4
  252. package/dist/usage-report.js.map +1 -1
  253. package/dist/usage-rollup.d.ts +4 -1
  254. package/dist/usage-rollup.js +12 -3
  255. package/dist/usage-rollup.js.map +1 -1
  256. package/dist/view-api.d.ts +31 -0
  257. package/dist/view-api.js +186 -41
  258. package/dist/view-api.js.map +1 -1
  259. package/dist/view-history.d.ts +84 -0
  260. package/dist/view-history.js +116 -0
  261. package/dist/view-history.js.map +1 -0
  262. package/dist/web-api.d.ts +6 -0
  263. package/dist/web-api.js +15 -4
  264. package/dist/web-api.js.map +1 -1
  265. package/dist/web-chat-history.d.ts +20 -0
  266. package/dist/web-chat-history.js +21 -0
  267. package/dist/web-chat-history.js.map +1 -1
  268. package/dist/web-guest-access.js +5 -1
  269. package/dist/web-guest-access.js.map +1 -1
  270. package/dist/web-login.d.ts +4 -3
  271. package/dist/web-login.js +23 -14
  272. package/dist/web-login.js.map +1 -1
  273. package/dist/web-office-pack-tools.d.ts +1 -1
  274. package/dist/web-office-pack-tools.js +1 -1
  275. package/dist/web-office-pack-tools.js.map +1 -1
  276. package/dist/web-session-authorization.d.ts +13 -2
  277. package/dist/web-session-authorization.js +10 -3
  278. package/dist/web-session-authorization.js.map +1 -1
  279. package/dist/web-session.d.ts +5 -0
  280. package/dist/web-session.js +20 -8
  281. package/dist/web-session.js.map +1 -1
  282. package/launcher/agend-install.cjs +16 -2
  283. package/launcher/private-home.cjs +1 -1
  284. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import { nativeHumanOrigin } from "../../human-delivery-receipts.js";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { randomBytes } from "node:crypto";
4
+ import { inboxTempName } from "../inbox-temp-name.js";
4
5
  import { Agent as HttpAgent } from "node:http";
5
6
  import { Agent as HttpsAgent } from "node:https";
6
- import { createReadStream, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
7
+ import { createReadStream, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
7
8
  import { join, extname, basename } from "node:path";
9
+ import { atomicWriteFileSync } from "../../atomic-write.js";
8
10
  import { Bot, GrammyError, HttpError, InputFile } from "grammy";
9
11
  import { InlineKeyboard } from "grammy";
10
12
  import { stripPlaceRefs } from "../../topic-link.js";
@@ -51,9 +53,34 @@ export const TELEGRAM_ALLOWED_UPDATES = [
51
53
  "callback_query",
52
54
  "message_reaction",
53
55
  ];
56
+ /**
57
+ * Pending Telegram updates older than this are filed as a Needs-you item
58
+ * ("missed while AgEnD was offline") and never auto-injected into an agent.
59
+ * Telegram itself keeps the getUpdates queue for about 24 hours.
60
+ */
61
+ export const TELEGRAM_PENDING_LIVE_MAX_MS = 2 * 60 * 60 * 1000;
62
+ /**
63
+ * Telegram may pick a new random `update_id` after about a week with no
64
+ * updates (Bot API). A floor older than this is ignored so inbound is not
65
+ * dropped forever.
66
+ */
67
+ export const TELEGRAM_UPDATE_ID_FLOOR_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
68
+ /** Telegram's default getUpdates page size; a shorter page means the queue drained. */
69
+ export const TELEGRAM_GETUPDATES_LIMIT = 100;
70
+ /** Crash-safe last processed `update_id` for this adapter (inside inboxDir; prune skips it). */
71
+ export function telegramLastUpdateIdPath(inboxDir, adapterId) {
72
+ const safe = adapterId.replace(/[^A-Za-z0-9._-]+/g, "_") || "adapter";
73
+ return join(inboxDir, `telegram-${safe}-last-update-id`);
74
+ }
54
75
  function errorMessage(err) {
55
76
  return err instanceof Error ? err.message : String(err);
56
77
  }
78
+ function telegramUpdateAgeMs(update, now) {
79
+ const date = update.message?.date ?? update.callback_query?.message?.date;
80
+ if (typeof date !== "number" || !Number.isFinite(date))
81
+ return null;
82
+ return now - date * 1000;
83
+ }
57
84
  /** Remove Bot API credentials from nested node-fetch errors before logging. */
58
85
  export function redactTelegramSecrets(value, botToken = "") {
59
86
  let redacted = value.replace(/\/bot[^/\s]+(?=\/)/gi, "/bot[REDACTED]");
@@ -246,6 +273,14 @@ export class TelegramAdapter extends EventEmitter {
246
273
  /** Topics the probe itself is addressing right now — their failures are results, not hints. */
247
274
  probingThreads = new Set();
248
275
  topicGoneHintAt = new Map();
276
+ /** Last processed `update_id`; 0 means no floor. Telegram ids are usually monotonic. */
277
+ lastUpdateId = 0;
278
+ floorBotId;
279
+ floorRecordedAt = 0;
280
+ /** False until a getUpdates page shorter than TELEGRAM_GETUPDATES_LIMIT (the queue drained). */
281
+ backlogDrained = false;
282
+ backlogUpdateIds = new Set();
283
+ persistFloorTimer;
249
284
  constructor(opts) {
250
285
  super();
251
286
  this.id = opts.id;
@@ -296,6 +331,7 @@ export class TelegramAdapter extends EventEmitter {
296
331
  }
297
332
  },
298
333
  });
334
+ this.loadFloor();
299
335
  this._registerHandlers();
300
336
  }
301
337
  /**
@@ -313,11 +349,21 @@ export class TelegramAdapter extends EventEmitter {
313
349
  if (!this.bot.api.config?.use)
314
350
  return;
315
351
  this.bot.api.config.use(async (prev, method, payload, signal) => {
352
+ // grammY.stop sends a final offset ACK, and an already-fetched batch can finish later.
353
+ // Once fenced, neither path may confirm or admit updates for the stopping fleet.
354
+ if (method === "getUpdates" && this.stopped)
355
+ return { ok: true, result: [] };
316
356
  const requestGeneration = this.httpAgentGeneration;
317
357
  try {
318
- return await prev(method, payload, signal);
358
+ const result = await prev(method, payload, signal);
359
+ if (method === "getUpdates" && this.stopped)
360
+ return { ok: true, result: [] };
361
+ this.noteGetUpdatesBacklog(method, result);
362
+ return result;
319
363
  }
320
364
  catch (err) {
365
+ if (method === "getUpdates" && this.stopped)
366
+ return { ok: true, result: [] };
321
367
  if (!(err instanceof HttpError))
322
368
  throw err;
323
369
  const firstDetail = telegramNetworkErrorDetails(err, this.botToken);
@@ -325,6 +371,8 @@ export class TelegramAdapter extends EventEmitter {
325
371
  console.warn(`[telegram] ${method} transport failed; HTTP pool ${reset ? "reset" : "reset suppressed by cooldown"}: ${firstDetail}`);
326
372
  if (isDefinitelyPreDeliveryNetworkError(err) && !signal?.aborted) {
327
373
  await new Promise(resolve => setTimeout(resolve, TELEGRAM_NETWORK_RETRY_MS));
374
+ if (method === "getUpdates" && this.stopped)
375
+ return { ok: true, result: [] };
328
376
  const retryGeneration = this.httpAgentGeneration;
329
377
  try {
330
378
  return await prev(method, payload, signal);
@@ -369,14 +417,42 @@ export class TelegramAdapter extends EventEmitter {
369
417
  return true;
370
418
  }
371
419
  _registerHandlers() {
372
- // Own messages must never become inbound work even when transport replays
373
- // them. Fleet-wide echo provenance is checked in the common ingress path.
374
- // Put this ahead of rich-message middleware as well as ordinary messages.
420
+ // Dedup, own-message skip, and the downtime age cap run before any handler.
421
+ // Record the update_id before acting so a crash mid-batch cannot inject twice.
375
422
  this.bot.use(async (ctx, next) => {
376
- const message = ctx.update?.message;
423
+ if (this.stopped)
424
+ return; // Before the durable floor as well as handler dispatch.
425
+ const update = ctx.update;
426
+ const updateId = update.update_id;
427
+ this.discardFloorIfStale(Date.now());
428
+ if (typeof updateId === "number") {
429
+ if (this.lastUpdateId > 0 && updateId <= this.lastUpdateId)
430
+ return;
431
+ this.lastUpdateId = updateId;
432
+ this.floorRecordedAt = Date.now();
433
+ if (this.bot.botInfo?.id != null)
434
+ this.floorBotId = this.bot.botInfo.id;
435
+ this.schedulePersistFloor();
436
+ }
437
+ const message = update.message;
377
438
  const sender = message?.from?.id;
378
439
  if (sender != null && sender === this.bot.botInfo?.id)
379
440
  return;
441
+ // Age cap / stale callbacks apply to the downtime queue (pages until a short getUpdates).
442
+ const isBacklog = typeof updateId === "number" && this.backlogUpdateIds.has(updateId);
443
+ if (isBacklog) {
444
+ if (update.callback_query) {
445
+ // The card's date is not the click's; queued callbacks are stale by being in the backlog.
446
+ ctx.answerCallbackQuery().catch(() => { });
447
+ return;
448
+ }
449
+ const ageMs = telegramUpdateAgeMs(update, Date.now());
450
+ if (ageMs != null && ageMs > TELEGRAM_PENDING_LIVE_MAX_MS
451
+ && message && !this._isServiceMessage(message)) {
452
+ this.emitMissedOffline(updateId ?? 0, message);
453
+ return;
454
+ }
455
+ }
380
456
  return next();
381
457
  });
382
458
  // Middleware: catch Rich Message updates that grammy doesn't route to bot.on("message")
@@ -639,6 +715,8 @@ export class TelegramAdapter extends EventEmitter {
639
715
  authRejected = false;
640
716
  /** #1537 review: stop() was called (and no start() since). */
641
717
  stopped = false;
718
+ pollingGeneration = 0;
719
+ receiveStop = null;
642
720
  connectionEvidence() {
643
721
  return { ready: this.pollingReady, authRejected: this.authRejected, stopped: this.stopped };
644
722
  }
@@ -646,6 +724,10 @@ export class TelegramAdapter extends EventEmitter {
646
724
  // #1537 review: a new start borrows nothing from the last one — not ready until its own onStart, and not stopped.
647
725
  this.pollingReady = false;
648
726
  this.stopped = false;
727
+ this.receiveStop = null;
728
+ const generation = ++this.pollingGeneration, current = () => !this.stopped && this.pollingGeneration === generation;
729
+ this.backlogDrained = false;
730
+ this.backlogUpdateIds.clear();
649
731
  this.queue.start();
650
732
  this._pruneInbox();
651
733
  this.cleanupTimer = setInterval(() => this._pruneInbox(), 60 * 60 * 1000);
@@ -662,15 +744,18 @@ export class TelegramAdapter extends EventEmitter {
662
744
  const MAX_RECONNECT_RETRIES = 10; // auto-reconnect on non-409 errors
663
745
  void (async () => {
664
746
  let reconnects = 0;
665
- while (true) {
666
- for (let attempt = 1; attempt <= MAX_409_RETRIES; attempt++) {
747
+ while (current()) {
748
+ for (let attempt = 1; current() && attempt <= MAX_409_RETRIES; attempt++) {
667
749
  try {
668
750
  await this.bot.start({
669
751
  // Keep the server-side long poll comfortably below both 30s
670
752
  // client deadlines. Equal 30s deadlines race the normal empty
671
753
  // getUpdates response and would churn the HTTP pool while idle.
672
754
  timeout: TELEGRAM_LONG_POLL_TIMEOUT_SECONDS,
673
- drop_pending_updates: attempt === 1 && reconnects === 0,
755
+ // Telegram queues ~24h of updates. Dropping them on a cold start
756
+ // discards messages sent while AgEnD was stopped. Keep them; the
757
+ // adapter delivers recent ones and files older ones as Needs-you.
758
+ drop_pending_updates: false,
674
759
  // Telegram's DEFAULT allowed_updates excludes message_reaction, so
675
760
  // reactions cannot arrive unless the set is listed explicitly. Passing
676
761
  // this list also FREEZES the set: any update type omitted here stops
@@ -680,15 +765,20 @@ export class TelegramAdapter extends EventEmitter {
680
765
  // added here too, or it will never fire.
681
766
  allowed_updates: TELEGRAM_ALLOWED_UPDATES,
682
767
  onStart: (info) => {
768
+ if (!current())
769
+ return;
683
770
  reconnects = 0; // reset on successful start
684
771
  this.pollingReady = true;
685
772
  this.authRejected = false;
773
+ this.discardFloorIfStale(Date.now(), info.id);
686
774
  this.emit("started", info.username, String(info.id));
687
775
  },
688
776
  });
689
777
  return; // bot.stop() was called — clean exit
690
778
  }
691
779
  catch (err) {
780
+ if (!current())
781
+ return;
692
782
  // #1519 P6: what the connection's status reports — not polling now, and whether Telegram refused the token.
693
783
  this.pollingReady = false;
694
784
  if (err instanceof GrammyError && err.error_code === 401)
@@ -719,10 +809,19 @@ export class TelegramAdapter extends EventEmitter {
719
809
  }
720
810
  })();
721
811
  }
722
- async stop() {
723
- // #1537 review: the connection is not polling from the moment it is told to stop (before any await).
812
+ stopReceiving() {
813
+ if (this.receiveStop)
814
+ return this.receiveStop;
815
+ // Set before bot.stop (and its final getUpdates ACK); do not close the outbound queue yet.
724
816
  this.pollingReady = false;
725
817
  this.stopped = true;
818
+ this.pollingGeneration++;
819
+ this.flushPersistFloor();
820
+ this.receiveStop = this.bot.stop();
821
+ return this.receiveStop;
822
+ }
823
+ async stop() {
824
+ const receivingStopped = this.stopReceiving();
726
825
  // Invalidate queued status work before any await. Re-created adapters must
727
826
  // not inherit proof from this object's pending requests.
728
827
  this.reactionGeneration = (this.reactionGeneration ?? 0) + 1;
@@ -732,9 +831,126 @@ export class TelegramAdapter extends EventEmitter {
732
831
  if (this.cleanupTimer)
733
832
  clearInterval(this.cleanupTimer);
734
833
  this.queue.stop();
735
- await this.bot.stop();
736
- this.httpAgent.destroy();
737
- this.httpsAgent.destroy();
834
+ try {
835
+ await receivingStopped;
836
+ }
837
+ finally {
838
+ this.httpAgent.destroy();
839
+ this.httpsAgent.destroy();
840
+ }
841
+ }
842
+ lastUpdateIdFile() {
843
+ return telegramLastUpdateIdPath(this.inboxDir, this.id);
844
+ }
845
+ loadFloor() {
846
+ try {
847
+ const parsed = JSON.parse(readFileSync(this.lastUpdateIdFile(), "utf8"));
848
+ if (!parsed || typeof parsed !== "object")
849
+ return;
850
+ const rec = parsed;
851
+ const id = rec.update_id;
852
+ if (typeof id !== "number" || !Number.isSafeInteger(id) || id < 0)
853
+ return;
854
+ this.lastUpdateId = id;
855
+ this.floorBotId = typeof rec.bot_id === "number" && Number.isSafeInteger(rec.bot_id) ? rec.bot_id : undefined;
856
+ this.floorRecordedAt = typeof rec.recorded_at === "number" && Number.isFinite(rec.recorded_at) ? rec.recorded_at : 0;
857
+ this.discardFloorIfStale(Date.now());
858
+ }
859
+ catch { /* no floor yet */ }
860
+ }
861
+ clearFloor() {
862
+ this.lastUpdateId = 0;
863
+ this.floorBotId = undefined;
864
+ this.floorRecordedAt = 0;
865
+ }
866
+ /** Drop a floor that belongs to another bot, or that is older than a week of silence. */
867
+ discardFloorIfStale(now, botId) {
868
+ if (this.lastUpdateId <= 0)
869
+ return;
870
+ if (this.floorRecordedAt > 0 && now - this.floorRecordedAt > TELEGRAM_UPDATE_ID_FLOOR_MAX_AGE_MS) {
871
+ this.clearFloor();
872
+ return;
873
+ }
874
+ const id = botId ?? (this.bot.isInited() ? this.bot.botInfo.id : undefined);
875
+ if (this.floorBotId != null && id != null && this.floorBotId !== id)
876
+ this.clearFloor();
877
+ }
878
+ schedulePersistFloor() {
879
+ if (this.persistFloorTimer)
880
+ return;
881
+ this.persistFloorTimer = setImmediate(() => {
882
+ this.persistFloorTimer = undefined;
883
+ this.writeFloor();
884
+ });
885
+ }
886
+ flushPersistFloor() {
887
+ if (this.persistFloorTimer) {
888
+ clearImmediate(this.persistFloorTimer);
889
+ this.persistFloorTimer = undefined;
890
+ }
891
+ this.writeFloor();
892
+ }
893
+ writeFloor() {
894
+ try {
895
+ const rec = { update_id: this.lastUpdateId };
896
+ if (this.floorBotId != null)
897
+ rec.bot_id = this.floorBotId;
898
+ if (this.floorRecordedAt > 0)
899
+ rec.recorded_at = this.floorRecordedAt;
900
+ atomicWriteFileSync(this.lastUpdateIdFile(), `${JSON.stringify(rec)}\n`, { mode: 0o600 });
901
+ }
902
+ catch (err) {
903
+ console.warn(`[telegram:${this.id}] could not persist last update_id (${errorMessage(err)})`);
904
+ }
905
+ }
906
+ noteGetUpdatesBacklog(method, result) {
907
+ if (method !== "getUpdates" || this.backlogDrained)
908
+ return;
909
+ if (result && typeof result === "object" && "ok" in result && result.ok !== true)
910
+ return;
911
+ // Transformers see the raw Bot API envelope `{ ok, result }`; callApi unwraps after.
912
+ const updates = result && typeof result === "object" && "result" in result
913
+ ? result.result
914
+ : result;
915
+ if (!Array.isArray(updates))
916
+ return;
917
+ const ids = [];
918
+ for (const item of updates) {
919
+ if (item && typeof item === "object" && typeof item.update_id === "number") {
920
+ ids.push(item.update_id);
921
+ }
922
+ }
923
+ // A new random sequence after a week of silence can land entirely below the stored floor.
924
+ if (ids.length > 0 && this.lastUpdateId > 0 && Math.max(...ids) < this.lastUpdateId) {
925
+ this.clearFloor();
926
+ this.schedulePersistFloor();
927
+ }
928
+ for (const id of ids)
929
+ this.backlogUpdateIds.add(id);
930
+ if (updates.length < TELEGRAM_GETUPDATES_LIMIT)
931
+ this.backlogDrained = true;
932
+ }
933
+ emitMissedOffline(updateId, msg) {
934
+ const userId = msg.from?.id;
935
+ if (userId == null || msg.chat?.id == null)
936
+ return;
937
+ const text = String(msg.text ?? msg.caption ?? "").replace(/\s+/g, " ").trim();
938
+ const preview = text.length > 160 ? `${text.slice(0, 159)}…` : text;
939
+ const threadId = msg.message_thread_id != null ? String(msg.message_thread_id) : undefined;
940
+ const payload = {
941
+ source: "telegram",
942
+ adapterId: this.id,
943
+ updateId,
944
+ chatId: String(msg.chat.id),
945
+ ...(threadId !== undefined ? { threadId } : {}),
946
+ messageId: String(msg.message_id ?? ""),
947
+ userId: String(userId),
948
+ username: msg.from?.username ?? msg.from?.first_name ?? String(userId),
949
+ preview,
950
+ timestamp: new Date((msg.date ?? 0) * 1000),
951
+ ...(msg.chat.title ? { chatTitle: String(msg.chat.title) } : {}),
952
+ };
953
+ this.emit("missed_offline", payload);
738
954
  }
739
955
  /** Delete inbox files older than 1 hour */
740
956
  _pruneInbox() {
@@ -742,6 +958,8 @@ export class TelegramAdapter extends EventEmitter {
742
958
  const now = Date.now();
743
959
  try {
744
960
  for (const name of readdirSync(this.inboxDir)) {
961
+ if (/^telegram-.+-last-update-id$/.test(name))
962
+ continue;
745
963
  const filePath = join(this.inboxDir, name);
746
964
  try {
747
965
  const mtime = statSync(filePath).mtimeMs;
@@ -1413,7 +1631,8 @@ export class TelegramAdapter extends EventEmitter {
1413
1631
  // Construct the download URL
1414
1632
  const token = this.bot.token;
1415
1633
  const url = `${this.apiRoot}/file/bot${token}/${filePath}`;
1416
- const filename = `${Date.now()}-${fileId.slice(-8)}-${filePath.split("/").pop() ?? fileId}`;
1634
+ // #2959: unique per download — the inbox is shared by every bot on the host.
1635
+ const filename = inboxTempName(fileId, filePath.split("/").pop() ?? fileId);
1417
1636
  const localPath = join(this.inboxDir, filename);
1418
1637
  // Download using fetch
1419
1638
  const response = await fetch(url);
@@ -1496,12 +1715,13 @@ export class TelegramAdapter extends EventEmitter {
1496
1715
  return res.message_thread_id;
1497
1716
  }
1498
1717
  async deleteTopic(topicId) {
1499
- // Telegram's General topic is the forum root, not a deletable topic.
1718
+ // Telegram's General topic is the forum root, not a deletable topic. Refused, never "done": a caller reports what
1719
+ // happened (delete_instance's topic_deleted), and silence would read as a deletion.
1500
1720
  if (String(topicId) === "1")
1501
- return;
1721
+ throw new Error("the General topic is never deleted");
1502
1722
  const chatId = this.getChatId();
1503
1723
  if (!chatId)
1504
- return;
1724
+ throw new Error("the Telegram bot is not connected to its group");
1505
1725
  await this.bot.api.raw.deleteForumTopic({ chat_id: Number(chatId), message_thread_id: Number(topicId) });
1506
1726
  }
1507
1727
  async probeTopicPresence(topicId) {